deb-ryu/ryu/services/protocols/bgp/core_managers/table_manager.py

585 lines
23 KiB
Python

import logging
import netaddr
from collections import OrderedDict
from ryu.services.protocols.bgp.base import SUPPORTED_GLOBAL_RF
from ryu.services.protocols.bgp.info_base.rtc import RtcTable
from ryu.services.protocols.bgp.info_base.ipv4 import Ipv4Path
from ryu.services.protocols.bgp.info_base.ipv4 import Ipv4Table
from ryu.services.protocols.bgp.info_base.ipv6 import Ipv6Path
from ryu.services.protocols.bgp.info_base.ipv6 import Ipv6Table
from ryu.services.protocols.bgp.info_base.vpnv4 import Vpnv4Path
from ryu.services.protocols.bgp.info_base.vpnv4 import Vpnv4Table
from ryu.services.protocols.bgp.info_base.vpnv6 import Vpnv6Path
from ryu.services.protocols.bgp.info_base.vpnv6 import Vpnv6Table
from ryu.services.protocols.bgp.info_base.vrf4 import Vrf4Table
from ryu.services.protocols.bgp.info_base.vrf6 import Vrf6Table
from ryu.services.protocols.bgp.rtconf import vrfs
from ryu.services.protocols.bgp.rtconf.vrfs import VRF_RF_IPV4
from ryu.services.protocols.bgp.rtconf.vrfs import VRF_RF_IPV6
from ryu.lib.packet.bgp import RF_IPv4_UC
from ryu.lib.packet.bgp import RF_IPv6_UC
from ryu.lib.packet.bgp import RF_IPv4_VPN
from ryu.lib.packet.bgp import RF_IPv6_VPN
from ryu.lib.packet.bgp import RF_RTC_UC
from ryu.lib.packet.bgp import BGPPathAttributeOrigin
from ryu.lib.packet.bgp import BGPPathAttributeAsPath
from ryu.lib.packet.bgp import BGP_ATTR_TYPE_ORIGIN
from ryu.lib.packet.bgp import BGP_ATTR_TYPE_AS_PATH
from ryu.lib.packet.bgp import BGP_ATTR_ORIGIN_IGP
from ryu.lib.packet.bgp import IPAddrPrefix
from ryu.lib.packet.bgp import IP6AddrPrefix
from ryu.services.protocols.bgp.utils.validation import is_valid_ipv4
from ryu.services.protocols.bgp.utils.validation import is_valid_ipv4_prefix
from ryu.services.protocols.bgp.utils.validation import is_valid_ipv6
from ryu.services.protocols.bgp.utils.validation import is_valid_ipv6_prefix
LOG = logging.getLogger('bgpspeaker.core_managers.table_mixin')
class TableCoreManager(object):
"""Methods performing core operations on tables."""
def __init__(self, core_service, common_conf):
self._tables = {}
self._rt_mgr = core_service.rt_manager
self._signal_bus = core_service.signal_bus
# (VRF) Tables to which the routes with a given route target
# should be imported.
#
# Key: RouteTarget
# Value: List of tables.
self._tables_for_rt = {}
# Global/Default tables, keyed by RouteFamily.
self._global_tables = {}
self._core_service = core_service
self._signal_bus = self._core_service.signal_bus
# VPN label range
self._asbr_label_range = common_conf.label_range
self._next_vpnv4_label = int(self._asbr_label_range[0])
self._next_hop_label = {}
@property
def global_tables(self):
return self._global_tables
def remove_vrf_by_vrf_conf(self, vrf_conf):
route_family = vrf_conf.route_family
assert route_family in (vrfs.VRF_RF_IPV4, vrfs.VRF_RF_IPV6)
table_id = (vrf_conf.route_dist, route_family)
vrf_table = self._tables.pop(table_id)
self._remove_links_to_vrf_table(vrf_table)
# Withdraw the best-path whose source was NC since it may have been
# exported to VPN table.
for destination in vrf_table.values():
best_path = destination.best_path
if best_path and best_path.source is None:
vpn_clone = best_path.clone_to_vpn(vrf_conf.route_dist,
for_withdrawal=True)
self.learn_path(vpn_clone)
LOG.debug('VRF with RD %s marked for removal', vrf_conf.route_dist)
def import_all_vpn_paths_to_vrf(self, vrf_table, import_rts=None):
"""Imports Vpnv4/6 paths from Global/VPN table into given Vrfv4/6
table.
:param vrf_table: Vrf table to which we import
:type vrf_table: VrfTable
:param import_rts: import RTs to override default import_rts of
vrf table for this import
:type import_rts: set of strings
Checks if we have any path RT common with VRF table's import RT.
"""
rfs = (Vrf4Table.ROUTE_FAMILY, Vrf6Table.ROUTE_FAMILY)
assert vrf_table.route_family in rfs, 'Invalid VRF table.'
if vrf_table.route_family == Vrf4Table.ROUTE_FAMILY:
vpn_table = self.get_vpn4_table()
else:
vpn_table = self.get_vpn6_table()
vrf_table.import_vpn_paths_from_table(vpn_table, import_rts)
def learn_path(self, path):
"""Inserts `path` into correct global table.
Since known paths to `Destination` has changes, we queue it for further
processing.
"""
# Get VPN/Global table
table = self.get_global_table_by_route_family(path.route_family)
gpath_dest = table.insert(path)
# Since destination was updated, we enqueue it for processing.
self._signal_bus.dest_changed(gpath_dest)
def remember_sent_route(self, sent_route):
"""Records `sent_route` inside proper table.
Records of `sent_route` from Adj-RIB-out.
"""
route_family = sent_route.path.route_family
table = self.get_global_table_by_route_family(route_family)
table.insert_sent_route(sent_route)
def on_interesting_rts_change(self, new_global_rts, removed_global_rts):
"""Update global tables as interested RTs changed.
Adds `new_rts` and removes `removed_rts` rt nlris. Does not check if
`new_rts` or `removed_rts` are already present. Schedules refresh
request to peers that do not participate in RTC address-family.
"""
# We add new RT NLRI and request RR for other peers.
if new_global_rts:
LOG.debug(
'Sending route_refresh to all neighbors that'
' did not negotiate RTC capability.'
)
pm = self._core_service.peer_manager
pm.schedule_rr_to_non_rtc_peers()
if removed_global_rts:
LOG.debug(
'Cleaning up global tables as some interested RTs were removed'
)
self._clean_global_uninteresting_paths()
def get_global_table_by_route_family(self, route_family):
if route_family not in SUPPORTED_GLOBAL_RF:
raise ValueError(
'Given route family: %s currently not supported' % route_family
)
global_table = None
if route_family == RF_IPv4_UC:
global_table = self.get_ipv4_table()
elif route_family == RF_IPv6_UC:
global_table = self.get_ipv6_table()
elif route_family == RF_IPv4_VPN:
global_table = self.get_vpn4_table()
elif route_family == RF_IPv6_VPN:
global_table = self.get_vpn6_table()
elif route_family == RF_RTC_UC:
global_table = self.get_rtc_table()
return global_table
def get_vrf_table(self, vrf_rd, vrf_rf):
assert vrf_rd is not None
return self._tables.get((vrf_rd, vrf_rf))
def get_vrf_tables(self, vrf_rf=None):
vrf_tables = {}
for (scope_id, table_id), table in self._tables.items():
if scope_id is None:
continue
if vrf_rf is not None and table_id != vrf_rf:
continue
vrf_tables[(scope_id, table_id)] = table
return vrf_tables
def get_ipv4_table(self):
"""Returns global IPv4 table.
Creates the table if it does not exist.
"""
vpn_table = self._global_tables.get(RF_IPv4_UC)
# Lazy initialize the table.
if not vpn_table:
vpn_table = Ipv4Table(self._core_service, self._signal_bus)
self._global_tables[RF_IPv4_UC] = vpn_table
self._tables[(None, RF_IPv4_UC)] = vpn_table
return vpn_table
def get_ipv6_table(self):
table = self._global_tables.get(RF_IPv6_UC)
if not table:
table = Ipv6Table(self._core_service, self._signal_bus)
self._global_tables[RF_IPv6_UC] = table
self._tables[(None, RF_IPv6_UC)] = table
return table
def get_vpn6_table(self):
"""Returns global VPNv6 table.
Creates the table if it does not exist.
"""
vpn_table = self._global_tables.get(RF_IPv6_VPN)
# Lazy initialize the table.
if not vpn_table:
vpn_table = Vpnv6Table(self._core_service, self._signal_bus)
self._global_tables[RF_IPv6_VPN] = vpn_table
self._tables[(None, RF_IPv6_VPN)] = vpn_table
return vpn_table
def get_vpn4_table(self):
"""Returns global VPNv6 table.
Creates the table if it does not exist.
"""
vpn_table = self._global_tables.get(RF_IPv4_VPN)
# Lazy initialize the table.
if not vpn_table:
vpn_table = Vpnv4Table(self._core_service, self._signal_bus)
self._global_tables[RF_IPv4_VPN] = vpn_table
self._tables[(None, RF_IPv4_VPN)] = vpn_table
return vpn_table
def get_rtc_table(self):
"""Returns global RTC table.
Creates the table if it does not exist.
"""
rtc_table = self._global_tables.get(RF_RTC_UC)
# Lazy initialization of the table.
if not rtc_table:
rtc_table = RtcTable(self._core_service, self._signal_bus)
self._global_tables[RF_RTC_UC] = rtc_table
self._tables[(None, RF_RTC_UC)] = rtc_table
return rtc_table
def get_next_vpnv4_label(self):
# Get next available label
lbl = self._next_vpnv4_label
# Check if label is within max. range allowed.
if lbl > int(self._asbr_label_range[1]):
# Currently we log error message if we exceed configured range.
message = 'Have reached max label range'
LOG.error(message)
raise ValueError(message)
# Increment label by 1 as next label.
self._next_vpnv4_label += 1
return lbl
def get_nexthop_label(self, label_key):
return self._next_hop_label.get(label_key, None)
def set_nexthop_label(self, key, value):
self._next_hop_label[key] = value
def update_vrf_table_links(self, vrf_table, new_imp_rts,
removed_imp_rts):
"""Update mapping from RT to VRF table."""
assert vrf_table
if new_imp_rts:
self._link_vrf_table(vrf_table, new_imp_rts)
if removed_imp_rts:
self._remove_links_to_vrf_table_for_rts(vrf_table,
removed_imp_rts)
def re_install_net_ctrl_paths(self, vrf_table):
"""Re-installs paths from NC with current BGP policy.
Iterates over known paths from NC installed in `vrf4_table` and
adds new path with path attributes as per current VRF configuration.
"""
assert vrf_table
for dest in vrf_table.values():
for path in dest.known_path_list:
if path.source is None:
vrf_table.insert_vrf_path(
path.nlri,
path.nexthop,
gen_lbl=True
)
LOG.debug('Re-installed NC paths with current policy for table %s.',
vrf_table)
def _remove_links_to_vrf_table(self, vrf_table):
"""Removes any links to given `vrf_table`."""
assert vrf_table
vrf_conf = vrf_table.vrf_conf
self._remove_links_to_vrf_table_for_rts(vrf_table,
vrf_conf.import_rts)
def _remove_links_to_vrf_table_for_rts(self, vrf_table, rts):
rts_with_no_table = set()
affected_tables = set()
route_family = vrf_table.route_family
for rt in rts:
rt_rf_id = rt + ':' + str(route_family)
rt_specific_tables = self._tables_for_rt.get(rt_rf_id)
affected_tables.update(rt_specific_tables)
if rt_specific_tables:
try:
rt_specific_tables.remove(vrf_table)
except KeyError:
LOG.debug('Did not find table listed as interested '
'for its import RT: %s', rt)
if len(rt_specific_tables) == 0:
rts_with_no_table.add(rt)
# Remove records of RT that have no tables associated with it.
for rt in rts_with_no_table:
rt_rf_id = rt + ':' + str(route_family)
del self._tables_for_rt[rt_rf_id]
def create_and_link_vrf_table(self, vrf_conf):
"""Factory method to create VRF table for given `vrf_conf`.
Adds mapping to this table with appropriate scope. Also, adds mapping
for import RT of this VRF to created table to facilitate
importing/installing of paths from global tables.
Returns created table.
"""
route_family = vrf_conf.route_family
assert route_family in (VRF_RF_IPV4, VRF_RF_IPV6)
vrf_table = None
if route_family == VRF_RF_IPV4:
vrf_table = Vrf4Table(
vrf_conf, self._core_service, self._signal_bus
)
table_id = (vrf_conf.route_dist, route_family)
self._tables[table_id] = vrf_table
elif route_family == VRF_RF_IPV6:
vrf_table = Vrf6Table(
vrf_conf, self._core_service, self._signal_bus
)
table_id = (vrf_conf.route_dist, route_family)
self._tables[table_id] = vrf_table
assert vrf_table is not None
LOG.debug('Added new VrfTable with rd: %s and add_fmly: %s',
vrf_conf.route_dist, route_family)
import_rts = vrf_conf.import_rts
# If VRF is configured with import RT, we put this table
# in a list corresponding to this RT for easy access.
if import_rts:
self._link_vrf_table(vrf_table, import_rts)
return vrf_table
def _link_vrf_table(self, vrf_table, rt_list):
route_family = vrf_table.route_family
for rt in rt_list:
rt_rf_id = rt + ':' + str(route_family)
table_set = self._tables_for_rt.get(rt_rf_id)
if table_set is None:
table_set = set()
self._tables_for_rt[rt_rf_id] = table_set
table_set.add(vrf_table)
LOG.debug('Added VrfTable %s to import RT table list: %s',
vrf_table, rt)
def _clean_global_uninteresting_paths(self):
"""Marks paths that do not have any route targets of interest
for withdrawal.
Since global tables can have paths with route targets that are not
interesting any more, we have to clean these paths so that appropriate
withdraw are sent out to NC and other peers. Interesting route targets
change as VRF are modified or some filter is that specify what route
targets are allowed are updated. This clean up should only be done when
a route target is no longer considered interesting and some paths with
that route target was installing in any of the global table.
"""
uninteresting_dest_count = 0
interested_rts = self._rt_mgr.global_interested_rts
LOG.debug('Cleaning uninteresting paths. Global interested RTs %s',
interested_rts)
for route_family in [RF_IPv4_VPN, RF_IPv6_VPN, RF_RTC_UC]:
# TODO(PH): We currently do not install RT_NLRI paths based on
# extended path attributes (RT)
if route_family == RF_RTC_UC:
continue
table = self.get_global_table_by_route_family(route_family)
uninteresting_dest_count += \
table.clean_uninteresting_paths(interested_rts)
LOG.debug('Found %s number of destinations had uninteresting paths.',
uninteresting_dest_count)
def import_single_vpn_path_to_all_vrfs(self, vpn_path, path_rts=None):
"""Imports *vpnv4_path* to qualifying VRF tables.
Import RTs of VRF table is matched with RTs from *vpn4_path* and if we
have any common RTs we import the path into VRF.
"""
assert (vpn_path.route_family in
(Vpnv4Path.ROUTE_FAMILY, Vpnv6Path.ROUTE_FAMILY))
LOG.debug('Importing path %s to qualifying VRFs', vpn_path)
# If this path has no RTs we are done.
if not path_rts:
LOG.info('Encountered a path with no RTs: %s', vpn_path)
return
# We match path RTs with all VRFs that are interested in them.
interested_tables = set()
# Get route family of VRF to when this VPN Path can be imported to
route_family = RF_IPv4_UC
if vpn_path.route_family != RF_IPv4_VPN:
route_family = RF_IPv6_UC
for rt in path_rts:
rt_rf_id = rt + ':' + str(route_family)
vrf_rt_tables = self._tables_for_rt.get(rt_rf_id)
if vrf_rt_tables:
interested_tables.update(vrf_rt_tables)
if interested_tables:
# We iterate over all VRF tables that are interested in the RT
# of the given path and import this path into them.
route_dist = vpn_path.nlri.route_dist
for vrf_table in interested_tables:
if (vpn_path.source is not None and
route_dist != vrf_table.vrf_conf.route_dist):
update_vrf_dest = vrf_table.import_vpn_path(vpn_path)
# Queue the destination for further processing.
if update_vrf_dest is not None:
self._signal_bus.\
dest_changed(update_vrf_dest)
else:
# If we do not have any VRF with import RT that match with path RT
LOG.debug('No VRF table found that imports RTs: %s', path_rts)
def add_to_vrf(self, route_dist, prefix, next_hop, route_family):
"""Adds `prefix` to VRF identified by `route_dist` with given
`next_hop`.
Returns assigned VPN label.
"""
from ryu.services.protocols.bgp.core import BgpCoreError
assert route_dist and prefix and next_hop
if route_family not in (VRF_RF_IPV4, VRF_RF_IPV6):
raise ValueError('Given route_family %s is not supported.' %
route_family)
vrf_table = None
table_id = (route_dist, route_family)
if route_family == VRF_RF_IPV4:
vrf_table = self._tables.get(table_id)
if vrf_table is None:
raise BgpCoreError(desc='VRF table for RD: %s does not '
'exist.' % route_dist)
if not is_valid_ipv4_prefix(prefix) or not is_valid_ipv4(next_hop):
raise BgpCoreError(desc='Invalid Ipv4 prefix or nexthop.')
ip, masklen = prefix.split('/')
prefix = IPAddrPrefix(int(masklen), ip)
elif route_family == VRF_RF_IPV6:
vrf_table = self._tables.get(table_id)
if vrf_table is None:
raise BgpCoreError(desc='VRF table for RD: %s does not '
'exist.' % route_dist)
if not is_valid_ipv6_prefix(prefix) or not is_valid_ipv6(next_hop):
raise BgpCoreError(desc='Invalid Ipv6 prefix or nexthop.')
ip6, masklen = prefix.split('/')
prefix = IP6AddrPrefix(int(masklen), ip6)
return vrf_table.insert_vrf_path(
prefix, next_hop=next_hop,
gen_lbl=True
)
def add_to_global_table(self, prefix, nexthop=None,
is_withdraw=False):
src_ver_num = 1
peer = None
# set mandatory path attributes
origin = BGPPathAttributeOrigin(BGP_ATTR_ORIGIN_IGP)
aspath = BGPPathAttributeAsPath([[]])
pathattrs = OrderedDict()
pathattrs[BGP_ATTR_TYPE_ORIGIN] = origin
pathattrs[BGP_ATTR_TYPE_AS_PATH] = aspath
net = netaddr.IPNetwork(prefix)
ip = str(net.ip)
masklen = net.prefixlen
if netaddr.valid_ipv4(ip):
_nlri = IPAddrPrefix(masklen, ip)
if nexthop is None:
nexthop = '0.0.0.0'
p = Ipv4Path
else:
_nlri = IP6AddrPrefix(masklen, ip)
if nexthop is None:
nexthop = '::'
p = Ipv6Path
new_path = p(peer, _nlri, src_ver_num,
pattrs=pathattrs, nexthop=nexthop,
is_withdraw=is_withdraw)
# add to global ipv4 table and propagates to neighbors
self.learn_path(new_path)
def remove_from_vrf(self, route_dist, prefix, route_family):
"""Removes `prefix` from VRF identified by `route_dist`.
Returns assigned VPN label.
"""
from ryu.services.protocols.bgp.core import BgpCoreError
# Validate given
if route_family not in (VRF_RF_IPV4, VRF_RF_IPV6):
raise BgpCoreError(desc='Unsupported route family %s' %
route_family)
val_ipv4 = route_family == VRF_RF_IPV4\
and is_valid_ipv4_prefix(prefix)
val_ipv6 = route_family == VRF_RF_IPV6\
and is_valid_ipv6_prefix(prefix)
if not val_ipv4 and not val_ipv6:
raise BgpCoreError(desc='Invalid prefix or nexthop.')
table_id = (route_dist, route_family)
if route_family == VRF_RF_IPV4:
vrf_table = self._tables.get(table_id)
if not vrf_table:
raise BgpCoreError(desc='Vrf for route distinguisher %s does '
'not exist.' % route_dist)
ip, masklen = prefix.split('/')
prefix = IPAddrPrefix(int(masklen), ip)
else:
vrf_table = self._tables.get(table_id)
if not vrf_table:
raise BgpCoreError(desc='Vrf for route distinguisher %s does '
'not exist.' % route_dist)
ip6, masklen = prefix.split('/')
prefix = IP6AddrPrefix(int(masklen), ip6)
# We do not check if we have a path to given prefix, we issue
# withdrawal. Hence multiple withdrawals have not side effect.
return vrf_table.insert_vrf_path(prefix, is_withdraw=True)
def clean_stale_routes(self, peer, route_family=None):
"""Removes old routes from `peer` from `route_family` table.
Routes/paths version number is compared with `peer`s current version
number.
"""
if route_family is not None:
if route_family not in SUPPORTED_GLOBAL_RF:
raise ValueError('Given route family %s is not supported.' %
route_family)
tables = [self._global_tables.get(route_family)]
else:
tables = self._global_tables.values()
for table in tables:
table.cleanup_paths_for_peer(peer)