# Copyright 2021 Red Hat, Inc. # # 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 ipaddress import random import re import sys from socket import AF_INET from socket import AF_INET6 from oslo_log import log as logging import pyroute2 from pyroute2.netlink import exceptions as netlink_exceptions import tenacity from ovn_bgp_agent import constants from ovn_bgp_agent import exceptions as agent_exc import ovn_bgp_agent.privileged.linux_net LOG = logging.getLogger(__name__) def get_ip_version(ip): return ipaddress.ip_address(ip.split('/')[0]).version @tenacity.retry( retry=tenacity.retry_if_exception_type( netlink_exceptions.NetlinkDumpInterrupted), wait=tenacity.wait_exponential(multiplier=0.02, max=1), stop=tenacity.stop_after_delay(8), reraise=True) def get_interfaces(filter_out=[]): with pyroute2.NDB() as ndb: return [iface.ifname for iface in ndb.interfaces if iface.ifname not in filter_out] @tenacity.retry( retry=tenacity.retry_if_exception_type( netlink_exceptions.NetlinkDumpInterrupted), wait=tenacity.wait_exponential(multiplier=0.02, max=1), stop=tenacity.stop_after_delay(8), reraise=True) def get_interface_index(nic): with pyroute2.NDB() as ndb: return ndb.interfaces[nic]['index'] def ensure_vrf(vrf_name, vrf_table): ovn_bgp_agent.privileged.linux_net.ensure_vrf(vrf_name, vrf_table) def ensure_bridge(bridge_name): ovn_bgp_agent.privileged.linux_net.ensure_bridge(bridge_name) def ensure_vxlan(vxlan_name, vni, local_ip, dstport): ovn_bgp_agent.privileged.linux_net.ensure_vxlan(vxlan_name, vni, local_ip, dstport) def ensure_veth(veth_name, veth_peer): ovn_bgp_agent.privileged.linux_net.ensure_veth(veth_name, veth_peer) def set_master_for_device(device, master): ovn_bgp_agent.privileged.linux_net.set_master_for_device(device, master) def ensure_dummy_device(device): ovn_bgp_agent.privileged.linux_net.ensure_dummy_device(device) def ensure_ovn_device(ovn_ifname, vrf_name): ensure_dummy_device(ovn_ifname) set_master_for_device(ovn_ifname, vrf_name) def delete_device(device): ovn_bgp_agent.privileged.linux_net.delete_device(device) def ensure_arp_ndp_enabled_for_bridge(bridge, offset, vlan_tag=None): ipv4 = "192.168." + str(int(offset / 256)) + "." + str(offset % 256) ipv6 = "fd53:d91e:400:7f17::%x" % offset try: ovn_bgp_agent.privileged.linux_net.add_ip_to_dev(ipv4, bridge) except KeyError as e: if "object exists" not in str(e): LOG.error("Unable to add IP on bridge %s to enable arp/ndp. " "Exception: %s", bridge, e) raise try: ovn_bgp_agent.privileged.linux_net.add_ip_to_dev(ipv6, bridge) except KeyError as e: if "object exists" not in str(e): LOG.error("Unable to add IP on bridge %s to enable arp/ndp. " "Exception: %s", bridge, e) raise if not vlan_tag: enable_proxy_arp(bridge) enable_proxy_ndp(bridge) def ensure_routing_table_for_bridge(ovn_routing_tables, bridge, vrf_table): # check a routing table with the bridge name exists on # /etc/iproute2/rt_tables regex = r'^[0-9]*[\s]*{}$'.format(bridge) matching_table = [line.replace('\t', ' ') for line in open('/etc/iproute2/rt_tables') if re.findall(regex, line)] if matching_table: table_info = matching_table[0].strip().split() ovn_routing_tables[table_info[1]] = int(table_info[0]) LOG.debug("Found routing table for %s with: %s", bridge, table_info) # if not configured, add random number for the table else: LOG.debug("Routing table for bridge %s not configured " "at /etc/iproute2/rt_tables", bridge) regex = r'^[0-9]+[\s]*' existing_routes = [int(line.replace('\t', ' ').split(' ')[0]) for line in open('/etc/iproute2/rt_tables') if re.findall(regex, line)] # pick a number between 1 and 252 try: table_number = random.choice(list( set([x for x in range(1, 253) if x != int(vrf_table)]).difference( set(existing_routes)))) except IndexError: LOG.error("No more routing tables available for bridge %s " "at /etc/iproute2/rt_tables", bridge) sys.exit() ovn_bgp_agent.privileged.linux_net.create_routing_table_for_bridge( table_number, bridge) ovn_routing_tables[bridge] = int(table_number) LOG.debug("Added routing table for %s with number: %s", bridge, table_number) return _ensure_routing_table_routes(ovn_routing_tables, bridge) @tenacity.retry( retry=tenacity.retry_if_exception_type( netlink_exceptions.NetlinkDumpInterrupted), wait=tenacity.wait_exponential(multiplier=0.02, max=1), stop=tenacity.stop_after_delay(8), reraise=True) def _ensure_routing_table_routes(ovn_routing_tables, bridge): # add default route on that table if it does not exist extra_routes = [] with pyroute2.NDB() as ndb: table_route_dsts = set( [ (r.dst, r.dst_len) for r in ndb.routes.summary().filter( table=ovn_routing_tables[bridge] ) ] ) if not table_route_dsts: r1 = {'dst': 'default', 'oif': ndb.interfaces[bridge]['index'], 'table': ovn_routing_tables[bridge], 'scope': 253, 'proto': 3} ovn_bgp_agent.privileged.linux_net.route_create(r1) r2 = {'dst': 'default', 'oif': ndb.interfaces[bridge]['index'], 'table': ovn_routing_tables[bridge], 'family': AF_INET6, 'proto': 3} ovn_bgp_agent.privileged.linux_net.route_create(r2) else: route_missing = True route6_missing = True for (dst, dst_len) in table_route_dsts: if not dst: # default route try: route = ndb.routes[ {'table': ovn_routing_tables[bridge], 'dst': '', 'family': AF_INET}] if (bridge == ndb.interfaces[{'index': route['oif']}][ 'ifname']): route_missing = False else: extra_routes.append(route) except KeyError: pass # no ipv4 default rule try: route_6 = ndb.routes[ {'table': ovn_routing_tables[bridge], 'dst': '', 'family': AF_INET6}] if (bridge == ndb.interfaces[{'index': route_6['oif']}][ 'ifname']): route6_missing = False else: extra_routes.append(route_6) except KeyError: pass # no ipv6 default rule else: if get_ip_version(dst) == constants.IP_VERSION_6: extra_routes.append( ndb.routes[{'table': ovn_routing_tables[bridge], 'dst': dst, 'dst_len': dst_len, 'family': AF_INET6}] ) else: extra_routes.append( ndb.routes[{'table': ovn_routing_tables[bridge], 'dst': dst, 'dst_len': dst_len, 'family': AF_INET}] ) if route_missing: r = {'dst': 'default', 'oif': ndb.interfaces[bridge]['index'], 'table': ovn_routing_tables[bridge], 'scope': 253, 'proto': 3} ovn_bgp_agent.privileged.linux_net.route_create(r) if route6_missing: r = {'dst': 'default', 'oif': ndb.interfaces[bridge]['index'], 'table': ovn_routing_tables[bridge], 'family': AF_INET6, 'proto': 3} ovn_bgp_agent.privileged.linux_net.route_create(r) return extra_routes def ensure_vlan_device_for_network(bridge, vlan_tag): ovn_bgp_agent.privileged.linux_net.ensure_vlan_device_for_network(bridge, vlan_tag) device = "{}/{}".format(bridge, vlan_tag) enable_proxy_arp(device) enable_proxy_ndp(device) def delete_vlan_device_for_network(bridge, vlan_tag): vlan_device_name = '{}.{}'.format(bridge, vlan_tag) delete_device(vlan_device_name) def enable_proxy_ndp(device): flag = "net.ipv6.conf.{}.proxy_ndp".format(device) ovn_bgp_agent.privileged.linux_net.set_kernel_flag(flag, 1) def enable_proxy_arp(device): flag = "net.ipv4.conf.{}.proxy_arp".format(device) ovn_bgp_agent.privileged.linux_net.set_kernel_flag(flag, 1) @tenacity.retry( retry=tenacity.retry_if_exception_type( netlink_exceptions.NetlinkDumpInterrupted), wait=tenacity.wait_exponential(multiplier=0.02, max=1), stop=tenacity.stop_after_delay(8), reraise=True) def get_exposed_ips(nic): exposed_ips = [] with pyroute2.NDB() as ndb: exposed_ips = [ip.address for ip in ndb.interfaces[nic].ipaddr.summary() if ip.prefixlen == 32 or ip.prefixlen == 128] return exposed_ips @tenacity.retry( retry=tenacity.retry_if_exception_type( netlink_exceptions.NetlinkDumpInterrupted), wait=tenacity.wait_exponential(multiplier=0.02, max=1), stop=tenacity.stop_after_delay(8), reraise=True) def get_nic_ip(nic, prefixlen_filter=None): exposed_ips = [] with pyroute2.NDB() as ndb: if prefixlen_filter: exposed_ips = [ip.address for ip in ndb.interfaces[nic].ipaddr.summary( ).filter(prefixlen=prefixlen_filter)] else: exposed_ips = [ip.address for ip in ndb.interfaces[nic].ipaddr.summary()] return exposed_ips @tenacity.retry( retry=tenacity.retry_if_exception_type( netlink_exceptions.NetlinkDumpInterrupted), wait=tenacity.wait_exponential(multiplier=0.02, max=1), stop=tenacity.stop_after_delay(8), reraise=True) def get_exposed_ips_on_network(nic, network): exposed_ips = [] with pyroute2.NDB() as ndb: try: exposed_ips = [ip.address for ip in ndb.interfaces[nic].ipaddr.summary() if ((ip.prefixlen == 32 or ip.prefixlen == 128) and ipaddress.ip_address(ip.address) in network)] except KeyError: # Nic does not exists LOG.debug("Nic %s does not yet exists, so it does not have " "exposed IPs", nic) return exposed_ips @tenacity.retry( retry=tenacity.retry_if_exception_type( netlink_exceptions.NetlinkDumpInterrupted), wait=tenacity.wait_exponential(multiplier=0.02, max=1), stop=tenacity.stop_after_delay(8), reraise=True) def get_exposed_routes_on_network(table_ids, network): with pyroute2.NDB() as ndb: # NOTE: skip bgp routes (proto 186) return [ r for r in ndb.routes.dump() if r.table in table_ids and r.dst != "" and r.gateway is not None and r.proto != 186 and ipaddress.ip_address(r.gateway) in network ] @tenacity.retry( retry=tenacity.retry_if_exception_type( netlink_exceptions.NetlinkDumpInterrupted), wait=tenacity.wait_exponential(multiplier=0.02, max=1), stop=tenacity.stop_after_delay(8), reraise=True) def get_ovn_ip_rules(routing_table): # get the rules pointing to ovn bridges ovn_ip_rules = {} with pyroute2.NDB() as ndb: rules_info = [(rule.table, "{}/{}".format(rule.dst, rule.dst_len), rule.family) for rule in ndb.rules.dump() if rule.table in routing_table] for table, dst, family in rules_info: ovn_ip_rules[dst] = {'table': table, 'family': family} return ovn_ip_rules def delete_exposed_ips(ips, nic): ovn_bgp_agent.privileged.linux_net.delete_exposed_ips(ips, nic) def delete_ip_rules(ip_rules): ovn_bgp_agent.privileged.linux_net.delete_ip_rules(ip_rules) def delete_bridge_ip_routes(routing_tables, routing_tables_routes, extra_routes): with pyroute2.NDB() as ndb: for device, routes_info in routing_tables_routes.items(): if not extra_routes.get(device): continue for route_info in routes_info: oif = ndb.interfaces[device]['index'] if route_info['vlan']: vlan_device_name = '{}.{}'.format(device, route_info['vlan']) oif = ndb.interfaces[vlan_device_name]['index'] if 'gateway' in route_info['route'].keys(): # subnet route possible_matchings = [ r for r in extra_routes[device] if (r['dst'] == route_info['route']['dst'] and r['dst_len'] == route_info['route']['dst_len'] and r['gateway'] == route_info['route']['gateway'])] else: # cr-lrp possible_matchings = [ r for r in extra_routes[device] if (r['dst'] == route_info['route']['dst'] and r['dst_len'] == route_info['route']['dst_len'] and r['oif'] == oif)] for r in possible_matchings: extra_routes[device].remove(r) for bridge, routes in extra_routes.items(): for route in routes: r_info = {'dst': route['dst'], 'dst_len': route['dst_len'], 'family': route['family'], 'oif': route['oif'], 'gateway': route['gateway'], 'table': routing_tables[bridge]} ovn_bgp_agent.privileged.linux_net.route_delete(r_info) def delete_routes_from_table(table): table_routes = _get_table_routes(table) delete_ip_routes(table_routes) @tenacity.retry( retry=tenacity.retry_if_exception_type( netlink_exceptions.NetlinkDumpInterrupted), wait=tenacity.wait_exponential(multiplier=0.02, max=1), stop=tenacity.stop_after_delay(8), reraise=True) def _get_table_routes(table): with pyroute2.NDB() as ndb: # FIXME: problem in pyroute2 removing routes with local (254) scope return [r for r in ndb.routes.dump().filter(table=table) if r.scope != 254 and r.proto != 186] @tenacity.retry( retry=tenacity.retry_if_exception_type( netlink_exceptions.NetlinkDumpInterrupted), wait=tenacity.wait_exponential(multiplier=0.02, max=1), stop=tenacity.stop_after_delay(8), reraise=True) def get_routes_on_tables(table_ids): with pyroute2.NDB() as ndb: # NOTE: skip bgp routes (proto 186) return [r for r in ndb.routes.dump() if r.table in table_ids and r.dst != '' and r.proto != 186] def delete_ip_routes(routes): for route in routes: r_info = {'dst': route['dst'], 'dst_len': route['dst_len'], 'family': route['family'], 'oif': route['oif'], 'gateway': route['gateway'], 'table': route['table']} ovn_bgp_agent.privileged.linux_net.route_delete(r_info) def add_ndp_proxy(ip, dev, vlan=None): ovn_bgp_agent.privileged.linux_net.add_ndp_proxy(ip, dev, vlan) def del_ndp_proxy(ip, dev, vlan=None): ovn_bgp_agent.privileged.linux_net.del_ndp_proxy(ip, dev, vlan) def add_ips_to_dev(nic, ips, clear_local_route_at_table=False): already_added_ips = [] for ip in ips: try: ovn_bgp_agent.privileged.linux_net.add_ip_to_dev(ip, nic) except KeyError: # NDB raises KeyError: 'object exists' # if the ip is already added already_added_ips.append(ip) if clear_local_route_at_table: for ip in ips: if ip in already_added_ips: continue with pyroute2.NDB() as ndb: oif = ndb.interfaces[nic]['index'] route = {'table': clear_local_route_at_table, 'proto': 2, 'scope': 254, 'dst': ip, 'oif': oif} ovn_bgp_agent.privileged.linux_net.route_delete(route) def del_ips_from_dev(nic, ips): for ip in ips: ovn_bgp_agent.privileged.linux_net.del_ip_from_dev(ip, nic) def add_ip_rule(ip, table, dev=None, lladdr=None): ip_version = get_ip_version(ip) ip_info = ip.split("/") if len(ip_info) == 1: rule = {'dst': ip_info[0], 'table': table, 'dst_len': 32} if ip_version == constants.IP_VERSION_6: rule['dst_len'] = 128 rule['family'] = AF_INET6 elif len(ip_info) == 2: rule = {'dst': ip_info[0], 'table': table, 'dst_len': int(ip_info[1])} if ip_version == constants.IP_VERSION_6: rule['family'] = AF_INET6 else: raise agent_exc.InvalidPortIP(ip=ip) ovn_bgp_agent.privileged.linux_net.rule_create(rule) if lladdr: add_ip_nei(ip, lladdr, dev) def add_ip_nei(ip, lladdr, dev): """Add ip neighbor permament entry param ip: IP of the neighbor to add an entry for param lladdr: link layer address of the neighbor to associate to that IP param dev: the interface to which the neighbor is attached """ # FIXME: There is no support for creating neighbours in NDB # So we are using iproute here ovn_bgp_agent.privileged.linux_net.add_ip_nei(ip, lladdr, dev) def del_ip_rule(ip, table, dev=None, lladdr=None): ip_version = get_ip_version(ip) ip_info = ip.split("/") if len(ip_info) == 1: rule = {'dst': ip_info[0], 'table': table, 'dst_len': 32} if ip_version == constants.IP_VERSION_6: rule['dst_len'] = 128 rule['family'] = AF_INET6 elif len(ip_info) == 2: rule = {'dst': ip_info[0], 'table': table, 'dst_len': int(ip_info[1])} if ip_version == constants.IP_VERSION_6: rule['family'] = AF_INET6 else: raise agent_exc.InvalidPortIP(ip=ip) ovn_bgp_agent.privileged.linux_net.rule_delete(rule) if lladdr: del_ip_nei(ip, lladdr, dev) def del_ip_nei(ip, lladdr, dev): """Del ip neighbor permament entry param ip: IP of the neighbor to delete the entry param lladdr: link layer address of the neighbor to disassociate param dev: the interface to which the neighbor is attached """ # FIXME: There is no support for deleting neighbours in NDB # So we are using iproute here ovn_bgp_agent.privileged.linux_net.del_ip_nei(ip, lladdr, dev) def add_unreachable_route(vrf_name): ovn_bgp_agent.privileged.linux_net.add_unreachable_route(vrf_name) def add_ip_route(ovn_routing_tables_routes, ip_address, route_table, dev, vlan=None, mask=None, via=None): net_ip = ip_address if not mask: # default /32 or /128 if get_ip_version(ip_address) == constants.IP_VERSION_6: mask = 128 else: mask = 32 else: ip = '{}/{}'.format(ip_address, mask) if get_ip_version(ip_address) == constants.IP_VERSION_6: net_ip = '{}'.format(ipaddress.IPv6Network( ip, strict=False).network_address) else: net_ip = '{}'.format(ipaddress.IPv4Network( ip, strict=False).network_address) with pyroute2.NDB() as ndb: if vlan: oif_name = '{}.{}'.format(dev, vlan) try: oif = ndb.interfaces[oif_name]['index'] except KeyError: # Most provider network was recently created an # there has not been a sync since then, therefore # the vlan device has not yet been created # Trying to create the device and retrying ensure_vlan_device_for_network(dev, vlan) oif = ndb.interfaces[oif_name]['index'] else: oif = ndb.interfaces[dev]['index'] route = {'dst': net_ip, 'dst_len': int(mask), 'oif': oif, 'table': int(route_table), 'proto': 3} if via: route['gateway'] = via route['scope'] = 0 else: route['scope'] = 253 if get_ip_version(net_ip) == constants.IP_VERSION_6: route['family'] = AF_INET6 del route['scope'] with pyroute2.NDB() as ndb: try: with ndb.routes[route]: LOG.debug("Route already existing: %s", route) except KeyError: LOG.debug("Creating route at table %s: %s", route_table, route) ovn_bgp_agent.privileged.linux_net.route_create(route) LOG.debug("Route created at table %s: %s", route_table, route) route_info = {'vlan': vlan, 'route': route} ovn_routing_tables_routes.setdefault(dev, []).append(route_info) def del_ip_route(ovn_routing_tables_routes, ip_address, route_table, dev, vlan=None, mask=None, via=None): net_ip = ip_address if not mask: # default /32 or /128 if get_ip_version(ip_address) == constants.IP_VERSION_6: mask = 128 else: mask = 32 else: ip = '{}/{}'.format(ip_address, mask) if get_ip_version(ip_address) == constants.IP_VERSION_6: net_ip = '{}'.format(ipaddress.IPv6Network( ip, strict=False).network_address) else: net_ip = '{}'.format(ipaddress.IPv4Network( ip, strict=False).network_address) with pyroute2.NDB() as ndb: try: if vlan: oif_name = '{}.{}'.format(dev, vlan) oif = ndb.interfaces[oif_name]['index'] else: oif = ndb.interfaces[dev]['index'] except KeyError: LOG.debug("Device %s does not exists, so the associated " "routes should have been automatically deleted.", dev) ovn_routing_tables_routes.pop(dev, None) return route = {'dst': net_ip, 'dst_len': int(mask), 'oif': oif, 'table': int(route_table), 'proto': 3} if via: route['gateway'] = via route['scope'] = 0 else: route['scope'] = 253 if get_ip_version(net_ip) == constants.IP_VERSION_6: route['family'] = AF_INET6 del route['scope'] LOG.debug("Deleting route at table %s: %s", route_table, route) ovn_bgp_agent.privileged.linux_net.route_delete(route) LOG.debug("Route deleted at table %s: %s", route_table, route) route_info = {'vlan': vlan, 'route': route} if route_info in ovn_routing_tables_routes.get(dev, []): ovn_routing_tables_routes[dev].remove(route_info) def set_device_status(device, status, ndb=None): ovn_bgp_agent.privileged.linux_net.set_device_status( device, status, ndb=ndb)