deb-ryu/ryu/services/protocols/bgp/info_base/base.py

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))