1243 lines
41 KiB
Python
1243 lines
41 KiB
Python
# Copyright (C) 2014 Nippon Telegraph and Telephone Corporation.
|
|
#
|
|
# 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.
|
|
"""
|
|
Defines some model classes related BGP.
|
|
|
|
These class include types used in saving information sent/received over BGP
|
|
sessions.
|
|
"""
|
|
import abc
|
|
from abc import ABCMeta
|
|
from abc import abstractmethod
|
|
from copy import copy
|
|
import logging
|
|
import functools
|
|
import netaddr
|
|
import six
|
|
|
|
from ryu.lib.packet.bgp import RF_IPv4_UC
|
|
from ryu.lib.packet.bgp import RouteTargetMembershipNLRI
|
|
from ryu.lib.packet.bgp import BGP_ATTR_TYPE_EXTENDED_COMMUNITIES
|
|
from ryu.lib.packet.bgp import BGPPathAttributeLocalPref
|
|
from ryu.lib.packet.bgp import BGP_ATTR_TYPE_AS_PATH
|
|
|
|
from ryu.services.protocols.bgp.base import OrderedDict
|
|
from ryu.services.protocols.bgp.constants import VPN_TABLE
|
|
from ryu.services.protocols.bgp.constants import VRF_TABLE
|
|
from ryu.services.protocols.bgp.model import OutgoingRoute
|
|
from ryu.services.protocols.bgp.processor import BPR_ONLY_PATH
|
|
from ryu.services.protocols.bgp.processor import BPR_UNKNOWN
|
|
|
|
|
|
LOG = logging.getLogger('bgpspeaker.info_base.base')
|
|
|
|
|
|
@six.add_metaclass(ABCMeta)
|
|
class Table(object):
|
|
"""A container for holding information about destination/prefixes.
|
|
|
|
Routing information base for a particular afi/safi.
|
|
This is a base class which should be sub-classed for different route
|
|
family. A table can be uniquely identified by (Route Family, Scope Id).
|
|
"""
|
|
ROUTE_FAMILY = RF_IPv4_UC
|
|
|
|
def __init__(self, scope_id, core_service, signal_bus):
|
|
self._destinations = dict()
|
|
# Scope in which this table exists.
|
|
# If this table represents the VRF, then this could be a VPN ID.
|
|
# For global/VPN tables this should be None
|
|
self._scope_id = scope_id
|
|
self._signal_bus = signal_bus
|
|
self._core_service = core_service
|
|
|
|
@property
|
|
def route_family(self):
|
|
return self.__class__.ROUTE_FAMILY
|
|
|
|
@property
|
|
def core_service(self):
|
|
return self._core_service
|
|
|
|
@property
|
|
def scope_id(self):
|
|
return self._scope_id
|
|
|
|
@abstractmethod
|
|
def _create_dest(self, nlri):
|
|
"""Creates destination specific for this table.
|
|
Returns destination that stores information of paths to *nlri*.
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
def values(self):
|
|
return iter(self._destinations.values())
|
|
|
|
def insert(self, path):
|
|
self._validate_path(path)
|
|
self._validate_nlri(path.nlri)
|
|
if path.is_withdraw:
|
|
updated_dest = self._insert_withdraw(path)
|
|
else:
|
|
updated_dest = self._insert_path(path)
|
|
return updated_dest
|
|
|
|
def insert_sent_route(self, sent_route):
|
|
self._validate_path(sent_route.path)
|
|
dest = self._get_or_create_dest(sent_route.path.nlri)
|
|
dest.add_sent_route(sent_route)
|
|
|
|
def _insert_path(self, path):
|
|
"""Add new path to destination identified by given prefix.
|
|
"""
|
|
assert path.is_withdraw is False
|
|
dest = self._get_or_create_dest(path.nlri)
|
|
# Add given path to matching Dest.
|
|
dest.add_new_path(path)
|
|
# Return updated destination.
|
|
return dest
|
|
|
|
def _insert_withdraw(self, path):
|
|
"""Appends given path to withdraw list of Destination for given prefix.
|
|
"""
|
|
assert path.is_withdraw is True
|
|
dest = self._get_or_create_dest(path.nlri)
|
|
# Add given path to matching destination.
|
|
dest.add_withdraw(path)
|
|
# Return updated destination.
|
|
return dest
|
|
|
|
def cleanup_paths_for_peer(self, peer):
|
|
"""Remove old paths from whose source is `peer`
|
|
|
|
Old paths have source version number that is less than current peer
|
|
version number. Also removes sent paths to this peer.
|
|
"""
|
|
LOG.debug('Cleaning paths from table %s for peer %s', self, peer)
|
|
for dest in self.values():
|
|
# Remove paths learned from this source
|
|
paths_deleted = dest.remove_old_paths_from_source(peer)
|
|
# Remove sent paths to this peer
|
|
had_sent = dest.remove_sent_route(peer)
|
|
if had_sent:
|
|
LOG.debug('Removed sent route %s for %s', dest.nlri, peer)
|
|
# If any paths are removed we enqueue respective destination for
|
|
# future processing.
|
|
if paths_deleted:
|
|
self._signal_bus.dest_changed(dest)
|
|
|
|
def clean_uninteresting_paths(self, interested_rts):
|
|
"""Cleans table of any path that do not have any RT in common
|
|
with `interested_rts`.
|
|
Parameters:
|
|
- `interested_rts`: (set) of RT that are of interest/that need to
|
|
be preserved
|
|
"""
|
|
LOG.debug('Cleaning table %s for given interested RTs %s',
|
|
self, interested_rts)
|
|
uninteresting_dest_count = 0
|
|
for dest in self.values():
|
|
added_withdraw = \
|
|
dest.withdraw_unintresting_paths(interested_rts)
|
|
if added_withdraw:
|
|
self._signal_bus.dest_changed(dest)
|
|
uninteresting_dest_count += 1
|
|
return uninteresting_dest_count
|
|
|
|
def delete_dest_by_nlri(self, nlri):
|
|
"""Deletes the destination identified by given prefix.
|
|
|
|
Returns the deleted destination if a match is found. If not match is
|
|
found return None.
|
|
"""
|
|
self._validate_nlri(nlri)
|
|
dest = self._get_dest(nlri)
|
|
if dest:
|
|
self._destinations.pop(dest)
|
|
return dest
|
|
|
|
def delete_dest(self, dest):
|
|
del self._destinations[self._table_key(dest.nlri)]
|
|
|
|
def _validate_nlri(self, nlri):
|
|
"""Validated *nlri* is the type that this table stores/supports.
|
|
"""
|
|
if not nlri or not (nlri.ROUTE_FAMILY == self.route_family):
|
|
raise ValueError('Invalid Vpnv4 prefix given.')
|
|
|
|
def _validate_path(self, path):
|
|
"""Check if given path is an instance of *Path*.
|
|
|
|
Raises ValueError if given is not a instance of *Path*.
|
|
"""
|
|
if not path or not (path.route_family == self.route_family):
|
|
raise ValueError('Invalid path. Expected instance of'
|
|
' Vpnv4 route family path, got %s.' % path)
|
|
|
|
def _get_or_create_dest(self, nlri):
|
|
table_key = self._table_key(nlri)
|
|
dest = self._destinations.get(table_key)
|
|
# If destination for given prefix does not exist we create it.
|
|
if dest is None:
|
|
dest = self._create_dest(nlri)
|
|
self._destinations[table_key] = dest
|
|
return dest
|
|
|
|
def _get_dest(self, nlri):
|
|
table_key = self._table_key(nlri)
|
|
dest = self._destinations.get(table_key)
|
|
return dest
|
|
|
|
def is_for_vrf(self):
|
|
"""Returns true if this table instance represents a VRF.
|
|
"""
|
|
return self.scope_id is not None
|
|
|
|
def __str__(self):
|
|
return 'Table(scope_id: %s, rf: %s)' % (self.scope_id,
|
|
self.route_family)
|
|
|
|
@abstractmethod
|
|
def _table_key(self, nlri):
|
|
"""Return a key that will uniquely identify this NLRI inside
|
|
this table.
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
|
|
class NonVrfPathProcessingMixin(object):
|
|
"""Mixin reacting to best-path selection algorithm on main table
|
|
level. Intended to use with "Destination" subclasses.
|
|
Applies to most of Destinations except for VrfDest
|
|
because they are processed at VRF level, so different logic applies.
|
|
"""
|
|
|
|
def __init__(self):
|
|
self._core_service = None # not assigned yet
|
|
self._known_path_list = []
|
|
|
|
def _best_path_lost(self):
|
|
self._best_path = None
|
|
|
|
if self._sent_routes:
|
|
# We have to send update-withdraw to all peers to whom old best
|
|
# path was sent.
|
|
for sent_route in self._sent_routes.values():
|
|
sent_path = sent_route.path
|
|
withdraw_clone = sent_path.clone(for_withdrawal=True)
|
|
outgoing_route = OutgoingRoute(withdraw_clone)
|
|
sent_route.sent_peer.enque_outgoing_msg(outgoing_route)
|
|
LOG.debug('Sending withdrawal to %s for %s',
|
|
sent_route.sent_peer, outgoing_route)
|
|
|
|
# Have to clear sent_route list for this destination as
|
|
# best path is removed.
|
|
self._sent_routes = {}
|
|
|
|
def _new_best_path(self, new_best_path):
|
|
old_best_path = self._best_path
|
|
self._best_path = new_best_path
|
|
LOG.debug('New best path selected for destination %s', self)
|
|
|
|
# If old best path was withdrawn
|
|
if (old_best_path and
|
|
old_best_path not in self._known_path_list and
|
|
self._sent_routes):
|
|
# Have to clear sent_route list for this destination as
|
|
# best path is removed.
|
|
self._sent_routes = {}
|
|
|
|
# Communicate that we have new best path to all qualifying
|
|
# bgp-peers.
|
|
pm = self._core_service.peer_manager
|
|
pm.comm_new_best_to_bgp_peers(new_best_path)
|
|
|
|
# withdraw old best path
|
|
if old_best_path and self._sent_routes:
|
|
for sent_route in self._sent_routes.values():
|
|
sent_path = sent_route.path
|
|
withdraw_clone = sent_path.clone(for_withdrawal=True)
|
|
outgoing_route = OutgoingRoute(withdraw_clone)
|
|
sent_route.sent_peer.enque_outgoing_msg(outgoing_route)
|
|
LOG.debug('Sending withdrawal to %s for %s',
|
|
sent_route.sent_peer, outgoing_route)
|
|
self._sent_routes = {}
|
|
|
|
|
|
@six.add_metaclass(ABCMeta)
|
|
class Destination(object):
|
|
"""State about a particular destination.
|
|
|
|
For example, an IP prefix. This is the data-structure that is hung of the
|
|
a routing information base table *Table*.
|
|
"""
|
|
|
|
ROUTE_FAMILY = RF_IPv4_UC
|
|
|
|
def __init__(self, table, nlri):
|
|
# Validate arguments.
|
|
if table.route_family != self.__class__.ROUTE_FAMILY:
|
|
raise ValueError('Table and destination route family '
|
|
'do not match.')
|
|
|
|
# Back-pointer to the table that contains this destination.
|
|
self._table = table
|
|
|
|
self._core_service = table.core_service
|
|
|
|
self._nlri = nlri
|
|
|
|
# List of all known processed paths,
|
|
self._known_path_list = []
|
|
|
|
# List of new un-processed paths.
|
|
self._new_path_list = []
|
|
|
|
# Pointer to best-path. One from the the known paths.
|
|
self._best_path = None
|
|
|
|
# Reason current best path was chosen as best path.
|
|
self._best_path_reason = None
|
|
|
|
# List of withdrawn paths.
|
|
self._withdraw_list = []
|
|
|
|
# List of SentRoute objects. This is the Adj-Rib-Out for this
|
|
# destination. (key/value: peer/sent_route)
|
|
self._sent_routes = {}
|
|
|
|
# This is an (optional) list of paths that were created as a
|
|
# result of exporting this route to other tables.
|
|
# self.exported_paths = None
|
|
|
|
# Automatically generated
|
|
#
|
|
# On work queue for BGP processor.
|
|
# self.next_dest_to_process
|
|
# self.prev_dest_to_process
|
|
|
|
@property
|
|
def route_family(self):
|
|
return self.__class__.ROUTE_FAMILY
|
|
|
|
@property
|
|
def nlri(self):
|
|
return self._nlri
|
|
|
|
@property
|
|
def best_path(self):
|
|
return self._best_path
|
|
|
|
@property
|
|
def best_path_reason(self):
|
|
return self._best_path_reason
|
|
|
|
@property
|
|
def known_path_list(self):
|
|
return self._known_path_list[:]
|
|
|
|
@property
|
|
def sent_routes(self):
|
|
return list(self._sent_routes.values())
|
|
|
|
def add_new_path(self, new_path):
|
|
self._validate_path(new_path)
|
|
self._new_path_list.append(new_path)
|
|
|
|
def add_withdraw(self, withdraw):
|
|
self._validate_path(withdraw)
|
|
self._withdraw_list.append(withdraw)
|
|
|
|
def add_sent_route(self, sent_route):
|
|
self._sent_routes[sent_route.sent_peer] = sent_route
|
|
|
|
def remove_sent_route(self, peer):
|
|
if self.was_sent_to(peer):
|
|
del self._sent_routes[peer]
|
|
return True
|
|
return False
|
|
|
|
def was_sent_to(self, peer):
|
|
if peer in self._sent_routes.keys():
|
|
return True
|
|
return False
|
|
|
|
def _process(self):
|
|
"""Calculate best path for this destination.
|
|
|
|
A destination is processed when known paths to this destination has
|
|
changed. We might have new paths or withdrawals of last known paths.
|
|
Removes withdrawals and adds new learned paths from known path list.
|
|
Uses bgp best-path calculation algorithm on new list of known paths to
|
|
choose new best-path. Communicates best-path to core service.
|
|
"""
|
|
LOG.debug('Processing destination: %s', self)
|
|
new_best_path, reason = self._process_paths()
|
|
self._best_path_reason = reason
|
|
|
|
if self._best_path == new_best_path:
|
|
return
|
|
|
|
if new_best_path is None:
|
|
# we lost best path
|
|
assert not self._known_path_list, repr(self._known_path_list)
|
|
return self._best_path_lost()
|
|
else:
|
|
return self._new_best_path(new_best_path)
|
|
|
|
@abstractmethod
|
|
def _best_path_lost(self):
|
|
raise NotImplementedError()
|
|
|
|
@abstractmethod
|
|
def _new_best_path(self, new_best_path):
|
|
raise NotImplementedError()
|
|
|
|
@classmethod
|
|
def _validate_path(cls, path):
|
|
if not path or path.route_family != cls.ROUTE_FAMILY:
|
|
raise ValueError(
|
|
'Invalid path. Expected %s path got %s' %
|
|
(cls.ROUTE_FAMILY, path)
|
|
)
|
|
|
|
def process(self):
|
|
self._process()
|
|
if not self._known_path_list and not self._best_path:
|
|
self._remove_dest_from_table()
|
|
|
|
def _remove_dest_from_table(self):
|
|
self._table.delete_dest(self)
|
|
|
|
def remove_old_paths_from_source(self, source):
|
|
"""Removes known old paths from *source*.
|
|
|
|
Returns *True* if any of the known paths were found to be old and
|
|
removed/deleted.
|
|
"""
|
|
assert(source and hasattr(source, 'version_num'))
|
|
removed_paths = []
|
|
# Iterate over the paths in reverse order as we want to delete paths
|
|
# whose source is this peer.
|
|
source_ver_num = source.version_num
|
|
for path_idx in range(len(self._known_path_list) - 1, -1, -1):
|
|
path = self._known_path_list[path_idx]
|
|
if (path.source == source and
|
|
path.source_version_num < source_ver_num):
|
|
# If this peer is source of any paths, remove those path.
|
|
del(self._known_path_list[path_idx])
|
|
removed_paths.append(path)
|
|
return removed_paths
|
|
|
|
def withdraw_if_sent_to(self, peer):
|
|
"""Sends a withdraw for this destination to given `peer`.
|
|
|
|
Check the records if we indeed advertise this destination to given peer
|
|
and if so, creates a withdraw for advertised route and sends it to the
|
|
peer.
|
|
Parameter:
|
|
- `peer`: (Peer) peer to send withdraw to
|
|
"""
|
|
from ryu.services.protocols.bgp.peer import Peer
|
|
if not isinstance(peer, Peer):
|
|
raise TypeError('Currently we only support sending withdrawal'
|
|
' to instance of peer')
|
|
sent_route = self._sent_routes.pop(peer, None)
|
|
if not sent_route:
|
|
return False
|
|
|
|
sent_path = sent_route.path
|
|
withdraw_clone = sent_path.clone(for_withdrawal=True)
|
|
outgoing_route = OutgoingRoute(withdraw_clone)
|
|
sent_route.sent_peer.enque_outgoing_msg(outgoing_route)
|
|
return True
|
|
|
|
def _process_paths(self):
|
|
"""Calculates best-path among known paths for this destination.
|
|
|
|
Returns:
|
|
- Best path
|
|
|
|
Modifies destination's state related to stored paths. Removes withdrawn
|
|
paths from known paths. Also, adds new paths to known paths.
|
|
"""
|
|
# First remove the withdrawn paths.
|
|
# Note: If we want to support multiple paths per destination we may
|
|
# have to maintain sent-routes per path.
|
|
self._remove_withdrawals()
|
|
|
|
# Have to select best-path from available paths and new paths.
|
|
# If we do not have any paths, then we no longer have best path.
|
|
if not self._known_path_list and len(self._new_path_list) == 1:
|
|
# If we do not have any old but one new path
|
|
# it becomes best path.
|
|
self._known_path_list.append(self._new_path_list[0])
|
|
del(self._new_path_list[0])
|
|
return self._known_path_list[0], BPR_ONLY_PATH
|
|
|
|
# If we have a new version of old/known path we use it and delete old
|
|
# one.
|
|
self._remove_old_paths()
|
|
|
|
# Collect all new paths into known paths.
|
|
self._known_path_list.extend(self._new_path_list)
|
|
|
|
# Clear new paths as we copied them.
|
|
del(self._new_path_list[:])
|
|
|
|
# If we do not have any paths to this destination, then we do not have
|
|
# new best path.
|
|
if not self._known_path_list:
|
|
return None, BPR_UNKNOWN
|
|
|
|
# Compute new best path
|
|
current_best_path, reason = self._compute_best_known_path()
|
|
return current_best_path, reason
|
|
|
|
def _remove_withdrawals(self):
|
|
"""Removes withdrawn paths.
|
|
|
|
Note:
|
|
We may have disproportionate number of withdraws compared to know paths
|
|
since not all paths get installed into the table due to bgp policy and
|
|
we can receive withdraws for such paths and withdrawals may not be
|
|
stopped by the same policies.
|
|
"""
|
|
|
|
LOG.debug('Removing %s withdrawals', len(self._withdraw_list))
|
|
|
|
# If we have no withdrawals, we have nothing to do.
|
|
if not self._withdraw_list:
|
|
return
|
|
|
|
# If we have some withdrawals and no know-paths, it means it is safe to
|
|
# delete these withdraws.
|
|
if not self._known_path_list:
|
|
LOG.debug('Found %s withdrawals for path(s) that did not get'
|
|
' installed.', len(self._withdraw_list))
|
|
del(self._withdraw_list[:])
|
|
return
|
|
|
|
# If we have some known paths and some withdrawals, we find matches and
|
|
# delete them first.
|
|
matches = set()
|
|
w_matches = set()
|
|
# Match all withdrawals from destination paths.
|
|
for withdraw in self._withdraw_list:
|
|
match = None
|
|
for path in self._known_path_list:
|
|
# We have a match if the source are same.
|
|
if path.source == withdraw.source:
|
|
match = path
|
|
matches.add(path)
|
|
w_matches.add(withdraw)
|
|
# One withdraw can remove only one path.
|
|
break
|
|
# We do no have any match for this withdraw.
|
|
if not match:
|
|
LOG.debug('No matching path for withdraw found, may be path '
|
|
'was not installed into table: %s',
|
|
withdraw)
|
|
# If we have partial match.
|
|
if len(matches) != len(self._withdraw_list):
|
|
LOG.debug('Did not find match for some withdrawals. Number of '
|
|
'matches(%s), number of withdrawals (%s)',
|
|
len(matches), len(self._withdraw_list))
|
|
|
|
# Clear matching paths and withdrawals.
|
|
for match in matches:
|
|
self._known_path_list.remove(match)
|
|
for w_match in w_matches:
|
|
self._withdraw_list.remove(w_match)
|
|
|
|
def _remove_old_paths(self):
|
|
"""Identifies which of known paths are old and removes them.
|
|
|
|
Known paths will no longer have paths whose new version is present in
|
|
new paths.
|
|
"""
|
|
new_paths = self._new_path_list
|
|
known_paths = self._known_path_list
|
|
for new_path in new_paths:
|
|
old_paths = []
|
|
for path in known_paths:
|
|
# Here we just check if source is same and not check if path
|
|
# version num. as new_paths are implicit withdrawal of old
|
|
# paths and when doing RouteRefresh (not EnhancedRouteRefresh)
|
|
# we get same paths again.
|
|
if new_path.source == path.source:
|
|
old_paths.append(path)
|
|
break
|
|
|
|
for old_path in old_paths:
|
|
known_paths.remove(old_path)
|
|
LOG.debug('Implicit withdrawal of old path, since we have'
|
|
' learned new path from same source: %s', old_path)
|
|
|
|
def _compute_best_known_path(self):
|
|
"""Computes the best path among known paths.
|
|
|
|
Returns current best path among `known_paths`.
|
|
"""
|
|
if not self._known_path_list:
|
|
from ryu.services.protocols.bgp.processor import BgpProcessorError
|
|
raise BgpProcessorError(desc='Need at-least one known path to'
|
|
' compute best path')
|
|
|
|
# We pick the first path as current best path. This helps in breaking
|
|
# tie between two new paths learned in one cycle for which best-path
|
|
# calculation steps lead to tie.
|
|
current_best_path = self._known_path_list[0]
|
|
best_path_reason = BPR_ONLY_PATH
|
|
for next_path in self._known_path_list[1:]:
|
|
from ryu.services.protocols.bgp.processor import compute_best_path
|
|
# Compare next path with current best path.
|
|
new_best_path, reason = \
|
|
compute_best_path(self._core_service.asn, current_best_path,
|
|
next_path)
|
|
best_path_reason = reason
|
|
if new_best_path is not None:
|
|
current_best_path = new_best_path
|
|
|
|
return current_best_path, best_path_reason
|
|
|
|
def withdraw_unintresting_paths(self, interested_rts):
|
|
"""Withdraws paths that are no longer interesting.
|
|
|
|
For all known paths that do not have any route target in common with
|
|
given `interested_rts` we add a corresponding withdraw.
|
|
|
|
Returns True if we added any withdraws.
|
|
"""
|
|
add_withdraws = False
|
|
for path in self._known_path_list:
|
|
if not path.has_rts_in(interested_rts):
|
|
self.withdraw_path(path)
|
|
add_withdraws = True
|
|
return add_withdraws
|
|
|
|
def withdraw_path(self, path):
|
|
if path not in self.known_path_list:
|
|
raise ValueError("Path not known, no need to withdraw")
|
|
withdraw = path.clone(for_withdrawal=True)
|
|
self._withdraw_list.append(withdraw)
|
|
|
|
def to_dict(self):
|
|
return {'table': str(self._table),
|
|
'nlri': str(self._nlri),
|
|
'paths': self._known_path_list[:],
|
|
'withdraws': self._get_num_withdraws()}
|
|
|
|
def __str__(self):
|
|
return ('Destination(table: %s, nlri: %s, paths: %s, withdraws: %s,'
|
|
' new paths: %s)' % (self._table, str(self._nlri),
|
|
len(self._known_path_list),
|
|
len(self._withdraw_list),
|
|
len(self._new_path_list)))
|
|
|
|
def _get_num_valid_paths(self):
|
|
return len(self._known_path_list)
|
|
|
|
def _get_num_withdraws(self):
|
|
return len(self._withdraw_list)
|
|
|
|
def sent_routes_by_peer(self, peer):
|
|
"""get sent routes corresponding to specified peer.
|
|
|
|
Returns SentRoute list.
|
|
"""
|
|
result = []
|
|
for route in self._sent_routes.values():
|
|
if route.sent_peer == peer:
|
|
result.append(route)
|
|
|
|
return result
|
|
|
|
|
|
@six.add_metaclass(ABCMeta)
|
|
class Path(object):
|
|
"""Represents a way of reaching an IP destination.
|
|
|
|
Also contains other meta-data given to us by a specific source (such as a
|
|
peer).
|
|
"""
|
|
__slots__ = ('_source', '_path_attr_map', '_nlri', '_source_version_num',
|
|
'_exported_from', '_nexthop', 'next_path', 'prev_path',
|
|
'_is_withdraw', 'med_set_by_target_neighbor')
|
|
ROUTE_FAMILY = RF_IPv4_UC
|
|
|
|
def __init__(self, source, nlri, src_ver_num, pattrs=None, nexthop=None,
|
|
is_withdraw=False, med_set_by_target_neighbor=False):
|
|
"""Initializes Ipv4 path.
|
|
|
|
If this path is not a withdraw, then path attribute and nexthop both
|
|
should be provided.
|
|
Parameters:
|
|
- `source`: (Peer/str) source of this path.
|
|
- `nlri`: (Vpnv4) Nlri instance for Vpnv4 route family.
|
|
- `src_ver_num`: (int) version number of *source* when this path
|
|
was learned.
|
|
- `pattrs`: (OrderedDict) various path attributes for this path.
|
|
- `nexthop`: (str) nexthop advertised for this path.
|
|
- `is_withdraw`: (bool) True if this represents a withdrawal.
|
|
"""
|
|
self.med_set_by_target_neighbor = med_set_by_target_neighbor
|
|
if nlri.ROUTE_FAMILY != self.__class__.ROUTE_FAMILY:
|
|
raise ValueError('NLRI and Path route families do not'
|
|
' match (%s, %s).' %
|
|
(nlri.ROUTE_FAMILY, self.__class__.ROUTE_FAMILY))
|
|
|
|
# Currently paths injected directly into VRF has only one source
|
|
# src_peer can be None to denote NC else has to be instance of Peer.
|
|
# Paths can be exported from one VRF and then imported into another
|
|
# VRF, in such cases it source is denoted as string VPN_TABLE.
|
|
if not (source is None or
|
|
hasattr(source, 'version_num') or
|
|
source in (VRF_TABLE, VPN_TABLE)):
|
|
raise ValueError('Invalid or Unsupported source for path: %s' %
|
|
source)
|
|
|
|
# If this path is not a withdraw path, than it should have path-
|
|
# attributes and nexthop.
|
|
if not is_withdraw and not (pattrs and nexthop):
|
|
raise ValueError('Need to provide nexthop and patattrs '
|
|
'for path that is not a withdraw.')
|
|
|
|
# The entity (peer) that gave us this path.
|
|
self._source = source
|
|
|
|
# Path attribute of this path.
|
|
if pattrs:
|
|
self._path_attr_map = copy(pattrs)
|
|
else:
|
|
self._path_attr_map = OrderedDict()
|
|
|
|
# NLRI that this path represents.
|
|
self._nlri = nlri
|
|
|
|
# If given nlri is withdrawn.
|
|
self._is_withdraw = is_withdraw
|
|
|
|
# @see Source.version_num
|
|
self._source_version_num = src_ver_num
|
|
|
|
self._nexthop = nexthop
|
|
|
|
# Automatically generated.
|
|
#
|
|
# self.next_path
|
|
# self.prev_path
|
|
|
|
# The Destination from which this path was exported, if any.
|
|
self._exported_from = None
|
|
|
|
@property
|
|
def source_version_num(self):
|
|
return self._source_version_num
|
|
|
|
@property
|
|
def source(self):
|
|
return self._source
|
|
|
|
@property
|
|
def route_family(self):
|
|
return self.__class__.ROUTE_FAMILY
|
|
|
|
@property
|
|
def nlri(self):
|
|
return self._nlri
|
|
|
|
@property
|
|
def is_withdraw(self):
|
|
return self._is_withdraw
|
|
|
|
@property
|
|
def pathattr_map(self):
|
|
return copy(self._path_attr_map)
|
|
|
|
@property
|
|
def nexthop(self):
|
|
return self._nexthop
|
|
|
|
def get_pattr(self, pattr_type, default=None):
|
|
"""Returns path attribute of given type.
|
|
|
|
Returns None if we do not attribute of type *pattr_type*.
|
|
"""
|
|
return self._path_attr_map.get(pattr_type, default)
|
|
|
|
def clone(self, for_withdrawal=False):
|
|
pathattrs = None
|
|
if not for_withdrawal:
|
|
pathattrs = self.pathattr_map
|
|
clone = self.__class__(
|
|
self.source,
|
|
self.nlri,
|
|
self.source_version_num,
|
|
pattrs=pathattrs,
|
|
nexthop=self.nexthop,
|
|
is_withdraw=for_withdrawal
|
|
)
|
|
return clone
|
|
|
|
def get_rts(self):
|
|
extcomm_attr = self._path_attr_map.get(
|
|
BGP_ATTR_TYPE_EXTENDED_COMMUNITIES)
|
|
if extcomm_attr is None:
|
|
rts = []
|
|
else:
|
|
rts = extcomm_attr.rt_list
|
|
return rts
|
|
|
|
def has_rts_in(self, interested_rts):
|
|
"""Returns True if this `Path` has any `ExtCommunity` attribute
|
|
route target common with `interested_rts`.
|
|
"""
|
|
assert isinstance(interested_rts, set)
|
|
curr_rts = self.get_rts()
|
|
# Add default RT to path RTs so that we match interest for peers who
|
|
# advertised default RT
|
|
curr_rts.append(RouteTargetMembershipNLRI.DEFAULT_RT)
|
|
|
|
return not interested_rts.isdisjoint(curr_rts)
|
|
|
|
def is_local(self):
|
|
return self._source is None
|
|
|
|
def has_nexthop(self):
|
|
return not (not self._nexthop or self._nexthop == '0.0.0.0' or
|
|
self._nexthop == '::')
|
|
|
|
def __str__(self):
|
|
return (
|
|
'Path(source: %s, nlri: %s, source ver#: %s, '
|
|
'path attrs.: %s, nexthop: %s, is_withdraw: %s)' %
|
|
(
|
|
self._source, self._nlri, self._source_version_num,
|
|
self._path_attr_map, self._nexthop, self._is_withdraw
|
|
)
|
|
)
|
|
|
|
def __repr__(self):
|
|
return ('Path(%s, %s, %s, %s, %s, %s)' % (
|
|
self._source, self._nlri, self._source_version_num,
|
|
self._path_attr_map, self._nexthop, self._is_withdraw))
|
|
|
|
|
|
@six.add_metaclass(ABCMeta)
|
|
class Filter(object):
|
|
"""Represents a general filter for in-bound and out-bound filter
|
|
|
|
================ ==================================================
|
|
Attribute Description
|
|
================ ==================================================
|
|
policy Filter.POLICY_PERMIT or Filter.POLICY_DENY
|
|
================ ==================================================
|
|
|
|
"""
|
|
|
|
ROUTE_FAMILY = RF_IPv4_UC
|
|
|
|
POLICY_DENY = 0
|
|
POLICY_PERMIT = 1
|
|
|
|
def __init__(self, policy=POLICY_DENY):
|
|
self._policy = policy
|
|
|
|
@property
|
|
def policy(self):
|
|
return self._policy
|
|
|
|
@abstractmethod
|
|
def evaluate(self, path):
|
|
""" This method evaluates the path.
|
|
|
|
Returns this object's policy and the result of matching.
|
|
If the specified prefix matches this object's prefix and
|
|
ge and le condition,
|
|
this method returns True as the matching result.
|
|
|
|
``path`` specifies the path. prefix must be string.
|
|
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
@abstractmethod
|
|
def clone(self):
|
|
""" This method clones Filter object.
|
|
|
|
Returns Filter object that has the same values with the original one.
|
|
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
|
|
@functools.total_ordering
|
|
class PrefixFilter(Filter):
|
|
"""
|
|
used to specify a prefix for filter.
|
|
|
|
We can create PrefixFilter object as follows.
|
|
|
|
prefix_filter = PrefixFilter('10.5.111.0/24',
|
|
policy=PrefixFilter.POLICY_PERMIT)
|
|
|
|
================ ==================================================
|
|
Attribute Description
|
|
================ ==================================================
|
|
prefix A prefix used for this filter
|
|
policy PrefixFilter.POLICY.PERMIT or PrefixFilter.POLICY_DENY
|
|
ge Prefix length that will be applied to this filter.
|
|
ge means greater than or equal.
|
|
le Prefix length that will be applied to this filter.
|
|
le means less than or equal.
|
|
================ ==================================================
|
|
|
|
|
|
For example, when PrefixFilter object is created as follows:
|
|
|
|
* p = PrefixFilter('10.5.111.0/24',
|
|
policy=PrefixFilter.POLICY_DENY,
|
|
ge=26, le=28)
|
|
|
|
|
|
prefixes which match 10.5.111.0/24 and its length matches
|
|
from 26 to 28 will be filtered.
|
|
When this filter is used as an out-filter, it will stop sending
|
|
the path to neighbor because of POLICY_DENY.
|
|
When this filter is used as in-filter, it will stop importing the path
|
|
to the global rib because of POLICY_DENY.
|
|
If you specify POLICY_PERMIT, the path is sent to neighbor or imported to
|
|
the global rib.
|
|
|
|
If you don't want to send prefixes 10.5.111.64/26 and 10.5.111.32/27
|
|
and 10.5.111.16/28, and allow to send other 10.5.111.0's prefixes,
|
|
you can do it by specifying as follows;
|
|
|
|
* p = PrefixFilter('10.5.111.0/24',
|
|
policy=PrefixFilter.POLICY_DENY,
|
|
ge=26, le=28).
|
|
|
|
"""
|
|
|
|
def __init__(self, prefix, policy, ge=None, le=None):
|
|
super(PrefixFilter, self).__init__(policy)
|
|
self._prefix = prefix
|
|
self._network = netaddr.IPNetwork(prefix)
|
|
self._ge = ge
|
|
self._le = le
|
|
|
|
def __lt__(self, other):
|
|
return self._network < other._network
|
|
|
|
def __eq__(self, other):
|
|
return self._network == other._network
|
|
|
|
def __repr__(self):
|
|
policy = 'PERMIT' \
|
|
if self._policy == self.POLICY_PERMIT else 'DENY'
|
|
|
|
return 'PrefixFilter(prefix=%s,policy=%s,ge=%s,le=%s)'\
|
|
% (self._prefix, policy, self._ge, self._le)
|
|
|
|
@property
|
|
def prefix(self):
|
|
return self._prefix
|
|
|
|
@property
|
|
def policy(self):
|
|
return self._policy
|
|
|
|
@property
|
|
def ge(self):
|
|
return self._ge
|
|
|
|
@property
|
|
def le(self):
|
|
return self._le
|
|
|
|
def evaluate(self, path):
|
|
""" This method evaluates the prefix.
|
|
|
|
Returns this object's policy and the result of matching.
|
|
If the specified prefix matches this object's prefix and
|
|
ge and le condition,
|
|
this method returns True as the matching result.
|
|
|
|
``path`` specifies the path that has prefix.
|
|
|
|
"""
|
|
nlri = path.nlri
|
|
|
|
result = False
|
|
length = nlri.length
|
|
net = netaddr.IPNetwork(nlri.prefix)
|
|
|
|
if net in self._network:
|
|
if self._ge is None and self._le is None:
|
|
result = True
|
|
|
|
elif self._ge is None and self._le:
|
|
if length <= self._le:
|
|
result = True
|
|
|
|
elif self._ge and self._le is None:
|
|
if self._ge <= length:
|
|
result = True
|
|
|
|
elif self._ge and self._le:
|
|
if self._ge <= length <= self._le:
|
|
result = True
|
|
|
|
return self.policy, result
|
|
|
|
def clone(self):
|
|
""" This method clones PrefixFilter object.
|
|
|
|
Returns PrefixFilter object that has the same values with the
|
|
original one.
|
|
|
|
"""
|
|
|
|
return self.__class__(self.prefix,
|
|
policy=self._policy,
|
|
ge=self._ge,
|
|
le=self._le)
|
|
|
|
|
|
@functools.total_ordering
|
|
class ASPathFilter(Filter):
|
|
"""
|
|
used to specify a prefix for AS_PATH attribute.
|
|
|
|
We can create ASPathFilter object as follows;
|
|
|
|
* as_path_filter = ASPathFilter(65000,policy=ASPathFilter.TOP)
|
|
|
|
================ ==================================================
|
|
Attribute Description
|
|
================ ==================================================
|
|
as_number A AS number used for this filter
|
|
policy ASPathFilter.POLICY_TOP and ASPathFilter.POLICY_END,
|
|
ASPathFilter.POLICY_INCLUDE and
|
|
ASPathFilter.POLICY_NOT_INCLUDE are available.
|
|
================ ==================================================
|
|
|
|
Meaning of each policy is as follows;
|
|
|
|
* POLICY_TOP :
|
|
Filter checks if the specified AS number is at the top of
|
|
AS_PATH attribute.
|
|
|
|
* POLICY_END :
|
|
Filter checks is the specified AS number
|
|
is at the last of AS_PATH attribute.
|
|
|
|
* POLICY_INCLUDE :
|
|
Filter checks if specified AS number
|
|
exists in AS_PATH attribute
|
|
|
|
* POLICY_NOT_INCLUDE :
|
|
opposite to POLICY_INCLUDE
|
|
|
|
|
|
"""
|
|
|
|
POLICY_TOP = 2
|
|
POLICY_END = 3
|
|
POLICY_INCLUDE = 4
|
|
POLICY_NOT_INCLUDE = 5
|
|
|
|
def __init__(self, as_number, policy):
|
|
super(ASPathFilter, self).__init__(policy)
|
|
self._as_number = as_number
|
|
|
|
def __lt__(self, other):
|
|
return self.as_number < other.as_number
|
|
|
|
def __eq__(self, other):
|
|
return self.as_number == other.as_number
|
|
|
|
def __repr__(self):
|
|
policy = 'TOP'
|
|
if self._policy == self.POLICY_INCLUDE:
|
|
policy = 'INCLUDE'
|
|
elif self._policy == self.POLICY_NOT_INCLUDE:
|
|
policy = 'NOT_INCLUDE'
|
|
elif self._policy == self.POLICY_END:
|
|
policy = 'END'
|
|
|
|
return 'ASPathFilter(as_number=%s,policy=%s)'\
|
|
% (self._as_number, policy)
|
|
|
|
@property
|
|
def as_number(self):
|
|
return self._as_number
|
|
|
|
@property
|
|
def policy(self):
|
|
return self._policy
|
|
|
|
def evaluate(self, path):
|
|
""" This method evaluates as_path list.
|
|
|
|
Returns this object's policy and the result of matching.
|
|
If the specified AS number matches this object's AS number
|
|
according to the policy,
|
|
this method returns True as the matching result.
|
|
|
|
``path`` specifies the path.
|
|
|
|
"""
|
|
|
|
path_aspath = path.pathattr_map.get(BGP_ATTR_TYPE_AS_PATH)
|
|
path_seg_list = path_aspath.path_seg_list
|
|
if path_seg_list:
|
|
path_seg = path_seg_list[0]
|
|
else:
|
|
path_seg = []
|
|
result = False
|
|
|
|
LOG.debug("path_seg : %s", path_seg)
|
|
if self.policy == ASPathFilter.POLICY_TOP:
|
|
|
|
if len(path_seg) > 0 and path_seg[0] == self._as_number:
|
|
result = True
|
|
|
|
elif self.policy == ASPathFilter.POLICY_INCLUDE:
|
|
for aspath in path_seg:
|
|
LOG.debug("POLICY_INCLUDE as_number : %s", aspath)
|
|
if aspath == self._as_number:
|
|
result = True
|
|
break
|
|
|
|
elif self.policy == ASPathFilter.POLICY_END:
|
|
|
|
if len(path_seg) > 0 and path_seg[-1] == self._as_number:
|
|
result = True
|
|
|
|
elif self.policy == ASPathFilter.POLICY_NOT_INCLUDE:
|
|
|
|
if self._as_number not in path_seg:
|
|
result = True
|
|
|
|
return self.policy, result
|
|
|
|
def clone(self):
|
|
""" This method clones ASPathFilter object.
|
|
|
|
Returns ASPathFilter object that has the same values with the
|
|
original one.
|
|
|
|
"""
|
|
|
|
return self.__class__(self._as_number,
|
|
policy=self._policy)
|
|
|
|
|
|
class AttributeMap(object):
|
|
"""
|
|
This class is used to specify an attribute to add if the path matches
|
|
filters.
|
|
We can create AttributeMap object as follows;
|
|
|
|
pref_filter = PrefixFilter('192.168.103.0/30',
|
|
PrefixFilter.POLICY_PERMIT)
|
|
|
|
attribute_map = AttributeMap([pref_filter],
|
|
AttributeMap.ATTR_LOCAL_PREF, 250)
|
|
|
|
speaker.attribute_map_set('192.168.50.102', [attribute_map])
|
|
|
|
AttributeMap.ATTR_LOCAL_PREF means that 250 is set as a
|
|
local preference value if nlri in the path matches pref_filter.
|
|
|
|
ASPathFilter is also available as a filter. ASPathFilter checks if AS_PATH
|
|
attribute in the path matches AS number in the filter.
|
|
|
|
=================== ==================================================
|
|
Attribute Description
|
|
=================== ==================================================
|
|
filters A list of filter.
|
|
Each object should be a Filter class or its sub-class
|
|
attr_type A type of attribute to map on filters. Currently
|
|
AttributeMap.ATTR_LOCAL_PREF is available.
|
|
attr_value A attribute value
|
|
=================== ==================================================
|
|
|
|
"""
|
|
|
|
ATTR_LOCAL_PREF = '_local_pref'
|
|
|
|
def __init__(self, filters, attr_type, attr_value):
|
|
|
|
assert all(isinstance(f, Filter) for f in filters),\
|
|
'all the items in filters must be an instance of Filter sub-class'
|
|
self.filters = filters
|
|
self.attr_type = attr_type
|
|
self.attr_value = attr_value
|
|
|
|
def evaluate(self, path):
|
|
""" This method evaluates attributes of the path.
|
|
|
|
Returns the cause and result of matching.
|
|
Both cause and result are returned from filters
|
|
that this object contains.
|
|
|
|
``path`` specifies the path.
|
|
|
|
"""
|
|
result = False
|
|
cause = None
|
|
|
|
for f in self.filters:
|
|
|
|
cause, result = f.evaluate(path)
|
|
if not result:
|
|
break
|
|
|
|
return cause, result
|
|
|
|
def get_attribute(self):
|
|
func = getattr(self, 'get' + self.attr_type)
|
|
return func()
|
|
|
|
def get_local_pref(self):
|
|
local_pref_attr = BGPPathAttributeLocalPref(value=self.attr_value)
|
|
return local_pref_attr
|
|
|
|
def clone(self):
|
|
""" This method clones AttributeMap object.
|
|
|
|
Returns AttributeMap object that has the same values with the
|
|
original one.
|
|
|
|
"""
|
|
|
|
cloned_filters = [f.clone() for f in self.filters]
|
|
return self.__class__(cloned_filters, self.attr_type, self.attr_value)
|
|
|
|
def __repr__(self):
|
|
|
|
attr_type = 'LOCAL_PREF'\
|
|
if self.attr_type == self.ATTR_LOCAL_PREF else None
|
|
|
|
filter_string = ','.join(repr(f) for f in self.filters)
|
|
return ('AttributeMap(filters=[%s],'
|
|
'attribute_type=%s,'
|
|
'attribute_value=%s)' % (filter_string,
|
|
attr_type,
|
|
self.attr_value))
|