# Copyright (c) 2014 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 distutils import re import shutil import tempfile import netaddr from neutron_lib import constants as n_consts from neutron_lib import exceptions from oslo_config import cfg from oslo_log import log as logging from oslo_utils import uuidutils from oslo_utils import versionutils from neutron.agent.common import ovs_lib from neutron.agent.l3 import ha_router from neutron.agent.l3 import namespaces from neutron.agent.linux import external_process from neutron.agent.linux import ip_lib from neutron.agent.linux import keepalived from neutron.agent.linux import utils as agent_utils from neutron.common import utils as common_utils from neutron.conf.agent.l3 import config as l3_config from neutron.plugins.ml2.drivers.openvswitch.agent.common \ import constants as ovs_const from neutron.privileged.agent.linux import dhcp as priv_dhcp LOG = logging.getLogger(__name__) MINIMUM_DNSMASQ_VERSION = '2.67' DNSMASQ_VERSION_DHCP_RELEASE6 = '2.76' DNSMASQ_VERSION_HOST_ADDR6_LIST = '2.81' DIRECT_PORT_QOS_MIN_OVS_VERSION = '2.11' MINIMUM_DIBBLER_VERSION = '1.0.1' CONNTRACK_GRE_MODULE = 'nf_conntrack_proto_gre' def ovs_vxlan_supported(from_ip='192.0.2.1', to_ip='192.0.2.2'): name = common_utils.get_rand_device_name(prefix='vxlantest-') with ovs_lib.OVSBridge(name) as br: port = br.add_tunnel_port(from_ip, to_ip, n_consts.TYPE_VXLAN) return port != ovs_lib.INVALID_OFPORT def ovs_geneve_supported(from_ip='192.0.2.3', to_ip='192.0.2.4'): name = common_utils.get_rand_device_name(prefix='genevetest-') with ovs_lib.OVSBridge(name) as br: port = br.add_tunnel_port(from_ip, to_ip, n_consts.TYPE_GENEVE) return port != ovs_lib.INVALID_OFPORT def iproute2_vxlan_supported(): ip = ip_lib.IPWrapper() name = common_utils.get_rand_device_name(prefix='vxlantest-') port = ip.add_vxlan(name, 3000) ip.del_veth(name) return name == port.name def patch_supported(): name, peer_name, patch_name = common_utils.get_related_rand_device_names( ['patchtest-', 'peertest0-', 'peertest1-']) with ovs_lib.OVSBridge(name) as br: port = br.add_patch_port(patch_name, peer_name) return port != ovs_lib.INVALID_OFPORT def nova_notify_supported(): try: import neutron.notifiers.nova # noqa since unused return True except ImportError: return False def ofctl_arg_supported(cmd, **kwargs): """Verify if ovs-ofctl binary supports cmd with **kwargs. :param cmd: ovs-ofctl command to use for test. :param **kwargs: arguments to test with the command. :returns: a boolean if the supplied arguments are supported. """ br_name = common_utils.get_rand_device_name(prefix='br-test-') with ovs_lib.OVSBridge(br_name) as test_br: full_args = ["ovs-ofctl", cmd, test_br.br_name, ovs_lib._build_flow_expr_str(kwargs, cmd.split('-')[0], False)] try: agent_utils.execute(full_args, run_as_root=True) except RuntimeError as e: LOG.debug("Exception while checking supported feature via " "command %s. Exception: %s", full_args, e) return False except Exception: LOG.exception("Unexpected exception while checking supported" " feature via command: %s", full_args) return False else: return True def arp_responder_supported(): mac = netaddr.EUI('dead:1234:beef', dialect=netaddr.mac_unix) ip = netaddr.IPAddress('240.0.0.1') actions = ovs_const.ARP_RESPONDER_ACTIONS % {'mac': mac, 'ip': ip} return ofctl_arg_supported(cmd='add-flow', table=21, priority=1, proto='arp', dl_vlan=42, nw_dst='%s' % ip, actions=actions) def arp_header_match_supported(): return ofctl_arg_supported(cmd='add-flow', table=24, priority=1, proto='arp', arp_op='0x2', arp_spa='1.1.1.1', actions="NORMAL") def icmpv6_header_match_supported(): return ofctl_arg_supported(cmd='add-flow', table=ovs_const.ARP_SPOOF_TABLE, priority=1, dl_type=n_consts.ETHERTYPE_IPV6, nw_proto=n_consts.PROTO_NUM_IPV6_ICMP, icmp_type=n_consts.ICMPV6_TYPE_NA, nd_target='fdf8:f53b:82e4::10', actions="NORMAL") def netns_read_requires_helper(): nsname = "netnsreadtest-" + uuidutils.generate_uuid() ip_lib.create_network_namespace(nsname) try: # read without root_helper. if exists, not required. exists = ip_lib.network_namespace_exists(nsname) finally: ip_lib.delete_network_namespace(nsname) return not exists def get_minimal_dnsmasq_version_supported(): return MINIMUM_DNSMASQ_VERSION def get_dnsmasq_version_with_dhcp_release6(): return DNSMASQ_VERSION_DHCP_RELEASE6 def get_dnsmasq_version_with_host_addr6_list(): return DNSMASQ_VERSION_HOST_ADDR6_LIST def get_ovs_version_for_qos_direct_port_support(): return DIRECT_PORT_QOS_MIN_OVS_VERSION def dnsmasq_local_service_supported(): cmd = ['dnsmasq', '--test', '--local-service'] env = {'LC_ALL': 'C'} obj, cmd = agent_utils.create_process(cmd, addl_env=env) _stdout, _stderr = obj.communicate() returncode = obj.returncode if returncode == 127: LOG.debug("Exception while checking dnsmasq version. " "dnsmasq: No such file or directory") return False elif returncode == 1: return False return True def dnsmasq_version_supported(): try: cmd = ['dnsmasq', '--version'] env = {'LC_ALL': 'C'} out = agent_utils.execute(cmd, addl_env=env) m = re.search(r"version (\d+\.\d+)", out) ver = distutils.version.StrictVersion(m.group(1) if m else '0.0') if ver < distutils.version.StrictVersion(MINIMUM_DNSMASQ_VERSION): return False if (cfg.CONF.dnsmasq_enable_addr6_list is True and ver < distutils.version.StrictVersion( DNSMASQ_VERSION_HOST_ADDR6_LIST)): LOG.warning('Support for multiple IPv6 addresses in host ' 'entries was introduced in dnsmasq version ' '%(required)s. Found dnsmasq version %(current)s, ' 'which does not support this feature. Unless support ' 'for multiple IPv6 addresses was backported to the ' 'running build of dnsmasq, the configuration option ' 'dnsmasq_enable_addr6_list should be set to False.', {'required': DNSMASQ_VERSION_HOST_ADDR6_LIST, 'current': ver}) except (OSError, RuntimeError, IndexError, ValueError) as e: LOG.debug("Exception while checking minimal dnsmasq version. " "Exception: %s", e) return False return True def ovs_qos_direct_port_supported(): try: cmd = ['ovs-vsctl', '-V'] out = agent_utils.execute(cmd) matched_line = re.search(r"ovs-vsctl.*", out) matched_version = re.search(r"(\d+\.\d+)", matched_line.group(0)) ver = versionutils.convert_version_to_tuple(matched_version.group(1) if matched_version else '0.0') minver = versionutils.convert_version_to_tuple( DIRECT_PORT_QOS_MIN_OVS_VERSION) if ver < minver: return False except (OSError, RuntimeError, ValueError) as e: LOG.debug("Exception while checking minimal ovs version " "required for supporting direct ports QoS rules. " "Exception: %s", e) return False return True def dhcp_release6_supported(): return priv_dhcp.dhcp_release6_supported() def bridge_firewalling_enabled(): for proto in ('arp', 'ip', 'ip6'): knob = 'net.bridge.bridge-nf-call-%stables' % proto cmd = ['sysctl', '-b', knob] try: out = agent_utils.execute(cmd) except (OSError, RuntimeError, IndexError, ValueError) as e: LOG.debug("Exception while extracting %(knob)s. " "Exception: %(e)s", {'knob': knob, 'e': e}) return False if out == '0': return False return True class KeepalivedIPv6Test(object): def __init__(self, ha_port, gw_port, gw_vip, default_gw): l3_config.register_l3_agent_config_opts(l3_config.OPTS, cfg.CONF) self.ha_port = ha_port self.gw_port = gw_port self.gw_vip = gw_vip self.default_gw = default_gw self.manager = None self.config = None self.config_path = None self.nsname = "keepalivedtest-" + uuidutils.generate_uuid() self.pm = None self.orig_interval = cfg.CONF.AGENT.check_child_processes_interval def configure(self): config = keepalived.KeepalivedConf() instance1 = keepalived.KeepalivedInstance('MASTER', self.ha_port, 1, ['169.254.192.0/18'], advert_int=5) instance1.track_interfaces.append(self.ha_port) # Configure keepalived with an IPv6 address (gw_vip) on gw_port. vip_addr1 = keepalived.KeepalivedVipAddress(self.gw_vip, self.gw_port) instance1.vips.append(vip_addr1) # Configure keepalived with an IPv6 default route on gw_port. gateway_route = keepalived.KeepalivedVirtualRoute(n_consts.IPv6_ANY, self.default_gw, self.gw_port) instance1.virtual_routes.gateway_routes = [gateway_route] config.add_instance(instance1) self.config = config def start_keepalived_process(self): # Disable process monitoring for Keepalived process. cfg.CONF.set_override('check_child_processes_interval', 0, 'AGENT') self.pm = external_process.ProcessMonitor(cfg.CONF, 'router') # Create a temp directory to store keepalived configuration. self.config_path = tempfile.mkdtemp() # Instantiate keepalived manager with the IPv6 configuration. self.manager = keepalived.KeepalivedManager( 'router1', self.config, namespace=self.nsname, process_monitor=self.pm, conf_path=self.config_path) self.manager.spawn() def verify_ipv6_address_assignment(self, gw_dev): process = self.manager.get_process() common_utils.wait_until_true(lambda: process.active) def _gw_vip_assigned(): iface_ip = gw_dev.addr.list(ip_version=6, scope='global') if iface_ip: return self.gw_vip == iface_ip[0]['cidr'] common_utils.wait_until_true(_gw_vip_assigned) def __enter__(self): ip_lib.create_network_namespace(self.nsname) return self def __exit__(self, exc_type, exc_value, exc_tb): if self.pm: self.pm.stop() if self.manager: self.manager.disable() if self.config_path: shutil.rmtree(self.config_path, ignore_errors=True) ip_lib.delete_network_namespace(self.nsname) cfg.CONF.set_override('check_child_processes_interval', self.orig_interval, 'AGENT') def keepalived_ipv6_supported(): """Check if keepalived supports IPv6 functionality. Validation is done as follows. 1. Create a namespace. 2. Create OVS bridge with two ports (ha_port and gw_port) 3. Move the ovs ports to the namespace. 4. Spawn keepalived process inside the namespace with IPv6 configuration. 5. Verify if IPv6 address is assigned to gw_port. 6. Verify if IPv6 default route is configured by keepalived. """ br_name, ha_port, gw_port = common_utils.get_related_rand_device_names( ['ka-test-', ha_router.HA_DEV_PREFIX, namespaces.INTERNAL_DEV_PREFIX]) gw_vip = 'fdf8:f53b:82e4::10/64' expected_default_gw = 'fe80:f816::1' with ovs_lib.OVSBridge(br_name) as br: with KeepalivedIPv6Test(ha_port, gw_port, gw_vip, expected_default_gw) as ka: br.add_port(ha_port, ('type', 'internal')) br.add_port(gw_port, ('type', 'internal')) ha_dev = ip_lib.IPDevice(ha_port) gw_dev = ip_lib.IPDevice(gw_port) ha_dev.link.set_netns(ka.nsname) gw_dev.link.set_netns(ka.nsname) ha_dev.link.set_up() gw_dev.link.set_up() ha_dev.addr.add('169.254.192.8/18') ka.configure() ka.start_keepalived_process() ka.verify_ipv6_address_assignment(gw_dev) default_gw = gw_dev.route.get_gateway(ip_version=6) if default_gw: default_gw = default_gw['via'] return expected_default_gw == default_gw def ovsdb_native_supported(): # Running the test should ensure we are configured for OVSDB native try: ovs = ovs_lib.BaseOVS() ovs.get_bridges() return True except ImportError as ex: LOG.error("Failed to import required modules. Ensure that the " "python-openvswitch package is installed. Error: %s", ex) except Exception: LOG.exception("Unexpected exception occurred.") return False def ovs_conntrack_supported(): br_name = common_utils.get_rand_device_name(prefix="ovs-test-") with ovs_lib.OVSBridge(br_name) as br: try: br.add_protocols(*["OpenFlow%d" % i for i in range(10, 15)]) except RuntimeError as e: LOG.debug("Exception while checking ovs conntrack support: %s", e) return False return ofctl_arg_supported(cmd='add-flow', ct_state='+trk', actions='drop') def ebtables_supported(): try: cmd = ['ebtables', '--version'] agent_utils.execute(cmd) return True except (OSError, RuntimeError, IndexError, ValueError) as e: LOG.debug("Exception while checking for installed ebtables. " "Exception: %s", e) return False def ipset_supported(): try: cmd = ['ipset', '--version'] agent_utils.execute(cmd) return True except (OSError, RuntimeError, IndexError, ValueError) as e: LOG.debug("Exception while checking for installed ipset. " "Exception: %s", e) return False def ip6tables_supported(): try: cmd = ['ip6tables', '--version'] agent_utils.execute(cmd) return True except (OSError, RuntimeError, IndexError, ValueError) as e: LOG.debug("Exception while checking for installed ip6tables. " "Exception: %s", e) return False def conntrack_supported(): try: cmd = ['conntrack', '--version'] agent_utils.execute(cmd) return True except (OSError, RuntimeError, IndexError, ValueError) as e: LOG.debug("Exception while checking for installed conntrack. " "Exception: %s", e) return False def get_minimal_dibbler_version_supported(): return MINIMUM_DIBBLER_VERSION def dibbler_version_supported(): try: cmd = ['dibbler-client', 'help'] out = agent_utils.execute(cmd) return '-w' in out except (OSError, RuntimeError, IndexError, ValueError) as e: LOG.debug("Exception while checking minimal dibbler version. " "Exception: %s", e) return False def _fix_ip_nonlocal_bind_root_value(original_value): current_value = ip_lib.get_ip_nonlocal_bind(namespace=None) if current_value != original_value: ip_lib.set_ip_nonlocal_bind(value=original_value, namespace=None) def ip_nonlocal_bind(): nsname1 = "ipnonlocalbind1-" + uuidutils.generate_uuid() nsname2 = "ipnonlocalbind2-" + uuidutils.generate_uuid() ip_lib.create_network_namespace(nsname1) try: ip_lib.create_network_namespace(nsname2) try: original_value = ip_lib.get_ip_nonlocal_bind(namespace=None) try: ip_lib.set_ip_nonlocal_bind(value=0, namespace=nsname1) ip_lib.set_ip_nonlocal_bind(value=1, namespace=nsname2) ns1_value = ip_lib.get_ip_nonlocal_bind(namespace=nsname1) finally: _fix_ip_nonlocal_bind_root_value(original_value) except RuntimeError as e: LOG.debug("Exception while checking ip_nonlocal_bind. " "Exception: %s", e) return False finally: ip_lib.delete_network_namespace(nsname2) finally: ip_lib.delete_network_namespace(nsname1) return ns1_value == 0 def gre_conntrack_supported(): cmd = ['modinfo', CONNTRACK_GRE_MODULE] try: return agent_utils.execute(cmd, log_fail_as_error=False) except exceptions.ProcessExecutionError: return False