From d0a90239df31dbf0ab8b2d6e3e8fbb7b6501205c Mon Sep 17 00:00:00 2001 From: Tim Rozet Date: Mon, 29 Aug 2016 16:38:19 -0400 Subject: Adds NSDriver to Neutron agent drivers This is added to the entire overcloud, because it only adds driver code and will not impact other scenarios. It will also allow for CI coverage of this commit. This driver is needed for FDIO to be able to use Neutron DHCP agent. JIRA: APEX-249 opnfv-tht-pr: 70 Change-Id: Id2eb55b68dd97c79dcb9e2fd3061a252965e66ae Signed-off-by: Tim Rozet --- build/interface.py | 552 ++++++++++++++++++++++++++++++++++++++++++++++++ build/overcloud-full.sh | 1 + 2 files changed, 553 insertions(+) create mode 100644 build/interface.py (limited to 'build') diff --git a/build/interface.py b/build/interface.py new file mode 100644 index 00000000..709fd677 --- /dev/null +++ b/build/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) diff --git a/build/overcloud-full.sh b/build/overcloud-full.sh index 0984a923..5166bff1 100755 --- a/build/overcloud-full.sh +++ b/build/overcloud-full.sh @@ -144,6 +144,7 @@ LIBGUESTFS_BACKEND=direct virt-customize \ --run-command "cd /etc/puppet/modules/ && tar xzf puppet-tacker.tar.gz" \ --run-command "yum install -y https://dl.dropboxusercontent.com/u/7079970/rabbitmq-server-3.6.3-5.el7ost.noarch.rpm" \ --run-command "pip install python-senlinclient" \ + --upload ../interface.py:/usr/lib/python2.7/site-packages/neutron/agent/linux/ \ -a overcloud-full_build.qcow2 mv -f overcloud-full_build.qcow2 overcloud-full.qcow2 -- cgit 1.2.3-korg