From e984522f55bc1e024b4f5225903a1592219e1f86 Mon Sep 17 00:00:00 2001 From: Tim Rozet Date: Wed, 31 Aug 2016 15:33:03 -0400 Subject: Modifies L3 Agent for FDIO Deployments L3 Agent needs to use OVS driver for external interfaces and NSDriver for Internal interfaces. JIRA: APEX-252 opnfv-tht-pr: 73 Change-Id: Ic96e30421a1f1308c358a644e5602ca738091695 Signed-off-by: Tim Rozet --- build/neutron/agent/interface/interface.py | 552 +++++++++++++++++++++++++++++ 1 file changed, 552 insertions(+) create mode 100644 build/neutron/agent/interface/interface.py (limited to 'build/neutron/agent/interface/interface.py') diff --git a/build/neutron/agent/interface/interface.py b/build/neutron/agent/interface/interface.py new file mode 100644 index 00000000..709fd677 --- /dev/null +++ b/build/neutron/agent/interface/interface.py @@ -0,0 +1,552 @@ +# 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 eventlet +import netaddr +from oslo_config import cfg +from oslo_log import log as logging +import six + +from neutron._i18n import _, _LE, _LI, _LW +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 + + +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 OVS interface or not. ' + 'Support kernels with limited namespace support ' + '(e.g. RHEL 6.5) so long as ovs_use_veth is set to ' + 'True.')), + cfg.IntOpt('network_device_mtu', + deprecated_for_removal=True, + help=_('MTU setting for device. This option will be removed in ' + 'Newton. Please use the system-wide segment_mtu setting ' + 'which the agents will take into account when wiring ' + 'VIFs.')), +] + + +@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) + + @property + def use_gateway_ips(self): + """Whether to use gateway IPs instead of unique IP allocations. + + In each place where the DHCP agent runs, and for each subnet for + which DHCP is handling out IP addresses, the DHCP port needs - + at the Linux level - to have an IP address within that subnet. + Generally this needs to be a unique Neutron-allocated IP + address, because the subnet's underlying L2 domain is bridged + across multiple compute hosts and network nodes, and for HA + there may be multiple DHCP agents running on that same bridged + L2 domain. + + However, if the DHCP ports - on multiple compute/network nodes + but for the same network - are _not_ bridged to each other, + they do not need each to have a unique IP address. Instead + they can all share the same address from the relevant subnet. + This works, without creating any ambiguity, because those + ports are not all present on the same L2 domain, and because + no data within the network is ever sent to that address. + (DHCP requests are broadcast, and it is the network's job to + ensure that such a broadcast will reach at least one of the + available DHCP servers. DHCP responses will be sent _from_ + the DHCP port address.) + + Specifically, for networking backends where it makes sense, + the DHCP agent allows all DHCP ports to use the subnet's + gateway IP address, and thereby to completely avoid any unique + IP address allocation. This behaviour is selected by running + the DHCP agent with a configured interface driver whose + 'use_gateway_ips' property is True. + + When an operator deploys Neutron with an interface driver that + makes use_gateway_ips True, they should also ensure that a + gateway IP address is defined for each DHCP-enabled subnet, + and that the gateway IP address doesn't change during the + subnet's lifetime. + """ + return False + + def init_l3(self, device_name, ip_cidrs, namespace=None, + preserve_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 + clean_connections: Boolean to indicate if we should cleanup connections + associated to removed ips + """ + preserve_ips = preserve_ips or [] + 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) + + def init_router_port(self, + device_name, + ip_cidrs, + namespace, + preserve_ips=None, + extra_subnets=None, + 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 + 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 [], + clean_connections=clean_connections) + + device = ip_lib.IPDevice(device_name, namespace=namespace) + + # Manage on-link routes (routes without an associated address) + new_onlink_cidrs = set(s['cidr'] for s in extra_subnets or []) + + v4_onlink = device.route.list_onlink_routes(n_const.IP_VERSION_4) + v6_onlink = device.route.list_onlink_routes(n_const.IP_VERSION_6) + existing_onlink_cidrs = set(r['cidr'] for r in v4_onlink + v6_onlink) + + for route in new_onlink_cidrs - existing_onlink_cidrs: + LOG.debug("adding onlink route(%s)", route) + device.route.add_onlink_route(route) + for route in (existing_onlink_cidrs - new_onlink_cidrs - + set(preserve_ips or [])): + 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, mtu=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, mtu=None): + if not ip_lib.device_exists(device_name, + namespace=namespace): + try: + self.plug_new(network_id, port_id, device_name, mac_address, + bridge, namespace, prefix, mtu) + except TypeError: + 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.""" + + @property + def bridged(self): + """Whether the DHCP port is bridged to the VM TAP interfaces. + + When the DHCP port is bridged to the TAP interfaces for the + VMs for which it is providing DHCP service - as is the case + for most Neutron network implementations - the DHCP server + only needs to listen on the DHCP port, and will still receive + DHCP requests from all the relevant VMs. + + If the DHCP port is not bridged to the relevant VM TAP + interfaces, the DHCP server needs to listen explicitly on + those TAP interfaces, and to treat those as aliases of the + DHCP port where the IP subnet is defined. + """ + return True + + +class NullDriver(LinuxInterfaceDriver): + def plug_new(self, network_id, port_id, device_name, mac_address, + bridge=None, namespace=None, prefix=None, mtu=None): + pass + + def unplug(self, device_name, bridge=None, namespace=None, prefix=None): + pass + +class NSDriver(LinuxInterfaceDriver): + """Device independent driver enabling creation of a non device specific + interface in network spaces. Attachment to the device is not performed. + """ + MAX_TIME_FOR_DEVICE_EXISTENCE = 30 + + @classmethod + def _device_is_created_in_time(cls, device_name): + """See if device is created, within time limit.""" + attempt = 0 + while attempt < NSDriver.MAX_TIME_FOR_DEVICE_EXISTENCE: + if ip_lib.device_exists(device_name): + return True + attempt += 1 + eventlet.sleep(1) + LOG.error(_LE("Device %(dev)s was not created in %(time)d seconds"), + {'dev': device_name, + 'time': NSDriver.MAX_TIME_FOR_DEVICE_EXISTENCE}) + return False + + def _configure_mtu(self, ns_dev, mtu=None): + # Need to set MTU, after added to namespace. See review + # https://review.openstack.org/327651 + try: + # Note: network_device_mtu will be deprecated in future + mtu_override = self.conf.network_device_mtu + except cfg.NoSuchOptError: + LOG.warning(_LW("Config setting for MTU deprecated - any " + "override will be ignored.")) + mtu_override = None + if mtu_override: + mtu = mtu_override + LOG.debug("Overriding MTU to %d", mtu) + if mtu: + ns_dev.link.set_mtu(mtu) + else: + LOG.debug("No MTU provided - skipping setting value") + + def plug(self, network_id, port_id, device_name, mac_address, + bridge=None, namespace=None, prefix=None, mtu=None): + + # Overriding this, we still want to add an existing device into the + # namespace. + self.plug_new(network_id, port_id, device_name, mac_address, + bridge, namespace, prefix, mtu) + + def plug_new(self, network_id, port_id, device_name, mac_address, + bridge=None, namespace=None, prefix=None, mtu=None): + + ip = ip_lib.IPWrapper() + ns_dev = ip.device(device_name) + + LOG.debug("Plugging dev: '%s' into namespace: '%s' ", + device_name, namespace) + + # Wait for device creation + if not self._device_is_created_in_time(device_name): + return + + ns_dev.link.set_address(mac_address) + + if namespace: + namespace_obj = ip.ensure_namespace(namespace) + namespace_obj.add_device_to_namespace(ns_dev) + + self._configure_mtu(ns_dev, mtu) + + ns_dev.link.set_up() + + def unplug(self, device_name, bridge=None, namespace=None, prefix=None): + # Device removal is done externally. Just remove the namespace + LOG.debug("Removing namespace: '%s'", namespace) + ip_lib.IPWrapper(namespace).garbage_collect_namespace() + + +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, mtu=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) + root_dev.disable_ipv6() + 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) + + # 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) + + # NOTE(ihrachys): the order here is significant: we must set MTU after + # the device is moved into a namespace, otherwise OVS bridge does not + # allow to set MTU that is higher than the least of all device MTUs on + # the bridge + mtu = self.conf.network_device_mtu or mtu + if mtu: + ns_dev.link.set_mtu(mtu) + if self.conf.ovs_use_veth: + root_dev.link.set_mtu(mtu) + else: + LOG.warning(_LW("No MTU configured for port %s"), port_id) + + 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 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, mtu=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) + root_dev.disable_ipv6() + + self._ivs_add_port(tap_name, port_id, mac_address) + + ns_dev = ip.device(device_name) + ns_dev.link.set_address(mac_address) + + mtu = self.conf.network_device_mtu or mtu + if mtu: + ns_dev.link.set_mtu(mtu) + root_dev.link.set_mtu(mtu) + else: + LOG.warning(_LW("No MTU configured for port %s"), port_id) + + 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, mtu=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) + root_veth.disable_ipv6() + ns_veth.link.set_address(mac_address) + + mtu = self.conf.network_device_mtu or mtu + if mtu: + root_veth.link.set_mtu(mtu) + ns_veth.link.set_mtu(mtu) + else: + LOG.warning(_LW("No MTU configured for port %s"), port_id) + + 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) -- cgit 1.2.3-korg