mirror of
https://github.com/faucetsdn/ryu.git
synced 2026-05-05 04:16:11 +02:00
add BGP daemon feature
Currently, the BGP code can work as an daemon (IOW, not RyuApp). Signed-off-by: FUJITA Tomonori <fujita.tomonori@lab.ntt.co.jp>
This commit is contained in:
parent
d6bbd83497
commit
e1e89ce57f
@ -106,7 +106,7 @@ run_pylint() {
|
||||
run_pep8() {
|
||||
echo "Running pep8 ..."
|
||||
|
||||
PEP8_EXCLUDE="vcsversion.py,*.pyc,contrib"
|
||||
PEP8_EXCLUDE="vcsversion.py,*.pyc,contrib,dictconfig.py"
|
||||
PEP8_OPTIONS="--exclude=$PEP8_EXCLUDE --repeat --show-source"
|
||||
PEP8_INCLUDE="ryu setup*.py"
|
||||
PEP8_LOG=pep8.log
|
||||
|
||||
6
ryu/services/protocols/bgp/api/all.py
Normal file
6
ryu/services/protocols/bgp/api/all.py
Normal file
@ -0,0 +1,6 @@
|
||||
# flake8: noqa
|
||||
import core
|
||||
import operator
|
||||
import prefix
|
||||
import rtconf
|
||||
import import_map
|
||||
226
ryu/services/protocols/bgp/api/base.py
Normal file
226
ryu/services/protocols/bgp/api/base.py
Normal file
@ -0,0 +1,226 @@
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Public API for BGPSpeaker.
|
||||
|
||||
This API can be used by various services like RPC, CLI, IoC, etc.
|
||||
"""
|
||||
import inspect
|
||||
import logging
|
||||
import traceback
|
||||
|
||||
from ryu.services.protocols.bgp.base import add_bgp_error_metadata
|
||||
from ryu.services.protocols.bgp.base import API_ERROR_CODE
|
||||
from ryu.services.protocols.bgp.base import BGPSException
|
||||
from ryu.services.protocols.bgp.core_manager import CORE_MANAGER
|
||||
from ryu.services.protocols.bgp.rtconf.base import get_validator
|
||||
from ryu.services.protocols.bgp.rtconf.base import MissingRequiredConf
|
||||
from ryu.services.protocols.bgp.rtconf.base import RuntimeConfigError
|
||||
|
||||
|
||||
LOG = logging.getLogger('bgpspeaker.api.base')
|
||||
|
||||
# Various constants used in API calls
|
||||
ROUTE_DISTINGUISHER = 'route_dist'
|
||||
PREFIX = 'prefix'
|
||||
NEXT_HOP = 'next_hop'
|
||||
VPN_LABEL = 'label'
|
||||
API_SYM = 'name'
|
||||
ORIGIN_RD = 'origin_rd'
|
||||
|
||||
# API call registry
|
||||
_CALL_REGISTRY = {}
|
||||
|
||||
|
||||
@add_bgp_error_metadata(code=API_ERROR_CODE,
|
||||
sub_code=1,
|
||||
def_desc='Unknown API error.')
|
||||
class ApiException(BGPSException):
|
||||
pass
|
||||
|
||||
|
||||
@add_bgp_error_metadata(code=API_ERROR_CODE,
|
||||
sub_code=2,
|
||||
def_desc='API symbol or method is not known.')
|
||||
class MethodNotFound(ApiException):
|
||||
pass
|
||||
|
||||
|
||||
@add_bgp_error_metadata(code=API_ERROR_CODE,
|
||||
sub_code=3,
|
||||
def_desc='Error related to BGPS core not starting.')
|
||||
class CoreNotStarted(ApiException):
|
||||
pass
|
||||
|
||||
|
||||
def register(**kwargs):
|
||||
"""Decorator for registering API function.
|
||||
|
||||
Does not do any check or validation.
|
||||
"""
|
||||
def decorator(func):
|
||||
_CALL_REGISTRY[kwargs.get(API_SYM, func.func_name)] = func
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def register_method(name):
|
||||
"""Decorator for registering methods that provide BGPS public API.
|
||||
"""
|
||||
def decorator(func):
|
||||
setattr(func, '__api_method_name__', name)
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def register_class(cls):
|
||||
"""Decorator for the registering class whose instance methods provide BGPS
|
||||
public API.
|
||||
"""
|
||||
old_init = cls.__init__
|
||||
|
||||
def new_init(self, *args, **kwargs):
|
||||
old_init(self, *args, **kwargs)
|
||||
api_registered_methods = \
|
||||
[(m_name, m) for m_name, m in
|
||||
inspect.getmembers(cls, predicate=inspect.ismethod)
|
||||
if hasattr(m, '__api_method_name__')]
|
||||
|
||||
for _, method in api_registered_methods:
|
||||
api_name = getattr(method, '__api_method_name__')
|
||||
|
||||
def create_wrapper(method):
|
||||
def api_method_wrapper(*args, **kwargs):
|
||||
return method(self, *args, **kwargs)
|
||||
return api_method_wrapper
|
||||
|
||||
register(name=api_name)(create_wrapper(method))
|
||||
|
||||
cls.__init__ = new_init
|
||||
return cls
|
||||
|
||||
|
||||
class RegisterWithArgChecks(object):
|
||||
"""Decorator for registering API functions.
|
||||
|
||||
Does some argument checking and validation of required arguments.
|
||||
"""
|
||||
def __init__(self, name, req_args=None, opt_args=None):
|
||||
self._name = name
|
||||
if not req_args:
|
||||
req_args = []
|
||||
self._req_args = req_args
|
||||
if not opt_args:
|
||||
opt_args = []
|
||||
self._opt_args = opt_args
|
||||
self._all_args = (set(self._req_args) | set(self._opt_args))
|
||||
|
||||
def __call__(self, func):
|
||||
"""Wraps given function and registers it as API.
|
||||
|
||||
Returns original function.
|
||||
"""
|
||||
def wrapped_fun(**kwargs):
|
||||
"""Wraps a function to do validation before calling actual func.
|
||||
|
||||
Wraps a function to take key-value args. only. Checks if:
|
||||
1) all required argument of wrapped function are provided
|
||||
2) no extra/un-known arguments are passed
|
||||
3) checks if validator for required arguments is available
|
||||
4) validates required arguments
|
||||
Raises exception if no validator can be found for required args.
|
||||
"""
|
||||
# Check if we are missing arguments.
|
||||
if not kwargs and len(self._req_args) > 0:
|
||||
raise MissingRequiredConf(desc='Missing all required '
|
||||
'attributes.')
|
||||
|
||||
# Check if we have unknown arguments.
|
||||
given_args = set(kwargs.keys())
|
||||
unknown_attrs = given_args - set(self._all_args)
|
||||
if unknown_attrs:
|
||||
raise RuntimeConfigError(desc=('Unknown attributes %r' %
|
||||
unknown_attrs))
|
||||
|
||||
# Check if required arguments are missing
|
||||
missing_req_args = set(self._req_args) - given_args
|
||||
if missing_req_args:
|
||||
conf_name = ', '.join(missing_req_args)
|
||||
raise MissingRequiredConf(conf_name=conf_name)
|
||||
|
||||
#
|
||||
# Prepare to call wrapped function.
|
||||
#
|
||||
# Collect required arguments in the order asked and validate it.
|
||||
req_values = []
|
||||
for req_arg in self._req_args:
|
||||
req_value = kwargs.get(req_arg)
|
||||
# Validate required value.
|
||||
validator = get_validator(req_arg)
|
||||
if not validator:
|
||||
raise ValueError('No validator registered for function %s'
|
||||
' and arg. %s' % (func, req_arg))
|
||||
validator(req_value)
|
||||
req_values.append(req_value)
|
||||
|
||||
# Collect optional arguments.
|
||||
opt_items = {}
|
||||
for opt_arg, opt_value in kwargs.iteritems():
|
||||
if opt_arg in self._opt_args:
|
||||
opt_items[opt_arg] = opt_value
|
||||
|
||||
# Call actual function
|
||||
return func(*req_values, **opt_items)
|
||||
|
||||
# Register wrapped function
|
||||
_CALL_REGISTRY[self._name] = wrapped_fun
|
||||
return func
|
||||
|
||||
|
||||
def is_call_registered(call_name):
|
||||
return call_name in _CALL_REGISTRY
|
||||
|
||||
|
||||
def get_call(call_name):
|
||||
return _CALL_REGISTRY.get(call_name)
|
||||
|
||||
|
||||
def call(symbol, **kwargs):
|
||||
"""Calls/executes BGPS public API identified by given symbol and passes
|
||||
given kwargs as param.
|
||||
"""
|
||||
LOG.info("API method %s called with args: %s", symbol, str(kwargs))
|
||||
|
||||
# TODO(PH, JK) improve the way api function modules are loaded
|
||||
import all # noqa
|
||||
if not is_call_registered(symbol):
|
||||
message = 'Did not find any method registered by symbol %s' % symbol
|
||||
raise MethodNotFound(message)
|
||||
|
||||
if not symbol.startswith('core') and not CORE_MANAGER.started:
|
||||
raise CoreNotStarted(desc='CoreManager is not active.')
|
||||
|
||||
call = get_call(symbol)
|
||||
try:
|
||||
return call(**kwargs)
|
||||
except BGPSException as r:
|
||||
LOG.error(traceback.format_exc())
|
||||
raise r
|
||||
except Exception as e:
|
||||
LOG.error(traceback.format_exc())
|
||||
raise ApiException(desc=str(e))
|
||||
88
ryu/services/protocols/bgp/api/core.py
Normal file
88
ryu/services/protocols/bgp/api/core.py
Normal file
@ -0,0 +1,88 @@
|
||||
# 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 APIs related to Core/CoreManager.
|
||||
"""
|
||||
import eventlet
|
||||
|
||||
from ryu.services.protocols.bgp.api.base import register
|
||||
from ryu.services.protocols.bgp.core_manager import CORE_MANAGER
|
||||
from ryu.services.protocols.bgp.rtconf.base import RuntimeConfigError
|
||||
from ryu.services.protocols.bgp.rtconf.common import CommonConf
|
||||
|
||||
|
||||
NEIGHBOR_RESET_WAIT_TIME = 3
|
||||
|
||||
|
||||
@register(name='core.start')
|
||||
def start(**kwargs):
|
||||
"""Starts new context using provided configuration.
|
||||
|
||||
Raises RuntimeConfigError if a context is already active.
|
||||
"""
|
||||
if CORE_MANAGER.started:
|
||||
raise RuntimeConfigError('Current context has to be stopped to start '
|
||||
'a new context.')
|
||||
|
||||
common_config = CommonConf(**kwargs)
|
||||
eventlet.spawn(CORE_MANAGER.start, *[], **{'common_conf': common_config})
|
||||
eventlet.sleep(2)
|
||||
return True
|
||||
|
||||
|
||||
@register(name='core.stop')
|
||||
def stop(**kwargs):
|
||||
"""Stops current context is one is active.
|
||||
|
||||
Raises RuntimeConfigError if runtime is not active or initialized yet.
|
||||
"""
|
||||
if not CORE_MANAGER.started:
|
||||
raise RuntimeConfigError('No runtime is active. Call start to create '
|
||||
'a runtime')
|
||||
CORE_MANAGER.stop()
|
||||
return True
|
||||
|
||||
|
||||
@register(name='core.reset_neighbor')
|
||||
def reset_neighor(ip_address):
|
||||
neighs_conf = CORE_MANAGER.neighbors_conf
|
||||
neigh_conf = neighs_conf.get_neighbor_conf(ip_address)
|
||||
# Check if we have neighbor with given IP.
|
||||
if not neigh_conf:
|
||||
raise RuntimeConfigError('No neighbor configuration found for given'
|
||||
' IP: %s' % ip_address)
|
||||
# If neighbor is enabled, we disable it.
|
||||
if neigh_conf.enabled:
|
||||
# Disable neighbor to close existing session.
|
||||
neigh_conf.enabled = False
|
||||
# Yield here so that we give chance for neighbor to be disabled.
|
||||
eventlet.sleep(NEIGHBOR_RESET_WAIT_TIME)
|
||||
# Enable neighbor, so that we have a new session with it.
|
||||
neigh_conf.enabled = True
|
||||
else:
|
||||
raise RuntimeConfigError('Neighbor %s is not enabled, hence cannot'
|
||||
' reset.' % ip_address)
|
||||
return True
|
||||
|
||||
|
||||
#==============================================================================
|
||||
# Common configuration related APIs
|
||||
#==============================================================================
|
||||
|
||||
@register(name='comm_conf.get')
|
||||
def get_common_conf():
|
||||
comm_conf = CORE_MANAGER.common_conf
|
||||
return comm_conf.settings
|
||||
79
ryu/services/protocols/bgp/api/import_map.py
Normal file
79
ryu/services/protocols/bgp/api/import_map.py
Normal file
@ -0,0 +1,79 @@
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Import-map configuration.
|
||||
"""
|
||||
import logging
|
||||
|
||||
from ryu.services.protocols.bgp.api.base import register
|
||||
from ryu.services.protocols.bgp.core_manager import CORE_MANAGER
|
||||
from ryu.services.protocols.bgp.core_managers.import_map_manager\
|
||||
import ImportMapAlreadyExistsError
|
||||
from ryu.services.protocols.bgp.rtconf.base import RuntimeConfigError
|
||||
|
||||
LOG = logging.getLogger('bgpspeaker.api.import_map')
|
||||
|
||||
|
||||
@register(name='importmap.create')
|
||||
def create_importmap(type, action, name, value, route_family=None):
|
||||
if action != 'drop':
|
||||
raise RuntimeConfigError(
|
||||
'Unknown action. For now we only support "drop" action.'
|
||||
)
|
||||
|
||||
if type not in ('prefix_match', 'rt_match'):
|
||||
raise RuntimeConfigError(
|
||||
'Unknown type. We support only "prefix_match" and "rt_match".'
|
||||
)
|
||||
|
||||
if type == 'prefix_match':
|
||||
return _create_prefix_match_importmap(name, value, route_family)
|
||||
elif type == 'rt_match':
|
||||
return _create_rt_match_importmap(name, value)
|
||||
|
||||
|
||||
def _create_prefix_match_importmap(name, value, route_family):
|
||||
core_service = CORE_MANAGER.get_core_service()
|
||||
importmap_manager = core_service.importmap_manager
|
||||
try:
|
||||
if route_family == 'ipv4':
|
||||
importmap_manager.create_vpnv4_nlri_import_map(name, value)
|
||||
elif route_family == 'ipv6':
|
||||
importmap_manager.create_vpnv6_nlri_import_map(name, value)
|
||||
else:
|
||||
raise RuntimeConfigError(
|
||||
'Unknown address family %s. it should be ipv4 or ipv6'
|
||||
% route_family
|
||||
)
|
||||
except ImportMapAlreadyExistsError:
|
||||
raise RuntimeConfigError(
|
||||
'Map with this name already exists'
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _create_rt_match_importmap(name, value):
|
||||
core_service = CORE_MANAGER.get_core_service()
|
||||
importmap_manager = core_service.importmap_manager
|
||||
try:
|
||||
importmap_manager.create_rt_import_map(name, value)
|
||||
except ImportMapAlreadyExistsError:
|
||||
raise RuntimeConfigError(
|
||||
'Map with this name already exists'
|
||||
)
|
||||
|
||||
return True
|
||||
77
ryu/services/protocols/bgp/api/operator.py
Normal file
77
ryu/services/protocols/bgp/api/operator.py
Normal file
@ -0,0 +1,77 @@
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Api for operator. Mainly commands to build CLI and
|
||||
operator interface around them.
|
||||
"""
|
||||
import logging
|
||||
|
||||
from ryu.services.protocols.bgp.api.base import ApiException
|
||||
from ryu.services.protocols.bgp.api.base import register_class
|
||||
from ryu.services.protocols.bgp.api.base import register_method
|
||||
from ryu.services.protocols.bgp.api.rpc_log_handler import RpcLogHandler
|
||||
from ryu.services.protocols.bgp.operator.command import Command
|
||||
from ryu.services.protocols.bgp.operator.command import STATUS_ERROR
|
||||
from ryu.services.protocols.bgp.operator.commands.clear import ClearCmd
|
||||
from ryu.services.protocols.bgp.operator.commands.set import SetCmd
|
||||
from ryu.services.protocols.bgp.operator.commands.show import ShowCmd
|
||||
from ryu.services.protocols.bgp.operator.internal_api import InternalApi
|
||||
|
||||
LOG = logging.getLogger('bgpspeaker.api.rtconf')
|
||||
|
||||
|
||||
class RootCmd(Command):
|
||||
subcommands = {
|
||||
'show': ShowCmd,
|
||||
'set': SetCmd,
|
||||
'clear': ClearCmd}
|
||||
|
||||
|
||||
@register_class
|
||||
class OperatorApi(object):
|
||||
default_log_format = '%(asctime)s %(levelname)s %(message)s'
|
||||
|
||||
def __init__(self):
|
||||
self._init_log_handler()
|
||||
self.internal_api = InternalApi(self.log_handler)
|
||||
|
||||
def _init_log_handler(self):
|
||||
self.log_handler = RpcLogHandler()
|
||||
self.log_handler.setLevel(logging.ERROR)
|
||||
self.log_handler.formatter = logging.Formatter(self.default_log_format)
|
||||
|
||||
@register_method(name="operator.show")
|
||||
def show(self, **kwargs):
|
||||
return self._run('show', kw=kwargs)
|
||||
|
||||
@register_method(name="operator.set")
|
||||
def set(self, **kwargs):
|
||||
return self._run('set', kw=kwargs)
|
||||
|
||||
@register_method(name="operator.clear")
|
||||
def clear(self, **kwargs):
|
||||
return self._run('clear', kw=kwargs)
|
||||
|
||||
def _run(self, cmd, kw={}):
|
||||
params = kw.get('params', [])
|
||||
fmt = kw.get('format', 'json')
|
||||
root = RootCmd(api=self.internal_api, resp_formatter_name=fmt)
|
||||
ret, _ = root([cmd] + params)
|
||||
if ret.status == STATUS_ERROR:
|
||||
raise ApiException(str(ret.value))
|
||||
return ret.value
|
||||
|
||||
_OPERATOR_API = OperatorApi()
|
||||
95
ryu/services/protocols/bgp/api/prefix.py
Normal file
95
ryu/services/protocols/bgp/api/prefix.py
Normal file
@ -0,0 +1,95 @@
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Prefix related APIs.
|
||||
"""
|
||||
import logging
|
||||
|
||||
from ryu.services.protocols.bgp.api.base import NEXT_HOP
|
||||
from ryu.services.protocols.bgp.api.base import PREFIX
|
||||
from ryu.services.protocols.bgp.api.base import RegisterWithArgChecks
|
||||
from ryu.services.protocols.bgp.api.base import ROUTE_DISTINGUISHER
|
||||
from ryu.services.protocols.bgp.api.base import VPN_LABEL
|
||||
from ryu.services.protocols.bgp.base import add_bgp_error_metadata
|
||||
from ryu.services.protocols.bgp.base import PREFIX_ERROR_CODE
|
||||
from ryu.services.protocols.bgp.base import validate
|
||||
from ryu.services.protocols.bgp.core import BgpCoreError
|
||||
from ryu.services.protocols.bgp.core_manager import CORE_MANAGER
|
||||
from ryu.services.protocols.bgp.rtconf.base import RuntimeConfigError
|
||||
from ryu.services.protocols.bgp.rtconf.vrfs import VRF_RF
|
||||
from ryu.services.protocols.bgp.rtconf.vrfs import VRF_RF_IPV4
|
||||
from ryu.services.protocols.bgp.utils import validation
|
||||
|
||||
|
||||
LOG = logging.getLogger('bgpspeaker.api.prefix')
|
||||
|
||||
|
||||
@add_bgp_error_metadata(code=PREFIX_ERROR_CODE,
|
||||
sub_code=1,
|
||||
def_desc='Unknown error related to operation on '
|
||||
'prefixes')
|
||||
class PrefixError(RuntimeConfigError):
|
||||
pass
|
||||
|
||||
|
||||
@validate(name=PREFIX)
|
||||
def is_valid_prefix(ipv4_prefix):
|
||||
return validation.is_valid_ipv4_prefix(ipv4_prefix)
|
||||
|
||||
|
||||
@validate(name=NEXT_HOP)
|
||||
def is_valid_next_hop(next_hop_addr):
|
||||
return validation.is_valid_ipv4(next_hop_addr)
|
||||
|
||||
|
||||
@RegisterWithArgChecks(name='prefix.add_local',
|
||||
req_args=[ROUTE_DISTINGUISHER, PREFIX, NEXT_HOP],
|
||||
opt_args=[VRF_RF])
|
||||
def add_local(route_dist, prefix, next_hop, route_family=VRF_RF_IPV4):
|
||||
"""Adds *prefix* from VRF identified by *route_dist* and sets the source as
|
||||
network controller.
|
||||
"""
|
||||
try:
|
||||
# Create new path and insert into appropriate VRF table.
|
||||
tm = CORE_MANAGER.get_core_service().table_manager
|
||||
label = tm.add_to_vrf(route_dist, prefix, next_hop, route_family)
|
||||
# Currently we only allocate one label per local_prefix,
|
||||
# so we share first label from the list.
|
||||
if label:
|
||||
label = label[0]
|
||||
|
||||
# Send success response with new label.
|
||||
return [{ROUTE_DISTINGUISHER: route_dist, PREFIX: prefix,
|
||||
VRF_RF: route_family, VPN_LABEL: label}]
|
||||
except BgpCoreError as e:
|
||||
raise PrefixError(desc=e)
|
||||
|
||||
|
||||
@RegisterWithArgChecks(name='prefix.delete_local',
|
||||
req_args=[ROUTE_DISTINGUISHER, PREFIX],
|
||||
opt_args=[VRF_RF])
|
||||
def delete_local(route_dist, prefix, route_family=VRF_RF_IPV4):
|
||||
"""Deletes/withdraws *prefix* from VRF identified by *route_dist* and
|
||||
source as network controller.
|
||||
"""
|
||||
try:
|
||||
tm = CORE_MANAGER.get_core_service().table_manager
|
||||
tm.remove_from_vrf(route_dist, prefix, route_family)
|
||||
# Send success response to ApgwAgent.
|
||||
return [{ROUTE_DISTINGUISHER: route_dist, PREFIX: prefix,
|
||||
VRF_RF: route_family}]
|
||||
except BgpCoreError as e:
|
||||
raise PrefixError(desc=e)
|
||||
36
ryu/services/protocols/bgp/api/rpc_log_handler.py
Normal file
36
ryu/services/protocols/bgp/api/rpc_log_handler.py
Normal file
@ -0,0 +1,36 @@
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Defined log handler to be used to log to RPC connection.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from ryu.services.protocols.bgp.net_ctrl import NET_CONTROLLER
|
||||
from ryu.services.protocols.bgp.net_ctrl import NOTF_LOG
|
||||
|
||||
|
||||
class RpcLogHandler(logging.Handler):
|
||||
"""Outputs log records to `NET_CONTROLLER`."""
|
||||
def emit(self, record):
|
||||
msg = self.format(record)
|
||||
NET_CONTROLLER.send_rpc_notification(
|
||||
NOTF_LOG,
|
||||
{
|
||||
'level': record.levelname,
|
||||
'msg': msg
|
||||
}
|
||||
)
|
||||
169
ryu/services/protocols/bgp/api/rtconf.py
Normal file
169
ryu/services/protocols/bgp/api/rtconf.py
Normal file
@ -0,0 +1,169 @@
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Runtime configuration manager.
|
||||
"""
|
||||
import logging
|
||||
|
||||
from ryu.services.protocols.bgp.api.base import register
|
||||
from ryu.services.protocols.bgp.api.base import RegisterWithArgChecks
|
||||
from ryu.services.protocols.bgp.core_manager import CORE_MANAGER
|
||||
from ryu.services.protocols.bgp.rtconf.base import ConfWithId
|
||||
from ryu.services.protocols.bgp.rtconf.base import RuntimeConfigError
|
||||
from ryu.services.protocols.bgp.rtconf import neighbors
|
||||
from ryu.services.protocols.bgp.rtconf.neighbors import NeighborConf
|
||||
from ryu.services.protocols.bgp.rtconf.vrfs import ROUTE_DISTINGUISHER
|
||||
from ryu.services.protocols.bgp.rtconf.vrfs import VRF_RF
|
||||
from ryu.services.protocols.bgp.rtconf.vrfs import VRF_RF_IPV4
|
||||
from ryu.services.protocols.bgp.rtconf.vrfs import VrfConf
|
||||
|
||||
LOG = logging.getLogger('bgpspeaker.api.rtconf')
|
||||
|
||||
|
||||
#==============================================================================
|
||||
# Neighbor configuration related APIs
|
||||
#==============================================================================
|
||||
|
||||
|
||||
def _get_neighbor_conf(neigh_ip_address):
|
||||
"""Returns neighbor configuration for given neighbor ip address.
|
||||
|
||||
Raises exception if no neighbor with `neigh_ip_address` exists.
|
||||
"""
|
||||
neigh_conf = \
|
||||
CORE_MANAGER.neighbors_conf.get_neighbor_conf(neigh_ip_address)
|
||||
if not neigh_conf:
|
||||
raise RuntimeConfigError(desc='No Neighbor configuration with IP'
|
||||
' address %s' % neigh_ip_address)
|
||||
assert isinstance(neigh_conf, NeighborConf)
|
||||
return neigh_conf
|
||||
|
||||
|
||||
@register(name='neighbor.create')
|
||||
def create_neighbor(**kwargs):
|
||||
neigh_conf = NeighborConf(**kwargs)
|
||||
CORE_MANAGER.neighbors_conf.add_neighbor_conf(neigh_conf)
|
||||
return True
|
||||
|
||||
|
||||
@RegisterWithArgChecks(name='neighbor.update_enabled',
|
||||
req_args=[neighbors.IP_ADDRESS, neighbors.ENABLED])
|
||||
def update_neighbor_enabled(neigh_ip_address, enabled):
|
||||
neigh_conf = _get_neighbor_conf(neigh_ip_address)
|
||||
neigh_conf.enabled = enabled
|
||||
return True
|
||||
|
||||
|
||||
@RegisterWithArgChecks(name='neighbor.update',
|
||||
req_args=[neighbors.IP_ADDRESS, neighbors.CHANGES])
|
||||
def update_neighbor(neigh_ip_address, changes):
|
||||
rets = []
|
||||
for k, v in changes.iteritems():
|
||||
if k == neighbors.MULTI_EXIT_DISC:
|
||||
rets.append(_update_med(neigh_ip_address, v))
|
||||
|
||||
if k == neighbors.ENABLED:
|
||||
rets.append(update_neighbor_enabled(neigh_ip_address, v))
|
||||
|
||||
return all(rets)
|
||||
|
||||
|
||||
def _update_med(neigh_ip_address, value):
|
||||
neigh_conf = _get_neighbor_conf(neigh_ip_address)
|
||||
neigh_conf.multi_exit_disc = value
|
||||
LOG.info('MED value for neigh: %s updated to %s' % (neigh_conf, value))
|
||||
return True
|
||||
|
||||
|
||||
@RegisterWithArgChecks(name='neighbor.delete',
|
||||
req_args=[neighbors.IP_ADDRESS])
|
||||
def delete_neighbor(neigh_ip_address):
|
||||
neigh_conf = _get_neighbor_conf(neigh_ip_address)
|
||||
if neigh_conf:
|
||||
neigh_conf.enabled = False
|
||||
CORE_MANAGER.neighbors_conf.remove_neighbor_conf(neigh_ip_address)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
@RegisterWithArgChecks(name='neighbor.get',
|
||||
req_args=[neighbors.IP_ADDRESS])
|
||||
def get_neighbor_conf(neigh_ip_address):
|
||||
"""Returns a neighbor configuration for given ip address if exists."""
|
||||
neigh_conf = _get_neighbor_conf(neigh_ip_address)
|
||||
return neigh_conf.settings
|
||||
|
||||
|
||||
@register(name='neighbors.get')
|
||||
def get_neighbors_conf():
|
||||
return CORE_MANAGER.neighbors_conf.settings
|
||||
|
||||
|
||||
#==============================================================================
|
||||
# VRF configuration related APIs
|
||||
#==============================================================================
|
||||
|
||||
@register(name='vrf.create')
|
||||
def create_vrf(**kwargs):
|
||||
vrf_conf = VrfConf(**kwargs)
|
||||
CORE_MANAGER.vrfs_conf.add_vrf_conf(vrf_conf)
|
||||
return True
|
||||
|
||||
|
||||
@register(name='vrf.update')
|
||||
def update_vrf(**kwargs):
|
||||
route_dist = kwargs.get(ROUTE_DISTINGUISHER)
|
||||
vrf_id = kwargs.get(ConfWithId.ID)
|
||||
vrf_rf = kwargs.get(VRF_RF)
|
||||
vrf_conf = CORE_MANAGER.vrfs_conf.get_vrf_conf(
|
||||
route_dist, vrf_rf, vrf_id=vrf_id
|
||||
)
|
||||
|
||||
# If we do not have a VrfConf with given id, we create one.
|
||||
if not vrf_conf:
|
||||
create_vrf(**kwargs)
|
||||
else:
|
||||
vrf_conf.update(**kwargs)
|
||||
return True
|
||||
|
||||
|
||||
@RegisterWithArgChecks(name='vrf.delete', req_args=[ROUTE_DISTINGUISHER])
|
||||
def delete_vrf(route_dist):
|
||||
vrf_conf = CORE_MANAGER.vrfs_conf.remove_vrf_conf(route_dist)
|
||||
if vrf_conf:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
@RegisterWithArgChecks(
|
||||
name='vrf.get',
|
||||
req_args=[ROUTE_DISTINGUISHER],
|
||||
opt_args=[VRF_RF])
|
||||
def get_vrf(route_dist, route_family=VRF_RF_IPV4):
|
||||
vrf_conf = CORE_MANAGER.vrfs_conf.get_vrf_conf(
|
||||
route_dist, vrf_rf=route_family
|
||||
)
|
||||
if not vrf_conf:
|
||||
raise RuntimeConfigError(desc='No VrfConf with vpn id %s' %
|
||||
route_dist)
|
||||
return vrf_conf.settings
|
||||
|
||||
|
||||
@register(name='vrfs.get')
|
||||
def get_vrfs_conf():
|
||||
vrfs_conf = CORE_MANAGER.vrfs_conf
|
||||
return vrfs_conf.settings
|
||||
198
ryu/services/protocols/bgp/application.py
Normal file
198
ryu/services/protocols/bgp/application.py
Normal file
@ -0,0 +1,198 @@
|
||||
# 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 bases classes to create a BGP application.
|
||||
"""
|
||||
import eventlet
|
||||
import imp
|
||||
import logging
|
||||
import traceback
|
||||
|
||||
from ryu.services.protocols.bgp.api.base import call
|
||||
from ryu.services.protocols.bgp.base import add_bgp_error_metadata
|
||||
from ryu.services.protocols.bgp.base import BGPSException
|
||||
from ryu.services.protocols.bgp.base import BIN_ERROR
|
||||
from ryu.services.protocols.bgp.core_manager import CORE_MANAGER
|
||||
from ryu.services.protocols.bgp import net_ctrl
|
||||
from ryu.services.protocols.bgp.rtconf.base import RuntimeConfigError
|
||||
from ryu.services.protocols.bgp.rtconf.common import BGP_SERVER_PORT
|
||||
from ryu.services.protocols.bgp.rtconf.common import DEFAULT_BGP_SERVER_PORT
|
||||
from ryu.services.protocols.bgp.rtconf.common import \
|
||||
DEFAULT_REFRESH_MAX_EOR_TIME
|
||||
from ryu.services.protocols.bgp.rtconf.common import \
|
||||
DEFAULT_REFRESH_STALEPATH_TIME
|
||||
from ryu.services.protocols.bgp.rtconf.common import LABEL_RANGE
|
||||
from ryu.services.protocols.bgp.rtconf.common import LOCAL_AS
|
||||
from ryu.services.protocols.bgp.rtconf.common import REFRESH_MAX_EOR_TIME
|
||||
from ryu.services.protocols.bgp.rtconf.common import REFRESH_STALEPATH_TIME
|
||||
from ryu.services.protocols.bgp.rtconf.common import ROUTER_ID
|
||||
from ryu.services.protocols.bgp.rtconf import neighbors
|
||||
from ryu.services.protocols.bgp.utils.dictconfig import dictConfig
|
||||
from ryu.services.protocols.bgp.utils.validation import is_valid_ipv4
|
||||
|
||||
LOG = logging.getLogger('bgpspeaker.application')
|
||||
|
||||
|
||||
@add_bgp_error_metadata(code=BIN_ERROR,
|
||||
sub_code=1,
|
||||
def_desc='Unknown bootstrap exception.')
|
||||
class ApplicationException(BGPSException):
|
||||
"""Specific Base exception related to `BaseApplication`."""
|
||||
pass
|
||||
|
||||
|
||||
class BaseApplication(object):
|
||||
def __init__(self, bind_ip, bind_port, config_file=None):
|
||||
self.bind_ip = BaseApplication.validate_rpc_ip(bind_ip)
|
||||
self.bind_port = BaseApplication.validate_rpc_port(bind_port)
|
||||
self.config_file = config_file
|
||||
|
||||
def start(self):
|
||||
# Only two main green threads are required for APGW bgp-agent.
|
||||
# One for NetworkController, another for BGPS core.
|
||||
pool = eventlet.GreenPool()
|
||||
|
||||
# If configuration file was provided and loaded successfully. We start
|
||||
# BGPS core using these settings. If no configuration file is provided
|
||||
# or if configuration file is missing minimum required settings BGPS
|
||||
# core is not started.
|
||||
if self.config_file:
|
||||
LOG.debug('Loading config. from settings file.')
|
||||
settings = self.load_config(self.config_file)
|
||||
# Configure log settings, if available.
|
||||
if getattr(settings, 'LOGGING', None):
|
||||
dictConfig(settings.LOGGING)
|
||||
|
||||
if getattr(settings, 'BGP', None):
|
||||
self._start_core(settings)
|
||||
|
||||
# Start Network Controller to server RPC peers.
|
||||
pool.spawn(net_ctrl.NET_CONTROLLER.start, *[],
|
||||
**{net_ctrl.NC_RPC_BIND_IP: self.bind_ip,
|
||||
net_ctrl.NC_RPC_BIND_PORT: self.bind_port})
|
||||
LOG.debug('Started Network Controller')
|
||||
|
||||
# Wait for Network Controller and/or BGPS to finish
|
||||
pool.waitall()
|
||||
|
||||
@classmethod
|
||||
def validate_rpc_ip(cls, ip):
|
||||
"""Validates given ip for use as rpc host bind address.
|
||||
"""
|
||||
if not is_valid_ipv4(ip):
|
||||
raise ApplicationException(desc='Invalid rpc ip address.')
|
||||
return ip
|
||||
|
||||
@classmethod
|
||||
def validate_rpc_port(cls, port):
|
||||
"""Validates give port for use as rpc server port.
|
||||
"""
|
||||
if not port:
|
||||
raise ApplicationException(desc='Invalid rpc port number.')
|
||||
if not isinstance(port, (int, long)) and isinstance(port, str):
|
||||
port = int(port)
|
||||
|
||||
return port
|
||||
|
||||
def load_config(self, config_file):
|
||||
"""Validates give file as settings file for BGPSpeaker.
|
||||
|
||||
Load the configuration from file as bgpspeaker.setting module.
|
||||
"""
|
||||
if not config_file or not isinstance(config_file, str):
|
||||
raise ApplicationException('Invalid configuration file.')
|
||||
|
||||
# Check if file can be read
|
||||
try:
|
||||
return imp.load_source('bgpspeaker.settings', config_file)
|
||||
except Exception as e:
|
||||
raise ApplicationException(desc=str(e))
|
||||
|
||||
def _start_core(self, settings):
|
||||
"""Starts BGPS core using setting and given pool.
|
||||
"""
|
||||
# Get common settings
|
||||
routing_settings = settings.BGP.get('routing')
|
||||
common_settings = {}
|
||||
|
||||
# Get required common settings.
|
||||
try:
|
||||
common_settings[LOCAL_AS] = routing_settings.pop(LOCAL_AS)
|
||||
common_settings[ROUTER_ID] = routing_settings.pop(ROUTER_ID)
|
||||
except KeyError as e:
|
||||
raise ApplicationException(
|
||||
desc='Required minimum configuration missing %s' %
|
||||
e)
|
||||
|
||||
# Get optional common settings
|
||||
common_settings[BGP_SERVER_PORT] = \
|
||||
routing_settings.get(BGP_SERVER_PORT, DEFAULT_BGP_SERVER_PORT)
|
||||
common_settings[REFRESH_STALEPATH_TIME] = \
|
||||
routing_settings.get(REFRESH_STALEPATH_TIME,
|
||||
DEFAULT_REFRESH_STALEPATH_TIME)
|
||||
common_settings[REFRESH_MAX_EOR_TIME] = \
|
||||
routing_settings.get(REFRESH_MAX_EOR_TIME,
|
||||
DEFAULT_REFRESH_MAX_EOR_TIME)
|
||||
label_range = routing_settings[LABEL_RANGE]
|
||||
if label_range:
|
||||
common_settings[LABEL_RANGE] = label_range
|
||||
|
||||
# Start BGPS core service
|
||||
call('core.start', **common_settings)
|
||||
# Give chance for core to start running
|
||||
|
||||
# TODO(Team): How to wait for core start to happen?!
|
||||
eventlet.sleep(3)
|
||||
|
||||
LOG.debug('Core started %s' % CORE_MANAGER.started)
|
||||
# Core manager started add configured neighbor and vrfs
|
||||
if CORE_MANAGER.started:
|
||||
# Add neighbors.
|
||||
self._add_neighbors(routing_settings)
|
||||
|
||||
# Add Vrfs.
|
||||
self._add_vrfs(routing_settings)
|
||||
|
||||
def _add_neighbors(self, routing_settings):
|
||||
"""Add bgp peers/neighbors from given settings to BGPS runtime.
|
||||
|
||||
All valid neighbors are loaded. Miss-configured neighbors are ignored
|
||||
and error is logged.
|
||||
"""
|
||||
bgp_neighbors = routing_settings.get('bgp_neighbors')
|
||||
for ip, bgp_neighbor in bgp_neighbors.items():
|
||||
try:
|
||||
bgp_neighbor[neighbors.IP_ADDRESS] = ip
|
||||
call('neighbor.create', **bgp_neighbor)
|
||||
LOG.debug('Added neighbor %s' % neighbors.IP_ADDRESS)
|
||||
except RuntimeConfigError as re:
|
||||
LOG.error(re)
|
||||
LOG.error(traceback.format_exc())
|
||||
continue
|
||||
|
||||
def _add_vrfs(self, routing_settings):
|
||||
"""Add VRFs from given settings to BGPS runtime.
|
||||
|
||||
If any of the VRFs are miss-configured errors are logged.
|
||||
All valid VRFs are loaded.
|
||||
"""
|
||||
vpns_conf = routing_settings.get('vpns')
|
||||
for vrf in vpns_conf:
|
||||
try:
|
||||
call('vrf.create', **vrf)
|
||||
LOG.debug('Added vrf %s' % str(vrf))
|
||||
except RuntimeConfigError as e:
|
||||
LOG.error(e)
|
||||
continue
|
||||
464
ryu/services/protocols/bgp/base.py
Normal file
464
ryu/services/protocols/bgp/base.py
Normal file
@ -0,0 +1,464 @@
|
||||
# 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 base class related to managing green threads.
|
||||
"""
|
||||
import abc
|
||||
import eventlet
|
||||
import logging
|
||||
import time
|
||||
import traceback
|
||||
import weakref
|
||||
|
||||
from eventlet.timeout import Timeout
|
||||
from ryu.services.protocols.bgp.protocols.bgp import nlri
|
||||
from ryu.services.protocols.bgp.utils.circlist import CircularListType
|
||||
from ryu.services.protocols.bgp.utils.evtlet import LoopingCall
|
||||
|
||||
|
||||
# Logger instance for this module.
|
||||
LOG = logging.getLogger('bgpspeaker.base')
|
||||
|
||||
|
||||
try:
|
||||
from collections import OrderedDict
|
||||
except ImportError:
|
||||
from ordereddict import OrderedDict
|
||||
|
||||
# Pointer to active/available OrderedDict.
|
||||
OrderedDict = OrderedDict
|
||||
|
||||
|
||||
# Currently supported address families.
|
||||
SUPPORTED_GLOBAL_RF = set([nlri.RF_IPv4_VPN,
|
||||
nlri.RF_RTC_UC,
|
||||
nlri.RF_IPv6_VPN
|
||||
])
|
||||
|
||||
|
||||
# Various error codes
|
||||
ACTIVITY_ERROR_CODE = 100
|
||||
RUNTIME_CONF_ERROR_CODE = 200
|
||||
BIN_ERROR = 300
|
||||
NET_CTRL_ERROR_CODE = 400
|
||||
API_ERROR_CODE = 500
|
||||
PREFIX_ERROR_CODE = 600
|
||||
BGP_PROCESSOR_ERROR_CODE = 700
|
||||
CORE_ERROR_CODE = 800
|
||||
|
||||
# Registry of custom exceptions
|
||||
# Key: code:sub-code
|
||||
# Value: exception class
|
||||
_EXCEPTION_REGISTRY = {}
|
||||
|
||||
|
||||
class BGPSException(Exception):
|
||||
"""Base exception class for all BGPS related exceptions.
|
||||
"""
|
||||
|
||||
CODE = 1
|
||||
SUB_CODE = 1
|
||||
DEF_DESC = 'Unknown exception.'
|
||||
|
||||
def __init__(self, desc=None):
|
||||
super(BGPSException, self).__init__()
|
||||
if not desc:
|
||||
desc = self.__class__.DEF_DESC
|
||||
kls = self.__class__
|
||||
self.message = '%d.%d - %s' % (kls.CODE, kls.SUB_CODE, desc)
|
||||
|
||||
def __repr__(self):
|
||||
kls = self.__class__
|
||||
return '<%s(desc=%s)>' % (kls, self.message)
|
||||
|
||||
def __str__(self, *args, **kwargs):
|
||||
return self.message
|
||||
|
||||
|
||||
def add_bgp_error_metadata(code, sub_code, def_desc='unknown'):
|
||||
"""Decorator for all exceptions that want to set exception class meta-data.
|
||||
"""
|
||||
# Check registry if we already have an exception with same code/sub-code
|
||||
if _EXCEPTION_REGISTRY.get((code, sub_code)) is not None:
|
||||
raise ValueError('BGPSException with code %d and sub-code %d '
|
||||
'already defined.' % (code, sub_code))
|
||||
|
||||
def decorator(klass):
|
||||
"""Sets class constants for exception code and sub-code.
|
||||
|
||||
If given class is sub-class of BGPSException we sets class constants.
|
||||
"""
|
||||
if issubclass(klass, BGPSException):
|
||||
_EXCEPTION_REGISTRY[(code, sub_code)] = klass
|
||||
klass.CODE = code
|
||||
klass.SUB_CODE = sub_code
|
||||
klass.DEF_DESC = def_desc
|
||||
return klass
|
||||
return decorator
|
||||
|
||||
|
||||
@add_bgp_error_metadata(code=ACTIVITY_ERROR_CODE,
|
||||
sub_code=1,
|
||||
def_desc='Unknown activity exception.')
|
||||
class ActivityException(BGPSException):
|
||||
"""Base class for exceptions related to Activity.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class Activity(object):
|
||||
"""Base class for a thread of execution that provides some custom settings.
|
||||
|
||||
Activity is also a container of other activities or threads that it has
|
||||
started. Inside a Activity you should always use one of the spawn method
|
||||
to start another activity or greenthread. Activity is also holds pointers
|
||||
to sockets that it or its child activities of threads have create.
|
||||
"""
|
||||
__metaclass__ = abc.ABCMeta
|
||||
|
||||
def __init__(self, name=None):
|
||||
self._name = name
|
||||
if self._name is None:
|
||||
self._name = 'UnknownActivity: ' + str(time.time())
|
||||
self._child_thread_map = weakref.WeakValueDictionary()
|
||||
self._child_activity_map = weakref.WeakValueDictionary()
|
||||
self._asso_socket_map = weakref.WeakValueDictionary()
|
||||
self._timers = weakref.WeakValueDictionary()
|
||||
self._started = False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def started(self):
|
||||
return self._started
|
||||
|
||||
def _validate_activity(self, activity):
|
||||
"""Checks the validity of the given activity before it can be started.
|
||||
"""
|
||||
if not self._started:
|
||||
raise ActivityException(desc='Tried to spawn a child activity'
|
||||
' before Activity was started.')
|
||||
|
||||
if activity.started:
|
||||
raise ActivityException(desc='Tried to start an Activity that was '
|
||||
'already started.')
|
||||
|
||||
def _spawn_activity(self, activity, *args, **kwargs):
|
||||
"""Starts *activity* in a new thread and passes *args* and *kwargs*.
|
||||
|
||||
Maintains pointer to this activity and stops *activity* when this
|
||||
activity is stopped.
|
||||
"""
|
||||
self._validate_activity(activity)
|
||||
|
||||
# Spawn a new greenthread for given activity
|
||||
greenthread = eventlet.spawn(activity.start, *args, **kwargs)
|
||||
self._child_thread_map[activity.name] = greenthread
|
||||
self._child_activity_map[activity.name] = activity
|
||||
return greenthread
|
||||
|
||||
def _spawn_activity_after(self, seconds, activity, *args, **kwargs):
|
||||
self._validate_activity(activity)
|
||||
|
||||
# Schedule to spawn a new greenthread after requested delay
|
||||
greenthread = eventlet.spawn_after(seconds, activity.start, *args,
|
||||
**kwargs)
|
||||
self._child_thread_map[activity.name] = greenthread
|
||||
self._child_activity_map[activity.name] = activity
|
||||
return greenthread
|
||||
|
||||
def _validate_callable(self, callable_):
|
||||
if callable_ is None:
|
||||
raise ActivityException(desc='Callable cannot be None')
|
||||
|
||||
if not hasattr(callable_, '__call__'):
|
||||
raise ActivityException(desc='Currently only supports instances'
|
||||
' that have __call__ as callable which'
|
||||
' is missing in given arg.')
|
||||
if not self._started:
|
||||
raise ActivityException(desc='Tried to spawn a child thread '
|
||||
'before this Activity was started.')
|
||||
|
||||
def _spawn(self, name, callable_, *args, **kwargs):
|
||||
self._validate_callable(callable_)
|
||||
greenthread = eventlet.spawn(callable_, *args, **kwargs)
|
||||
self._child_thread_map[name] = greenthread
|
||||
return greenthread
|
||||
|
||||
def _spawn_after(self, name, seconds, callable_, *args, **kwargs):
|
||||
self._validate_callable(callable_)
|
||||
greenthread = eventlet.spawn_after(seconds, callable_, *args, **kwargs)
|
||||
self._child_thread_map[name] = greenthread
|
||||
return greenthread
|
||||
|
||||
def _create_timer(self, name, func, *arg, **kwarg):
|
||||
timer = LoopingCall(func, *arg, **kwarg)
|
||||
self._timers[name] = timer
|
||||
return timer
|
||||
|
||||
@abc.abstractmethod
|
||||
def _run(self, *args, **kwargs):
|
||||
"""Main activity of this class.
|
||||
|
||||
Can launch other activity/callables here.
|
||||
Sub-classes should override this method.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def start(self, *args, **kwargs):
|
||||
"""Starts the main activity of this class.
|
||||
|
||||
Calls *_run* and calls *stop* when *_run* is finished.
|
||||
This method should be run in a new greenthread as it may not return
|
||||
immediately.
|
||||
"""
|
||||
if self.started:
|
||||
raise ActivityException(desc='Activity already started')
|
||||
|
||||
self._started = True
|
||||
try:
|
||||
self._run(*args, **kwargs)
|
||||
except BGPSException:
|
||||
LOG.error(traceback.format_exc())
|
||||
finally:
|
||||
if self.started: # could have been stopped somewhere else
|
||||
self.stop()
|
||||
|
||||
def pause(self, seconds=0):
|
||||
"""Relinquishes eventlet hub for given number of seconds.
|
||||
|
||||
In other words is puts to sleep to give other greeenthread a chance to
|
||||
run.
|
||||
"""
|
||||
eventlet.sleep(seconds)
|
||||
|
||||
def _stop_child_activities(self):
|
||||
"""Stop all child activities spawn by this activity.
|
||||
"""
|
||||
# Iterating over items list instead of iteritems to avoid dictionary
|
||||
# changed size during iteration
|
||||
child_activities = self._child_activity_map.items()
|
||||
for child_name, child_activity in child_activities:
|
||||
LOG.debug('%s: Stopping child activity %s ' %
|
||||
(self.name, child_name))
|
||||
if child_activity.started:
|
||||
child_activity.stop()
|
||||
|
||||
def _stop_child_threads(self):
|
||||
"""Stops all threads spawn by this activity.
|
||||
"""
|
||||
child_threads = self._child_thread_map.items()
|
||||
for thread_name, thread in child_threads:
|
||||
LOG.debug('%s: Stopping child thread %s' %
|
||||
(self.name, thread_name))
|
||||
thread.kill()
|
||||
|
||||
def _close_asso_sockets(self):
|
||||
"""Closes all the sockets linked to this activity.
|
||||
"""
|
||||
asso_sockets = self._asso_socket_map.items()
|
||||
for sock_name, sock in asso_sockets:
|
||||
LOG.debug('%s: Closing socket %s - %s' %
|
||||
(self.name, sock_name, sock))
|
||||
sock.close()
|
||||
|
||||
def _stop_timers(self):
|
||||
timers = self._timers.items()
|
||||
for timer_name, timer in timers:
|
||||
LOG.debug('%s: Stopping timer %s' % (self.name, timer_name))
|
||||
timer.stop()
|
||||
|
||||
def stop(self):
|
||||
"""Stops all child threads and activities and closes associated
|
||||
sockets.
|
||||
|
||||
Re-initializes this activity to be able to start again.
|
||||
Raise `ActivityException` if activity is not currently started.
|
||||
"""
|
||||
if not self.started:
|
||||
raise ActivityException(desc='Cannot call stop when activity is '
|
||||
'not started or has been stopped already.')
|
||||
|
||||
LOG.debug('Stopping activity %s.' % (self.name))
|
||||
self._stop_timers()
|
||||
self._stop_child_activities()
|
||||
self._stop_child_threads()
|
||||
self._close_asso_sockets()
|
||||
|
||||
# Setup activity for start again.
|
||||
self._started = False
|
||||
self._asso_socket_map = weakref.WeakValueDictionary()
|
||||
self._child_activity_map = weakref.WeakValueDictionary()
|
||||
self._child_thread_map = weakref.WeakValueDictionary()
|
||||
self._timers = weakref.WeakValueDictionary()
|
||||
LOG.debug('Stopping activity %s finished.' % self.name)
|
||||
|
||||
def _listen_tcp(self, loc_addr, conn_handle):
|
||||
"""Creates a TCP server socket which listens on `port` number.
|
||||
|
||||
For each connection `server_factory` starts a new protocol.
|
||||
"""
|
||||
server = eventlet.listen(loc_addr)
|
||||
server_name = self.name + '_server@' + str(loc_addr)
|
||||
self._asso_socket_map[server_name] = server
|
||||
|
||||
# We now wait for connection requests from client.
|
||||
while True:
|
||||
sock, client_address = server.accept()
|
||||
LOG.debug('Connect request received from client for port'
|
||||
' %s:%s' % client_address)
|
||||
client_name = self.name + '_client@' + str(client_address)
|
||||
self._asso_socket_map[client_name] = sock
|
||||
self._spawn(client_name, conn_handle, sock)
|
||||
|
||||
def _connect_tcp(self, peer_addr, conn_handler, time_out=None,
|
||||
bind_address=None):
|
||||
"""Creates a TCP connection to given peer address.
|
||||
|
||||
Tries to create a socket for `timeout` number of seconds. If
|
||||
successful, uses the socket instance to start `client_factory`.
|
||||
The socket is bound to `bind_address` if specified.
|
||||
"""
|
||||
LOG.debug('Connect TCP called for %s:%s' % (peer_addr[0],
|
||||
peer_addr[1]))
|
||||
with Timeout(time_out, False):
|
||||
sock = eventlet.connect(peer_addr, bind=bind_address)
|
||||
if sock:
|
||||
# Connection name for pro-active connection is made up
|
||||
# of local end address + remote end address
|
||||
conn_name = ('L: ' + str(sock.getsockname()) + ', R: ' +
|
||||
str(sock.getpeername()))
|
||||
self._asso_socket_map[conn_name] = sock
|
||||
# If connection is established, we call connection handler
|
||||
# in a new thread.
|
||||
self._spawn(conn_name, conn_handler, sock)
|
||||
|
||||
|
||||
#
|
||||
# Sink
|
||||
#
|
||||
class Sink(object):
|
||||
"""An entity to which we send out messages (eg. BGP routes)."""
|
||||
|
||||
#
|
||||
# OutgoingMsgList
|
||||
#
|
||||
# A circular list type in which objects are linked to each
|
||||
# other using the 'next_sink_out_route' and 'prev_sink_out_route'
|
||||
# attributes.
|
||||
#
|
||||
OutgoingMsgList = CircularListType(next_attr_name='next_sink_out_route',
|
||||
prev_attr_name='prev_sink_out_route')
|
||||
|
||||
# Next available index that can identify an instance uniquely.
|
||||
idx = 0
|
||||
|
||||
@staticmethod
|
||||
def next_index():
|
||||
"""Increments the sink index and returns the value."""
|
||||
Sink.idx = Sink.idx + 1
|
||||
return Sink.idx
|
||||
|
||||
def __init__(self):
|
||||
# A small integer that represents this sink.
|
||||
self.index = Sink.next_index()
|
||||
|
||||
# Event used to signal enqueing.
|
||||
from utils.evtlet import EventletIOFactory
|
||||
self.outgoing_msg_event = EventletIOFactory.create_custom_event()
|
||||
|
||||
self.messages_queued = 0
|
||||
# List of msgs. that are to be sent to this peer. Each item
|
||||
# in the list is an instance of OutgoingRoute.
|
||||
self.outgoing_msg_list = Sink.OutgoingMsgList()
|
||||
|
||||
def clear_outgoing_msg_list(self):
|
||||
self.outgoing_msg_list = Sink.OutgoingMsgList()
|
||||
|
||||
def enque_outgoing_msg(self, msg):
|
||||
self.outgoing_msg_list.append(msg)
|
||||
self.outgoing_msg_event.set()
|
||||
|
||||
self.messages_queued += 1
|
||||
|
||||
def enque_first_outgoing_msg(self, msg):
|
||||
self.outgoing_msg_list.prepend(msg)
|
||||
self.outgoing_msg_event.set()
|
||||
|
||||
def __iter__(self):
|
||||
return self
|
||||
|
||||
def next(self):
|
||||
"""Pops and returns the first outgoing message from the list.
|
||||
|
||||
If message list currently has no messages, the calling thread will
|
||||
be put to sleep until we have at-least one message in the list that
|
||||
can be poped and returned.
|
||||
"""
|
||||
# We pick the first outgoing available and send it.
|
||||
outgoing_msg = self.outgoing_msg_list.pop_first()
|
||||
# If we do not have any outgoing msg., we wait.
|
||||
if outgoing_msg is None:
|
||||
self.outgoing_msg_event.clear()
|
||||
self.outgoing_msg_event.wait()
|
||||
outgoing_msg = self.outgoing_msg_list.pop_first()
|
||||
|
||||
return outgoing_msg
|
||||
|
||||
|
||||
#
|
||||
# Source
|
||||
#
|
||||
class Source(object):
|
||||
"""An entity that gives us BGP routes. A BGP peer, for example."""
|
||||
|
||||
def __init__(self, version_num):
|
||||
# Number that is currently being used to stamp information
|
||||
# received from this source. We will bump this number up when
|
||||
# the information that is now expected from the source belongs
|
||||
# to a different logical batch. This mechanism can be used to
|
||||
# identify stale information.
|
||||
self.version_num = version_num
|
||||
|
||||
|
||||
class FlexinetPeer(Source, Sink):
|
||||
def __init__(self):
|
||||
# Initialize source and sink
|
||||
Source.__init__(self, 1)
|
||||
Sink.__init__(self)
|
||||
|
||||
|
||||
# Registry of validators for configuration/settings.
|
||||
_VALIDATORS = {}
|
||||
|
||||
|
||||
def validate(**kwargs):
|
||||
"""Defines a decorator to register a validator with a name for look-up.
|
||||
|
||||
If name is not provided we use function name as name of the validator.
|
||||
"""
|
||||
def decorator(func):
|
||||
_VALIDATORS[kwargs.pop('name', func.func_name)] = func
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def get_validator(name):
|
||||
"""Returns a validator registered for given name.
|
||||
"""
|
||||
return _VALIDATORS.get(name)
|
||||
50
ryu/services/protocols/bgp/constants.py
Normal file
50
ryu/services/protocols/bgp/constants.py
Normal file
@ -0,0 +1,50 @@
|
||||
# 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.
|
||||
"""
|
||||
Module that holds various constants.
|
||||
|
||||
This module helps in breaking circular dependencies too.
|
||||
"""
|
||||
|
||||
# Various states of bgp state machine.
|
||||
BGP_FSM_IDLE = 'Idle'
|
||||
BGP_FSM_CONNECT = 'Connect'
|
||||
BGP_FSM_ACTIVE = 'Active'
|
||||
BGP_FSM_OPEN_SENT = 'OpenSent'
|
||||
BGP_FSM_OPEN_CONFIRM = 'OpenConfirm'
|
||||
BGP_FSM_ESTABLISHED = 'Established'
|
||||
|
||||
# All valid BGP finite state machine states.
|
||||
BGP_FSM_VALID_STATES = (BGP_FSM_IDLE, BGP_FSM_CONNECT, BGP_FSM_ACTIVE,
|
||||
BGP_FSM_OPEN_SENT, BGP_FSM_OPEN_CONFIRM,
|
||||
BGP_FSM_ESTABLISHED)
|
||||
|
||||
# Supported bgp protocol version number.
|
||||
BGP_VERSION_NUM = 4
|
||||
|
||||
# Standard BGP server port number.
|
||||
STD_BGP_SERVER_PORT_NUM = 179
|
||||
|
||||
#
|
||||
# Constants used to indicate VRF prefix source.
|
||||
#
|
||||
# It indicates prefix inside VRF table came from bgp peer to VPN table and then
|
||||
# to VRF table..
|
||||
VPN_TABLE = 'vpn_table'
|
||||
VRF_TABLE = 'vrf_table'
|
||||
|
||||
# RTC EOR timer default value
|
||||
# Time to wait for RTC-EOR, before we can send initial UPDATE as per RFC
|
||||
RTC_EOR_DEFAULT_TIME = 60
|
||||
422
ryu/services/protocols/bgp/core.py
Normal file
422
ryu/services/protocols/bgp/core.py
Normal file
@ -0,0 +1,422 @@
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Core of BGPSpeaker.
|
||||
|
||||
Provides CoreService which is responsible for establishing bgp sessions with
|
||||
peers and maintains VRFs and Global tables.
|
||||
"""
|
||||
import logging
|
||||
|
||||
from ryu.services.protocols.bgp.protocols.bgp import exceptions
|
||||
from ryu.services.protocols.bgp.protocols.bgp import nlri
|
||||
from ryu.services.protocols.bgp.protocols.bgp import pathattr
|
||||
from ryu.services.protocols.bgp.base import Activity
|
||||
from ryu.services.protocols.bgp.base import add_bgp_error_metadata
|
||||
from ryu.services.protocols.bgp.base import BGPSException
|
||||
from ryu.services.protocols.bgp.base import CORE_ERROR_CODE
|
||||
from ryu.services.protocols.bgp.constants import STD_BGP_SERVER_PORT_NUM
|
||||
from ryu.services.protocols.bgp import core_managers
|
||||
from ryu.services.protocols.bgp.model import FlexinetOutgoingRoute
|
||||
from ryu.services.protocols.bgp.protocol import Factory
|
||||
from ryu.services.protocols.bgp.signals.emit import BgpSignalBus
|
||||
from ryu.services.protocols.bgp.speaker import BgpProtocol
|
||||
from ryu.services.protocols.bgp.utils.rtfilter import RouteTargetManager
|
||||
from ryu.services.protocols.bgp.utils import stats
|
||||
|
||||
|
||||
LOG = logging.getLogger('bgpspeaker.core')
|
||||
|
||||
# Interface IP address on which to run bgp server. Core service listens on all
|
||||
# interfaces of the host on port 179 - standard bgp port.
|
||||
CORE_IP = '0.0.0.0'
|
||||
|
||||
# Required dictates that Origin attribute be incomplete
|
||||
EXPECTED_ORIGIN = pathattr.Origin.INCOMPLETE
|
||||
|
||||
|
||||
@add_bgp_error_metadata(code=CORE_ERROR_CODE, sub_code=1,
|
||||
def_desc='Unknown error occurred related to core.')
|
||||
class BgpCoreError(BGPSException):
|
||||
"""Base exception related to all tables and peer management.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class CoreService(Factory, Activity):
|
||||
"""A service that maintains eBGP/iBGP sessions with BGP peers.
|
||||
|
||||
Two instances of this class don't share any BGP state with each
|
||||
other. Manages peers, tables for various address-families, etc.
|
||||
"""
|
||||
|
||||
protocol = BgpProtocol
|
||||
|
||||
def __init__(self, common_conf, neighbors_conf, vrfs_conf):
|
||||
self._common_config = common_conf
|
||||
self._neighbors_conf = neighbors_conf
|
||||
self._vrfs_conf = vrfs_conf
|
||||
|
||||
Activity.__init__(self, name='core_service')
|
||||
|
||||
self._signal_bus = BgpSignalBus()
|
||||
self._init_signal_listeners()
|
||||
|
||||
self._rt_mgr = RouteTargetManager(self, neighbors_conf, vrfs_conf)
|
||||
|
||||
self._table_manager = core_managers.TableCoreManager(
|
||||
self, common_conf
|
||||
)
|
||||
|
||||
self._importmap_manager = core_managers.ImportMapManager()
|
||||
|
||||
# Autonomous system number of this BGP speaker.
|
||||
self._asn = self._common_config.local_as
|
||||
|
||||
self._peer_manager = core_managers.PeerManager(
|
||||
self,
|
||||
self._neighbors_conf,
|
||||
)
|
||||
|
||||
# Initialize sink for flexinet-peers
|
||||
self._sinks = set()
|
||||
|
||||
self._conf_manager = core_managers.ConfigurationManager(
|
||||
self, common_conf, vrfs_conf, neighbors_conf
|
||||
)
|
||||
|
||||
# Register Flexinet peer sink
|
||||
from ryu.services.protocols.bgp.speaker.net_ctrl import NET_CONTROLLER
|
||||
|
||||
self.register_flexinet_sink(NET_CONTROLLER)
|
||||
|
||||
# State per route family
|
||||
# Key: RouteFamily
|
||||
# Value: BgpInstanceRf
|
||||
self.rf_state = {}
|
||||
|
||||
# Protocol factories for pro-active and re-active bgp-sessions.
|
||||
self.client_factory = None
|
||||
self.server_factory = None
|
||||
|
||||
# Key: RD:Next_Hop
|
||||
# Value: label
|
||||
self._next_hop_label = {}
|
||||
|
||||
# BgpProcessor instance (initialized during start)
|
||||
self._bgp_processor = None
|
||||
|
||||
def _init_signal_listeners(self):
|
||||
self._signal_bus.register_listener(
|
||||
BgpSignalBus.BGP_DEST_CHANGED,
|
||||
lambda _, dest: self.enqueue_for_bgp_processing(dest)
|
||||
)
|
||||
self._signal_bus.register_listener(
|
||||
BgpSignalBus.BGP_VRF_REMOVED,
|
||||
lambda _, route_dist: self.on_vrf_removed(route_dist)
|
||||
)
|
||||
self._signal_bus.register_listener(
|
||||
BgpSignalBus.BGP_VRF_ADDED,
|
||||
lambda _, vrf_conf: self.on_vrf_added(vrf_conf)
|
||||
)
|
||||
self._signal_bus.register_listener(
|
||||
BgpSignalBus.BGP_VRF_STATS_CONFIG_CHANGED,
|
||||
lambda _, vrf_conf: self.on_stats_config_change(vrf_conf)
|
||||
)
|
||||
|
||||
@property
|
||||
def router_id(self):
|
||||
return self._common_config.router_id
|
||||
|
||||
@property
|
||||
def global_interested_rts(self):
|
||||
return self._rt_mgr.global_interested_rts
|
||||
|
||||
@property
|
||||
def asn(self):
|
||||
return self._asn
|
||||
|
||||
@property
|
||||
def table_manager(self):
|
||||
return self._table_manager
|
||||
|
||||
@property
|
||||
def importmap_manager(self):
|
||||
return self._importmap_manager
|
||||
|
||||
@property
|
||||
def peer_manager(self):
|
||||
return self._peer_manager
|
||||
|
||||
@property
|
||||
def rt_manager(self):
|
||||
return self._rt_mgr
|
||||
|
||||
@property
|
||||
def signal_bus(self):
|
||||
return self._signal_bus
|
||||
|
||||
def enqueue_for_bgp_processing(self, dest):
|
||||
return self._bgp_processor.enqueue(dest)
|
||||
|
||||
def on_vrf_removed(self, route_dist):
|
||||
# Remove stats timer linked with this vrf.
|
||||
vrf_stats_timer = self._timers.get(route_dist)
|
||||
if vrf_stats_timer:
|
||||
vrf_stats_timer.stop()
|
||||
del self._timers[route_dist]
|
||||
|
||||
def on_vrf_added(self, vrf_conf):
|
||||
# Setup statistics timer.
|
||||
rd = vrf_conf.route_dist
|
||||
rf = vrf_conf.route_family
|
||||
vrf_table = self._table_manager.get_vrf_table(rd, rf)
|
||||
vrf_stats_timer = self._create_timer(
|
||||
rd,
|
||||
stats.log,
|
||||
stats_source=vrf_table.get_stats_summary_dict
|
||||
)
|
||||
|
||||
# Start statistics timer if applicable.
|
||||
if vrf_conf.stats_log_enabled:
|
||||
vrf_stats_timer.start(vrf_conf.stats_time)
|
||||
|
||||
def on_stats_config_change(self, vrf_conf):
|
||||
vrf_stats_timer = self._timers.get(
|
||||
vrf_conf.route_dist
|
||||
)
|
||||
vrf_stats_timer.stop()
|
||||
vrf_stats_timer.start(vrf_conf.stats_time)
|
||||
|
||||
def _run(self, *args, **kwargs):
|
||||
from ryu.services.protocols.bgp.speaker.processor import BgpProcessor
|
||||
# Initialize bgp processor.
|
||||
self._bgp_processor = BgpProcessor(self)
|
||||
# Start BgpProcessor in a separate thread.
|
||||
processor_thread = self._spawn_activity(self._bgp_processor)
|
||||
|
||||
# Pro-actively try to establish bgp-session with peers.
|
||||
for peer in self._peer_manager.iterpeers:
|
||||
self._spawn_activity(peer, self)
|
||||
|
||||
# Reactively establish bgp-session with peer by listening on
|
||||
# server port for connection requests.
|
||||
server_addr = (CORE_IP, self._common_config.bgp_server_port)
|
||||
server_thread = self._listen_tcp(server_addr, self.start_protocol)
|
||||
|
||||
server_thread.wait()
|
||||
processor_thread.wait()
|
||||
|
||||
#=========================================================================
|
||||
# RTC address family related utilities
|
||||
#=========================================================================
|
||||
|
||||
def update_rtfilters(self):
|
||||
"""Updates RT filters for each peer.
|
||||
|
||||
Should be called if a new RT Nlri's have changed based on the setting.
|
||||
Currently only used by `Processor` to update the RT filters after it
|
||||
has processed a RT destination. If RT filter has changed for a peer we
|
||||
call RT filter change handler.
|
||||
"""
|
||||
# Update RT filter for all peers
|
||||
# TODO(PH): Check if getting this map can be optimized (if expensive)
|
||||
new_peer_to_rtfilter_map = self._compute_rtfilter_map()
|
||||
|
||||
# If we have new best path for RT NLRI, we have to update peer RT
|
||||
# filters and take appropriate action of sending them NLRIs for other
|
||||
# address-families as per new RT filter if necessary.
|
||||
for peer in self._peer_manager.iterpeers:
|
||||
pre_rt_filter = self._rt_mgr.peer_to_rtfilter_map.get(peer, set())
|
||||
curr_rt_filter = new_peer_to_rtfilter_map.get(peer, set())
|
||||
|
||||
old_rts = pre_rt_filter - curr_rt_filter
|
||||
new_rts = curr_rt_filter - pre_rt_filter
|
||||
# If interested RTs for a peer changes
|
||||
if new_rts or old_rts:
|
||||
LOG.debug('RT Filter for peer %s updated: '
|
||||
'Added RTs %s, Removed Rts %s' %
|
||||
(peer.ip_address, new_rts, old_rts))
|
||||
self._on_update_rt_filter(peer, new_rts, old_rts)
|
||||
# Update to new RT filters
|
||||
self._peer_manager.set_peer_to_rtfilter_map(new_peer_to_rtfilter_map)
|
||||
self._rt_mgr.peer_to_rtfilter_map = new_peer_to_rtfilter_map
|
||||
LOG.debug('Updated RT filters: %s' %
|
||||
(str(self._rt_mgr.peer_to_rtfilter_map)))
|
||||
# Update interested RTs i.e. RTs on the path that will be installed
|
||||
# into global tables
|
||||
self._rt_mgr.update_interested_rts()
|
||||
|
||||
def _on_update_rt_filter(self, peer, new_rts, old_rts):
|
||||
"""Handles update of peer RT filter.
|
||||
|
||||
Parameters:
|
||||
- `peer`: (Peer) whose RT filter has changed.
|
||||
- `new_rts`: (set) of new RTs that peer is interested in.
|
||||
- `old_rts`: (set) of RTs that peers is no longer interested in.
|
||||
"""
|
||||
for table in self._table_manager._global_tables.itervalues():
|
||||
if table.route_family == nlri.RF_RTC_UC:
|
||||
continue
|
||||
self._spawn('rt_filter_chg_%s' % peer,
|
||||
self._rt_mgr.on_rt_filter_chg_sync_peer,
|
||||
peer, new_rts, old_rts, table)
|
||||
LOG.debug('RT Filter change handler launched for route_family %s'
|
||||
% table.route_family)
|
||||
|
||||
def _compute_rtfilter_map(self):
|
||||
"""Returns neighbor's RT filter (permit/allow filter based on RT).
|
||||
|
||||
Walks RT filter tree and computes current RT filters for each peer that
|
||||
have advertised RT NLRIs.
|
||||
Returns:
|
||||
dict of peer, and `set` of rts that a particular neighbor is
|
||||
interested in.
|
||||
"""
|
||||
rtfilter_map = {}
|
||||
|
||||
def get_neigh_filter(neigh):
|
||||
neigh_filter = rtfilter_map.get(neigh)
|
||||
# Lazy creation of neighbor RT filter
|
||||
if neigh_filter is None:
|
||||
neigh_filter = set()
|
||||
rtfilter_map[neigh] = neigh_filter
|
||||
return neigh_filter
|
||||
|
||||
# Check if we have to use all paths or just best path
|
||||
if self._common_config.max_path_ext_rtfilter_all:
|
||||
# We have to look at all paths for a RtDest
|
||||
for rtcdest in self._table_manager.get_rtc_table().itervalues():
|
||||
known_path_list = rtcdest.known_path_list
|
||||
for path in known_path_list:
|
||||
neigh = path.source
|
||||
|
||||
# We ignore NC
|
||||
if neigh is None:
|
||||
continue
|
||||
|
||||
neigh_filter = get_neigh_filter(neigh)
|
||||
neigh_filter.add(path.nlri.route_target)
|
||||
else:
|
||||
# We iterate over all destination of the RTC table and for iBGP
|
||||
# peers we use all known paths' RTs for RT filter and for eBGP
|
||||
# peers we only consider best-paths' RTs for RT filter
|
||||
for rtcdest in self._table_manager.get_rtc_table().itervalues():
|
||||
path = rtcdest.best_path
|
||||
# If this destination does not have any path, we continue
|
||||
if not path:
|
||||
continue
|
||||
|
||||
neigh = path.source
|
||||
# Consider only eBGP peers and ignore NC
|
||||
if neigh and neigh.is_ebgp_peer():
|
||||
# For eBGP peers we use only best-path to learn RT filter
|
||||
neigh_filter = get_neigh_filter(neigh)
|
||||
neigh_filter.add(path.nlri.route_target)
|
||||
else:
|
||||
# For iBGP peers we use all known paths to learn RT filter
|
||||
known_path_list = rtcdest.known_path_list
|
||||
for path in known_path_list:
|
||||
neigh = path.source
|
||||
# We ignore NC, and eBGP peers
|
||||
if neigh and not neigh.is_ebgp_peer():
|
||||
neigh_filter = get_neigh_filter(neigh)
|
||||
neigh_filter.add(path.nlri.route_target)
|
||||
|
||||
return rtfilter_map
|
||||
|
||||
#=========================================================================
|
||||
# Peer or Neighbor related handles/utilities.
|
||||
#=========================================================================
|
||||
def register_flexinet_sink(self, sink):
|
||||
self._sinks.add(sink)
|
||||
|
||||
def unregister_flexinet_sink(self, sink):
|
||||
self._sinks.remove(sink)
|
||||
|
||||
def update_flexinet_peers(self, path, route_disc):
|
||||
for sink in self._sinks:
|
||||
out_route = FlexinetOutgoingRoute(path, route_disc)
|
||||
sink.enque_outgoing_msg(out_route)
|
||||
|
||||
def on_peer_added(self, peer):
|
||||
if self.started:
|
||||
self._spawn_activity(
|
||||
peer, self.start_protocol
|
||||
)
|
||||
|
||||
# We need to handle new RTC_AS
|
||||
if peer.rtc_as != self.asn:
|
||||
self._spawn(
|
||||
'NEW_RTC_AS_HANDLER %s' % peer.rtc_as,
|
||||
self._rt_mgr.update_rtc_as_set
|
||||
)
|
||||
|
||||
def on_peer_removed(self, peer):
|
||||
if peer.rtc_as != self.asn:
|
||||
self._spawn(
|
||||
'OLD_RTC_AS_HANDLER %s' % peer.rtc_as,
|
||||
self._rt_mgr.update_rtc_as_set
|
||||
)
|
||||
|
||||
def build_protocol(self, socket):
|
||||
assert socket
|
||||
# Check if its a reactive connection or pro-active connection
|
||||
_, remote_port = socket.getpeername()
|
||||
is_reactive_conn = True
|
||||
if remote_port == STD_BGP_SERVER_PORT_NUM:
|
||||
is_reactive_conn = False
|
||||
|
||||
bgp_protocol = self.protocol(
|
||||
socket,
|
||||
self._signal_bus,
|
||||
is_reactive_conn=is_reactive_conn
|
||||
)
|
||||
return bgp_protocol
|
||||
|
||||
def start_protocol(self, socket):
|
||||
"""Handler of new connection requests on bgp server port.
|
||||
|
||||
Checks if new connection request is valid and starts new instance of
|
||||
protocol.
|
||||
"""
|
||||
assert socket
|
||||
peer_addr, peer_port = socket.getpeername()
|
||||
peer = self._peer_manager.get_by_addr(peer_addr)
|
||||
bgp_proto = self.build_protocol(socket)
|
||||
|
||||
# We reject this connection request from peer:
|
||||
# 1) If we have connection initiated by a peer that is not in our
|
||||
# configuration.
|
||||
# 2) If this neighbor is not enabled according to configuration.
|
||||
if not peer or not peer.enabled:
|
||||
LOG.debug('Closed connection to %s:%s as it is not a recognized'
|
||||
' peer.' % (peer_addr, peer_port))
|
||||
# Send connection rejected notification as per RFC
|
||||
code = exceptions.ConnRejected.CODE
|
||||
subcode = exceptions.ConnRejected.SUB_CODE
|
||||
bgp_proto.send_notification(code, subcode)
|
||||
elif not (peer.in_idle() or peer.in_active() or peer.in_connect()):
|
||||
LOG.debug('Closing connection to %s:%s as we have connection'
|
||||
' in state other than IDLE or ACTIVE,'
|
||||
' i.e. connection resolution' %
|
||||
(peer_addr, peer_port))
|
||||
# Send Connection Collision Resolution notification as per RFC.
|
||||
code = exceptions.CollisionResolution.CODE
|
||||
subcode = exceptions.CollisionResolution.SUB_CODE
|
||||
bgp_proto.send_notification(code, subcode)
|
||||
else:
|
||||
self._spawn_activity(bgp_proto, peer)
|
||||
72
ryu/services/protocols/bgp/core_manager.py
Normal file
72
ryu/services/protocols/bgp/core_manager.py
Normal file
@ -0,0 +1,72 @@
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Core Manager module dedicated for providing CORE_MANAGER singleton
|
||||
"""
|
||||
from ryu.services.protocols.bgp.base import Activity
|
||||
from ryu.services.protocols.bgp.base import ActivityException
|
||||
from ryu.services.protocols.bgp.rtconf.neighbors import NeighborsConf
|
||||
from ryu.services.protocols.bgp.rtconf.vrfs import VrfsConf
|
||||
|
||||
|
||||
class _CoreManager(Activity):
|
||||
"""Core service manager.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._common_conf = None
|
||||
self._neighbors_conf = None
|
||||
self._vrfs_conf = None
|
||||
self._core_service = None
|
||||
super(_CoreManager, self).__init__()
|
||||
|
||||
def _run(self, *args, **kwargs):
|
||||
self._common_conf = kwargs.pop('common_conf')
|
||||
self._neighbors_conf = NeighborsConf()
|
||||
self._vrfs_conf = VrfsConf()
|
||||
from ryu.services.protocols.bgp.speaker.core import CoreService
|
||||
self._core_service = CoreService(self._common_conf,
|
||||
self._neighbors_conf,
|
||||
self._vrfs_conf)
|
||||
core_activity = self._spawn_activity(self._core_service)
|
||||
core_activity.wait()
|
||||
|
||||
def get_core_service(self):
|
||||
self._check_started()
|
||||
return self._core_service
|
||||
|
||||
def _check_started(self):
|
||||
if not self.started:
|
||||
raise ActivityException('Cannot access any property before '
|
||||
'activity has started')
|
||||
|
||||
@property
|
||||
def common_conf(self):
|
||||
self._check_started()
|
||||
return self._common_conf
|
||||
|
||||
@property
|
||||
def neighbors_conf(self):
|
||||
self._check_started()
|
||||
return self._neighbors_conf
|
||||
|
||||
@property
|
||||
def vrfs_conf(self):
|
||||
self._check_started()
|
||||
return self._vrfs_conf
|
||||
|
||||
# _CoreManager instance that manages core bgp service and configuration data.
|
||||
CORE_MANAGER = _CoreManager()
|
||||
22
ryu/services/protocols/bgp/core_managers/__init__.py
Normal file
22
ryu/services/protocols/bgp/core_managers/__init__.py
Normal file
@ -0,0 +1,22 @@
|
||||
# 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.
|
||||
|
||||
|
||||
from configuration_manager import ConfigurationManager
|
||||
from import_map_manager import ImportMapManager
|
||||
from peer_manager import PeerManager
|
||||
from table_manager import TableCoreManager
|
||||
__all__ = ['ImportMapManager', 'TableCoreManager', 'PeerManager',
|
||||
'ConfigurationManager']
|
||||
@ -0,0 +1,125 @@
|
||||
from ryu.services.protocols.bgp.rtconf.base import ConfWithStats
|
||||
from ryu.services.protocols.bgp.rtconf.common import CommonConfListener
|
||||
from ryu.services.protocols.bgp.rtconf.neighbors import NeighborsConfListener
|
||||
from ryu.services.protocols.bgp.rtconf import vrfs
|
||||
from ryu.services.protocols.bgp.rtconf.vrfs import VrfConf
|
||||
from ryu.services.protocols.bgp.rtconf.vrfs import VrfsConfListener
|
||||
|
||||
import logging
|
||||
|
||||
LOG = logging.getLogger('bgpspeaker.core_managers.table_mixin')
|
||||
|
||||
|
||||
class ConfigurationManager(CommonConfListener, VrfsConfListener,
|
||||
NeighborsConfListener):
|
||||
def __init__(self, core_service, common_conf, vrfs_conf, neighbors_conf):
|
||||
self._signal_bus = core_service.signal_bus
|
||||
self._common_config = common_conf
|
||||
self._peer_manager = core_service.peer_manager
|
||||
self._table_manager = core_service.table_manager
|
||||
self._rt_manager = core_service.rt_manager
|
||||
CommonConfListener.__init__(self, common_conf)
|
||||
VrfsConfListener.__init__(self, vrfs_conf)
|
||||
NeighborsConfListener.__init__(self, neighbors_conf)
|
||||
|
||||
def on_update_common_conf(self, evt):
|
||||
raise NotImplementedError()
|
||||
|
||||
def on_add_neighbor_conf(self, evt):
|
||||
neigh_conf = evt.value
|
||||
self._peer_manager.add_peer(neigh_conf, self._common_config)
|
||||
|
||||
def on_remove_neighbor_conf(self, evt):
|
||||
neigh_conf = evt.value
|
||||
self._peer_manager.remove_peer(neigh_conf)
|
||||
|
||||
def on_chg_vrf_conf(self, evt):
|
||||
evt_value = evt.value
|
||||
vrf_conf = evt.src
|
||||
new_imp_rts, removed_imp_rts, import_maps, re_export, re_import = \
|
||||
evt_value
|
||||
route_family = vrf_conf.route_family
|
||||
vrf_table = self._table_manager.get_vrf_table(
|
||||
vrf_conf.route_dist, route_family
|
||||
)
|
||||
assert vrf_table
|
||||
|
||||
# If we have new import RTs we have to update RTC table and make route
|
||||
# refresh request to peers not participating in RT address-family
|
||||
self._table_manager.update_vrf_table_links(
|
||||
vrf_table, new_imp_rts, removed_imp_rts
|
||||
)
|
||||
|
||||
# If other properties of VRF changed we re-install local paths.
|
||||
if re_export:
|
||||
self._table_manager.re_install_net_ctrl_paths(vrf_table)
|
||||
|
||||
# We have to withdraw paths that do not have any RT that are or
|
||||
# interest
|
||||
vrf_table.clean_uninteresting_paths()
|
||||
if import_maps is not None:
|
||||
vrf_table.init_import_maps(import_maps)
|
||||
changed_dests = vrf_table.apply_import_maps()
|
||||
for dest in changed_dests:
|
||||
self._signal_bus.dest_changed(dest)
|
||||
|
||||
# import new rts
|
||||
if re_import:
|
||||
LOG.debug(
|
||||
"RE-importing prefixes from VPN table to VRF %s"
|
||||
% repr(vrf_table)
|
||||
)
|
||||
self._table_manager.import_all_vpn_paths_to_vrf(vrf_table)
|
||||
else:
|
||||
self._table_manager.import_all_vpn_paths_to_vrf(
|
||||
vrf_table, new_imp_rts
|
||||
)
|
||||
|
||||
# Update local/global RT NLRIs
|
||||
self._rt_manager.update_local_rt_nlris()
|
||||
|
||||
def on_remove_vrf_conf(self, evt):
|
||||
"""Removes VRF table associated with given `vrf_conf`.
|
||||
|
||||
Cleans up other links to this table as well.
|
||||
"""
|
||||
vrf_conf = evt.value
|
||||
# Detach VrfConf change listener.
|
||||
vrf_conf.remove_listener(VrfConf.VRF_CHG_EVT, self.on_chg_vrf_conf)
|
||||
|
||||
self._table_manager.remove_vrf_by_vrf_conf(vrf_conf)
|
||||
|
||||
# Update local RT NLRIs
|
||||
self._rt_manager.update_local_rt_nlris()
|
||||
|
||||
self._signal_bus.vrf_removed(vrf_conf.route_dist)
|
||||
|
||||
def on_add_vrf_conf(self, evt):
|
||||
"""Event handler for new VrfConf.
|
||||
|
||||
Creates a VrfTable to store routing information related to new Vrf.
|
||||
Also arranges for related paths to be imported to this VrfTable.
|
||||
"""
|
||||
vrf_conf = evt.value
|
||||
route_family = vrf_conf.route_family
|
||||
assert route_family in vrfs.SUPPORTED_VRF_RF
|
||||
# Create VRF table with given configuration.
|
||||
vrf_table = self._table_manager.create_and_link_vrf_table(vrf_conf)
|
||||
|
||||
# Attach VrfConf change listeners.
|
||||
vrf_conf.add_listener(ConfWithStats.UPDATE_STATS_LOG_ENABLED_EVT,
|
||||
self.on_stats_config_change)
|
||||
vrf_conf.add_listener(ConfWithStats.UPDATE_STATS_TIME_EVT,
|
||||
self.on_stats_config_change)
|
||||
vrf_conf.add_listener(VrfConf.VRF_CHG_EVT, self.on_chg_vrf_conf)
|
||||
|
||||
# Import paths from VPN table that match this VRF/VPN.
|
||||
self._table_manager.import_all_vpn_paths_to_vrf(vrf_table)
|
||||
|
||||
# Update local RT NLRIs
|
||||
self._rt_manager.update_local_rt_nlris()
|
||||
self._signal_bus.vrf_added(vrf_conf)
|
||||
|
||||
def on_stats_config_change(self, evt):
|
||||
vrf_conf = evt.src
|
||||
self._signal_bus.stats_config_changed(vrf_conf)
|
||||
@ -0,0 +1,45 @@
|
||||
# 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.
|
||||
|
||||
from ryu.services.protocols.bgp.info_base.vrf import VrfRtImportMap
|
||||
from ryu.services.protocols.bgp.info_base.vrf4 import Vrf4NlriImportMap
|
||||
from ryu.services.protocols.bgp.info_base.vrf6 import Vrf6NlriImportMap
|
||||
|
||||
|
||||
class ImportMapManager(object):
|
||||
|
||||
def __init__(self):
|
||||
self._import_maps_by_name = {}
|
||||
|
||||
def create_vpnv4_nlri_import_map(self, name, value):
|
||||
self._create_import_map_factory(name, value, Vrf4NlriImportMap)
|
||||
|
||||
def create_vpnv6_nlri_import_map(self, name, value):
|
||||
self._create_import_map_factory(name, value, Vrf6NlriImportMap)
|
||||
|
||||
def create_rt_import_map(self, name, value):
|
||||
self._create_import_map_factory(name, value, VrfRtImportMap)
|
||||
|
||||
def _create_import_map_factory(self, name, value, cls):
|
||||
if self._import_maps_by_name.get(name) is not None:
|
||||
raise ImportMapAlreadyExistsError()
|
||||
self._import_maps_by_name[name] = cls(value)
|
||||
|
||||
def get_import_map_by_name(self, name):
|
||||
return self._import_maps_by_name.get(name)
|
||||
|
||||
|
||||
class ImportMapAlreadyExistsError(Exception):
|
||||
pass
|
||||
305
ryu/services/protocols/bgp/core_managers/peer_manager.py
Normal file
305
ryu/services/protocols/bgp/core_managers/peer_manager.py
Normal file
@ -0,0 +1,305 @@
|
||||
import logging
|
||||
|
||||
from ryu.services.protocols.bgp.base import SUPPORTED_GLOBAL_RF
|
||||
from ryu.services.protocols.bgp.model import OutgoingRoute
|
||||
from ryu.services.protocols.bgp.peer import Peer
|
||||
from ryu.services.protocols.bgp.protocols.bgp import pathattr
|
||||
from ryu.services.protocols.bgp.protocols.bgp import nlri
|
||||
from ryu.services.protocols.bgp.utils.bgp \
|
||||
import clone_path_and_update_med_for_target_neighbor
|
||||
|
||||
LOG = logging.getLogger('bgpspeaker.core_managers.peer_manager')
|
||||
|
||||
|
||||
class PeerManager(object):
|
||||
def __init__(
|
||||
self, core_service, neighbors_conf,
|
||||
):
|
||||
self._core_service = core_service
|
||||
self._signal_bus = core_service.signal_bus
|
||||
self._table_manager = core_service.table_manager
|
||||
self._rt_manager = core_service.rt_manager
|
||||
self._peers = {}
|
||||
|
||||
# Peer to RTFilter map
|
||||
# Key: Peer instance
|
||||
# Value: set of RTs that constitute RT filter for this peer
|
||||
self._peer_to_rtfilter_map = {}
|
||||
self._neighbors_conf = neighbors_conf
|
||||
|
||||
@property
|
||||
def iterpeers(self):
|
||||
return self._peers.itervalues()
|
||||
|
||||
def set_peer_to_rtfilter_map(self, new_map):
|
||||
self._peer_to_rtfilter_map = new_map
|
||||
|
||||
def add_peer(self, neigh_conf, common_conf):
|
||||
peer = Peer(common_conf, neigh_conf, self._core_service,
|
||||
self._signal_bus, self)
|
||||
self._peers[neigh_conf.ip_address] = peer
|
||||
self._core_service.on_peer_added(peer)
|
||||
|
||||
def remove_peer(self, neigh_conf):
|
||||
neigh_ip_address = neigh_conf.ip_address
|
||||
peer = self._peers.get(neigh_ip_address)
|
||||
peer.stop()
|
||||
del self._peers[neigh_ip_address]
|
||||
self._core_service.on_peer_removed(peer)
|
||||
|
||||
def get_by_addr(self, addr):
|
||||
return self._peers.get(addr)
|
||||
|
||||
def on_peer_down(self, peer):
|
||||
"""Peer down handler.
|
||||
|
||||
Cleans up the paths in global tables that was received from this peer.
|
||||
"""
|
||||
LOG.debug('Cleaning obsolete paths whose source/version: %s/%s' %
|
||||
(peer.ip_address, peer.version_num))
|
||||
# Launch clean-up for each global tables.
|
||||
self._table_manager.clean_stale_routes(peer)
|
||||
|
||||
def _get_non_rtc_peers(self):
|
||||
non_rtc_peer_list = set()
|
||||
for peer in self._peers.itervalues():
|
||||
if (peer.in_established() and
|
||||
not peer.is_mpbgp_cap_valid(nlri.RF_RTC_UC)):
|
||||
non_rtc_peer_list.add(peer)
|
||||
return non_rtc_peer_list
|
||||
|
||||
def curr_peer_rtfilter(self, peer):
|
||||
return self._peer_to_rtfilter_map.get(peer)
|
||||
|
||||
def get_peers_in_established(self):
|
||||
"""Returns list of peers in established state."""
|
||||
est_peers = []
|
||||
for peer in self._peers.itervalues():
|
||||
if peer.in_established:
|
||||
est_peers.append(peer)
|
||||
return est_peers
|
||||
|
||||
def resend_sent(self, route_family, peer):
|
||||
"""For given `peer` re-send sent paths.
|
||||
|
||||
Parameters:
|
||||
- `route-family`: (RouteFamily) of the sent paths to re-send
|
||||
- `peer`: (Peer) peer for which we need to re-send sent paths
|
||||
"""
|
||||
if peer not in self._peers.values():
|
||||
raise ValueError('Could not find given peer (%s)' % peer)
|
||||
|
||||
if route_family not in SUPPORTED_GLOBAL_RF:
|
||||
raise ValueError(
|
||||
'Given route family (%s) is not supported.' % route_family
|
||||
)
|
||||
|
||||
# Iterate over the global table for given afi, safi and enqueue
|
||||
# out-going routes.
|
||||
table = self._table_manager.get_global_table_by_route_family(
|
||||
route_family
|
||||
)
|
||||
|
||||
for destination in table.itervalues():
|
||||
# Check if this destination's sent - routes include this peer.
|
||||
# i.e. check if this destinations was advertised and enqueue
|
||||
# the path only if it was. If the current best-path has not been
|
||||
# advertised before, it might already have a OutgoingRoute queued
|
||||
# to be sent to the peer.
|
||||
sent_routes = destination.sent_routes
|
||||
if sent_routes is None or len(sent_routes) == 0:
|
||||
continue
|
||||
for sent_route in sent_routes:
|
||||
if sent_route.sent_peer == peer:
|
||||
# update med - if previously med was set per neighbor or
|
||||
# wasn't set at all now it could have changed and we may
|
||||
# need to set new value there
|
||||
p = sent_route.path
|
||||
if p.med_set_by_target_neighbor\
|
||||
or p.get_pattr(pathattr.Med.ATTR_NAME) is None:
|
||||
sent_route.path = \
|
||||
clone_path_and_update_med_for_target_neighbor(
|
||||
sent_route.path, peer.med
|
||||
)
|
||||
|
||||
ogr = OutgoingRoute(sent_route.path,
|
||||
for_route_refresh=True)
|
||||
peer.enque_outgoing_msg(ogr)
|
||||
|
||||
def req_rr_to_non_rtc_peers(self, route_family):
|
||||
"""Makes refresh request to all peers for given address family.
|
||||
|
||||
Skips making request to peer that have valid RTC capability.
|
||||
"""
|
||||
assert route_family != nlri.RF_RTC_UC
|
||||
for peer in self._peers.itervalues():
|
||||
# First check if peer is in established state
|
||||
if (peer.in_established and
|
||||
# Check if peer has valid capability for given address
|
||||
# family
|
||||
peer.is_mbgp_cap_valid(route_family) and
|
||||
# Check if peer has valid capability for RTC
|
||||
not peer.is_mbgp_cap_valid(nlri.RF_RTC_UC)):
|
||||
peer.request_route_refresh(route_family)
|
||||
|
||||
def make_route_refresh_request(self, peer_ip, *route_families):
|
||||
"""Request route-refresh for peer with `peer_ip` for given
|
||||
`route_families`.
|
||||
|
||||
Will make route-refresh request for a given `route_family` only if such
|
||||
capability is supported and if peer is in ESTABLISHED state. Else, such
|
||||
requests are ignored. Raises appropriate error in other cases. If
|
||||
`peer_ip` is equal to 'all' makes refresh request to all valid peers.
|
||||
"""
|
||||
LOG.debug('Route refresh requested for peer %s and route families %s'
|
||||
% (peer_ip, route_families))
|
||||
if not SUPPORTED_GLOBAL_RF.intersection(route_families):
|
||||
raise ValueError('Given route family(s) % is not supported.' %
|
||||
route_families)
|
||||
|
||||
peer_list = []
|
||||
# If route-refresh is requested for all peers.
|
||||
if peer_ip == 'all':
|
||||
peer_list.extend(self.get_peers_in_established())
|
||||
else:
|
||||
given_peer = self._peers.get(peer_ip)
|
||||
if not given_peer:
|
||||
raise ValueError('Invalid/unrecognized peer %s' % peer_ip)
|
||||
if not given_peer.in_established:
|
||||
raise ValueError('Peer currently do not have established'
|
||||
' session.')
|
||||
peer_list.append(given_peer)
|
||||
|
||||
# Make route refresh request to valid peers.
|
||||
for peer in peer_list:
|
||||
peer.request_route_refresh(*route_families)
|
||||
|
||||
return True
|
||||
|
||||
def comm_all_rt_nlris(self, peer):
|
||||
"""Shares/communicates current best rt_nlri paths with this peers.
|
||||
|
||||
Can be used to send initial updates after we have established session
|
||||
with `peer` with which RTC capability is valid. Takes into account
|
||||
peers RTC_AS setting and filters all RT NLRIs whose origin AS do not
|
||||
match this setting.
|
||||
"""
|
||||
# First check if for this peer mpbgp-rtc is valid.
|
||||
if not peer.is_mbgp_cap_valid(nlri.RF_RTC_UC):
|
||||
return
|
||||
|
||||
neigh_conf = self._neighbors_conf.get_neighbor_conf(peer.ip_address)
|
||||
peer_rtc_as = neigh_conf.rtc_as
|
||||
# Iterate over all RT_NLRI destination communicate qualifying RT_NLRIs
|
||||
rtc_table = self._table_manager.get_rtc_table()
|
||||
for dest in rtc_table.itervalues():
|
||||
best_path = dest.best_path
|
||||
# Ignore a destination that currently does not have best path
|
||||
if not best_path:
|
||||
continue
|
||||
|
||||
# If this is a local path
|
||||
if best_path.source is None:
|
||||
# Check RT NLRI's origin AS matches peer RTC_AS setting
|
||||
origin_as = best_path.nlri.origin_as
|
||||
if origin_as == peer_rtc_as:
|
||||
peer.communicate_path(best_path)
|
||||
else:
|
||||
# Communicate all remote RT NLRIs
|
||||
peer.communicate_path(best_path)
|
||||
|
||||
# Also communicate EOR as per RFC
|
||||
peer.enque_end_of_rib(nlri.RF_RTC_UC)
|
||||
|
||||
def comm_all_best_paths(self, peer):
|
||||
"""Shares/communicates current best paths with this peers.
|
||||
|
||||
Can be used to send initial updates after we have established session
|
||||
with `peer`.
|
||||
"""
|
||||
LOG.debug('Communicating current best path for all afi/safi except'
|
||||
' 1/132')
|
||||
# We will enqueue best path from all global destination.
|
||||
for route_family, table in self._table_manager.iter:
|
||||
if route_family == nlri.RF_RTC_UC:
|
||||
continue
|
||||
if peer.is_mbgp_cap_valid(route_family):
|
||||
for dest in table.itervalues():
|
||||
if dest.best_path:
|
||||
peer.communicate_path(dest.best_path)
|
||||
|
||||
def comm_new_best_to_bgp_peers(self, new_best_path):
|
||||
"""Communicates/enqueues given best path to be sent to all qualifying
|
||||
bgp peers.
|
||||
|
||||
If this path came from iBGP peers, it is not sent to other iBGP peers.
|
||||
If this path has community-attribute, and if settings for recognize-
|
||||
well-know attributes is set, we do as per [RFC1997], and queue outgoing
|
||||
route only to qualifying BGP peers.
|
||||
"""
|
||||
# Filter based on standard community
|
||||
# If new best path has community attribute, it should be taken into
|
||||
# account when sending UPDATE to peers.
|
||||
comm_attr = new_best_path.get_pattr(pathattr.Community.ATTR_NAME)
|
||||
if comm_attr:
|
||||
comm_attr_na = comm_attr.has_comm_attr(
|
||||
pathattr.Community.NO_ADVERTISE
|
||||
)
|
||||
# If we have NO_ADVERTISE attribute is present, we do not send
|
||||
# UPDATE to any peers
|
||||
if comm_attr_na:
|
||||
LOG.debug('New best path has community attr. NO_ADVERTISE = %s'
|
||||
'. Hence not advertising to any peer' % comm_attr_na)
|
||||
return
|
||||
|
||||
qualified_peers = self._collect_peers_of_interest(
|
||||
new_best_path
|
||||
)
|
||||
|
||||
# Distribute new best-path to qualified peers.
|
||||
for peer in qualified_peers:
|
||||
peer.communicate_path(new_best_path)
|
||||
|
||||
def _collect_peers_of_interest(self, new_best_path):
|
||||
"""Collect all peers that qualify for sharing a path with given RTs.
|
||||
"""
|
||||
path_rts = new_best_path.get_rts()
|
||||
qualified_peers = set(self._peers.values())
|
||||
|
||||
# Filter out peers based on RTC_AS setting if path is for RT_NLRI
|
||||
qualified_peers = self._rt_manager.filter_by_origin_as(
|
||||
new_best_path, qualified_peers
|
||||
)
|
||||
|
||||
# We continue to filter out qualified peer based on path RTs
|
||||
# If new best path has RTs, we need to share this UPDATE with
|
||||
# qualifying peers
|
||||
if path_rts:
|
||||
# We add Default_RTC_NLRI to path RTs so that we can send it to
|
||||
# peers that have expressed interest in all paths
|
||||
path_rts.append(nlri.RtNlri.DEFAULT_RT)
|
||||
# All peers that do not have RTC capability qualify
|
||||
qualified_peers = set(self._get_non_rtc_peers())
|
||||
# Peers that have RTC capability and have common RT with the path
|
||||
# also qualify
|
||||
peer_to_rtfilter_map = self._peer_to_rtfilter_map
|
||||
for peer, rt_filter in peer_to_rtfilter_map.iteritems():
|
||||
# Ignore Network Controller (its not a BGP peer)
|
||||
if peer is None:
|
||||
continue
|
||||
|
||||
if rt_filter is None:
|
||||
qualified_peers.add(peer)
|
||||
elif rt_filter.intersection(path_rts):
|
||||
qualified_peers.add(peer)
|
||||
|
||||
return qualified_peers
|
||||
|
||||
def schedule_rr_to_non_rtc_peers(self):
|
||||
for route_family in SUPPORTED_GLOBAL_RF:
|
||||
# Since we are dealing with peers that do not support RTC,
|
||||
# ignore this address family
|
||||
if route_family == nlri.RF_RTC_UC:
|
||||
continue
|
||||
|
||||
self.req_rr_to_non_rtc_peers(route_family)
|
||||
500
ryu/services/protocols/bgp/core_managers/table_manager.py
Normal file
500
ryu/services/protocols/bgp/core_managers/table_manager.py
Normal file
@ -0,0 +1,500 @@
|
||||
import logging
|
||||
|
||||
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.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.services.protocols.bgp.protocols.bgp import nlri
|
||||
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.itervalues():
|
||||
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 == nlri.RF_IPv4_VPN:
|
||||
global_table = self.get_vpn4_table()
|
||||
|
||||
elif route_family == nlri.RF_IPv6_VPN:
|
||||
global_table = self.get_vpn6_table()
|
||||
|
||||
elif route_family == nlri.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_vpn6_table(self):
|
||||
"""Returns global VPNv6 table.
|
||||
|
||||
Creates the table if it does not exist.
|
||||
"""
|
||||
vpn_table = self._global_tables.get(nlri.RF_IPv6_VPN)
|
||||
# Lazy initialize the table.
|
||||
if not vpn_table:
|
||||
vpn_table = Vpnv6Table(self._core_service, self._signal_bus)
|
||||
self._global_tables[nlri.RF_IPv6_VPN] = vpn_table
|
||||
self._tables[(None, nlri.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(nlri.RF_IPv4_VPN)
|
||||
# Lazy initialize the table.
|
||||
if not vpn_table:
|
||||
vpn_table = Vpnv4Table(self._core_service, self._signal_bus)
|
||||
self._global_tables[nlri.RF_IPv4_VPN] = vpn_table
|
||||
self._tables[(None, nlri.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(nlri.RF_RTC_UC)
|
||||
# Lazy initialization of the table.
|
||||
if not rtc_table:
|
||||
rtc_table = RtcTable(self._core_service, self._signal_bus)
|
||||
self._global_tables[nlri.RF_RTC_UC] = rtc_table
|
||||
self._tables[(None, nlri.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.itervalues():
|
||||
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.' %
|
||||
str(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 SUPPORTED_GLOBAL_RF:
|
||||
# TODO(PH): We currently do not install RT_NLRI paths based on
|
||||
# extended path attributes (RT)
|
||||
if route_family == nlri.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.' %
|
||||
str(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 = nlri.RF_IPv4_UC
|
||||
if vpn_path.route_family != nlri.RF_IPv4_VPN:
|
||||
route_family = nlri.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_disc = vpn_path.nlri.route_disc
|
||||
for vrf_table in interested_tables:
|
||||
if not (vpn_path.source is None
|
||||
and route_disc == 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.speaker.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.')
|
||||
prefix = nlri.Ipv4(prefix)
|
||||
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.')
|
||||
prefix = nlri.Ipv6(prefix)
|
||||
return vrf_table.insert_vrf_path(
|
||||
prefix, next_hop=next_hop,
|
||||
gen_lbl=True
|
||||
)
|
||||
|
||||
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.speaker.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)
|
||||
prefix = nlri.Ipv4(prefix)
|
||||
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)
|
||||
prefix = nlri.Ipv6(prefix)
|
||||
# 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)
|
||||
3
ryu/services/protocols/bgp/info_base/__init__.py
Normal file
3
ryu/services/protocols/bgp/info_base/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
"""
|
||||
Package for Information Base of various kind and for different afi/safi.
|
||||
"""
|
||||
795
ryu/services/protocols/bgp/info_base/base.py
Normal file
795
ryu/services/protocols/bgp/info_base/base.py
Normal file
@ -0,0 +1,795 @@
|
||||
# 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
|
||||
|
||||
from ryu.services.protocols.bgp.protocols.bgp.nlri import RF_IPv4_UC
|
||||
from ryu.services.protocols.bgp.protocols.bgp.nlri import RtNlri
|
||||
from ryu.services.protocols.bgp.protocols.bgp.pathattr import ExtCommunity
|
||||
|
||||
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')
|
||||
|
||||
|
||||
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).
|
||||
"""
|
||||
__metaclass__ = abc.ABCMeta
|
||||
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 itervalues(self):
|
||||
return self._destinations.itervalues()
|
||||
|
||||
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.itervalues():
|
||||
# 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.itervalues():
|
||||
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 _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)
|
||||
|
||||
|
||||
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*.
|
||||
"""
|
||||
|
||||
__metaclass__ = abc.ABCMeta
|
||||
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 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.speaker.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)
|
||||
|
||||
|
||||
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).
|
||||
"""
|
||||
__metaclass__ = ABCMeta
|
||||
__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(ExtCommunity.ATTR_NAME)
|
||||
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(RtNlri.DEFAULT_RT)
|
||||
|
||||
return not interested_rts.isdisjoint(curr_rts)
|
||||
|
||||
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))
|
||||
73
ryu/services/protocols/bgp/info_base/rtc.py
Normal file
73
ryu/services/protocols/bgp/info_base/rtc.py
Normal file
@ -0,0 +1,73 @@
|
||||
# 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 data types and models required specifically for RTC support.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from ryu.services.protocols.bgp.protocols.bgp.nlri import RF_RTC_UC
|
||||
|
||||
from ryu.services.protocols.bgp.info_base.base import Destination
|
||||
from ryu.services.protocols.bgp.info_base.base import NonVrfPathProcessingMixin
|
||||
from ryu.services.protocols.bgp.info_base.base import Path
|
||||
from ryu.services.protocols.bgp.info_base.base import Table
|
||||
|
||||
LOG = logging.getLogger('bgpspeaker.info_base.rtc')
|
||||
|
||||
|
||||
class RtcTable(Table):
|
||||
"""Global table to store RT membership information.
|
||||
|
||||
Uses `RtDest` to store destination information for each known RT NLRI path.
|
||||
"""
|
||||
ROUTE_FAMILY = RF_RTC_UC
|
||||
|
||||
def __init__(self, core_service, signal_bus):
|
||||
Table.__init__(self, None, core_service, signal_bus)
|
||||
|
||||
def _table_key(self, rtc_nlri):
|
||||
"""Return a key that will uniquely identify this RT NLRI inside
|
||||
this table.
|
||||
"""
|
||||
return str(rtc_nlri.origin_as) + ':' + rtc_nlri.route_target
|
||||
|
||||
def _create_dest(self, nlri):
|
||||
return RtcDest(self, nlri)
|
||||
|
||||
def __str__(self):
|
||||
return 'RtcTable(scope_id: %s, rf: %s)' % (self.scope_id,
|
||||
self.route_family)
|
||||
|
||||
|
||||
class RtcDest(Destination, NonVrfPathProcessingMixin):
|
||||
ROUTE_FAMILY = RF_RTC_UC
|
||||
|
||||
def _new_best_path(self, new_best_path):
|
||||
NonVrfPathProcessingMixin._new_best_path(self, new_best_path)
|
||||
|
||||
def _best_path_lost(self):
|
||||
NonVrfPathProcessingMixin._best_path_lost(self)
|
||||
|
||||
|
||||
class RtcPath(Path):
|
||||
ROUTE_FAMILY = RF_RTC_UC
|
||||
|
||||
def __init__(self, source, nlri, src_ver_num, pattrs=None,
|
||||
nexthop='0.0.0.0', is_withdraw=False,
|
||||
med_set_by_target_neighbor=False):
|
||||
Path.__init__(self, source, nlri, src_ver_num, pattrs, nexthop,
|
||||
is_withdraw, med_set_by_target_neighbor)
|
||||
109
ryu/services/protocols/bgp/info_base/vpn.py
Normal file
109
ryu/services/protocols/bgp/info_base/vpn.py
Normal file
@ -0,0 +1,109 @@
|
||||
# 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 base data types and models required specifically for VPN support.
|
||||
"""
|
||||
|
||||
import abc
|
||||
import logging
|
||||
|
||||
from ryu.services.protocols.bgp.info_base.base import Destination
|
||||
from ryu.services.protocols.bgp.info_base.base import NonVrfPathProcessingMixin
|
||||
from ryu.services.protocols.bgp.info_base.base import Path
|
||||
from ryu.services.protocols.bgp.info_base.base import Table
|
||||
|
||||
LOG = logging.getLogger('bgpspeaker.info_base.vpn')
|
||||
|
||||
|
||||
class VpnTable(Table):
|
||||
"""Global table to store VPNv4 routing information.
|
||||
|
||||
Uses `VpnvXDest` to store destination information for each known vpnvX
|
||||
paths.
|
||||
"""
|
||||
ROUTE_FAMILY = None
|
||||
VPN_DEST_CLASS = None
|
||||
|
||||
def __init__(self, core_service, signal_bus):
|
||||
super(VpnTable, self).__init__(None, core_service, signal_bus)
|
||||
|
||||
def _table_key(self, vpn_nlri):
|
||||
"""Return a key that will uniquely identify this vpnvX NLRI inside
|
||||
this table.
|
||||
"""
|
||||
return vpn_nlri.route_disc + ':' + vpn_nlri.prefix
|
||||
|
||||
def _create_dest(self, nlri):
|
||||
return self.VPN_DEST_CLASS(self, nlri)
|
||||
|
||||
def __str__(self):
|
||||
return '%s(scope_id: %s, rf: %s)' % (
|
||||
self.__class__.__name__, self.scope_id, self.route_family
|
||||
)
|
||||
|
||||
|
||||
class VpnPath(Path):
|
||||
__metaclass__ = abc.ABCMeta
|
||||
ROUTE_FAMILY = None
|
||||
VRF_PATH_CLASS = None
|
||||
NLRI_CLASS = None
|
||||
|
||||
def clone_to_vrf(self, is_withdraw=False):
|
||||
vrf_nlri = self.NLRI_CLASS(self._nlri.prefix)
|
||||
|
||||
pathattrs = None
|
||||
if not is_withdraw:
|
||||
pathattrs = self.pathattr_map
|
||||
|
||||
vrf_path = self.VRF_PATH_CLASS(
|
||||
self.VRF_PATH_CLASS.create_puid(
|
||||
self._nlri.route_disc,
|
||||
self._nlri.prefix
|
||||
),
|
||||
self.source, vrf_nlri,
|
||||
self.source_version_num,
|
||||
pattrs=pathattrs,
|
||||
nexthop=self.nexthop,
|
||||
is_withdraw=is_withdraw,
|
||||
label_list=self._nlri.label_list)
|
||||
return vrf_path
|
||||
|
||||
|
||||
class VpnDest(Destination, NonVrfPathProcessingMixin):
|
||||
"""Base class for VPN destinations."""
|
||||
|
||||
__metaclass__ = abc.ABCMeta
|
||||
|
||||
def _best_path_lost(self):
|
||||
old_best_path = self._best_path
|
||||
NonVrfPathProcessingMixin._best_path_lost(self)
|
||||
|
||||
# Best-path might have been imported into VRF tables, we have to
|
||||
# withdraw from them, if the source is a peer.
|
||||
if old_best_path:
|
||||
withdraw_clone = old_best_path.clone(for_withdrawal=True)
|
||||
tm = self._core_service.table_manager
|
||||
tm.import_single_vpn_path_to_all_vrfs(
|
||||
withdraw_clone, path_rts=old_best_path.get_rts()
|
||||
)
|
||||
|
||||
def _new_best_path(self, best_path):
|
||||
NonVrfPathProcessingMixin._new_best_path(self, best_path)
|
||||
|
||||
# Extranet feature requires that we import new best path into VRFs.
|
||||
tm = self._core_service.table_manager
|
||||
tm.import_single_vpn_path_to_all_vrfs(
|
||||
self._best_path, self._best_path.get_rts())
|
||||
59
ryu/services/protocols/bgp/info_base/vpnv4.py
Normal file
59
ryu/services/protocols/bgp/info_base/vpnv4.py
Normal file
@ -0,0 +1,59 @@
|
||||
# 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 data types and models required specifically for VPNv4 support.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from ryu.services.protocols.bgp.protocols.bgp.nlri import Ipv4
|
||||
from ryu.services.protocols.bgp.protocols.bgp.nlri import RF_IPv4_VPN
|
||||
|
||||
from ryu.services.protocols.bgp.info_base.vpn import VpnDest
|
||||
from ryu.services.protocols.bgp.info_base.vpn import VpnPath
|
||||
from ryu.services.protocols.bgp.info_base.vpn import VpnTable
|
||||
|
||||
LOG = logging.getLogger('bgpspeaker.info_base.vpnv4')
|
||||
|
||||
|
||||
class Vpnv4Dest(VpnDest):
|
||||
"""VPNv4 Destination
|
||||
|
||||
Store IPv4 Paths.
|
||||
"""
|
||||
ROUTE_FAMILY = RF_IPv4_VPN
|
||||
|
||||
|
||||
class Vpnv4Table(VpnTable):
|
||||
"""Global table to store VPNv4 routing information.
|
||||
|
||||
Uses `Vpnv4Dest` to store destination information for each known vpnv4
|
||||
paths.
|
||||
"""
|
||||
ROUTE_FAMILY = RF_IPv4_VPN
|
||||
VPN_DEST_CLASS = Vpnv4Dest
|
||||
|
||||
|
||||
class Vpnv4Path(VpnPath):
|
||||
"""Represents a way of reaching an VPNv4 destination."""
|
||||
ROUTE_FAMILY = RF_IPv4_VPN
|
||||
VRF_PATH_CLASS = None # defined in init - anti cyclic import hack
|
||||
NLRI_CLASS = Ipv4
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(Vpnv4Path, self).__init__(*args, **kwargs)
|
||||
from ryu.services.protocols.bgp.speaker.info_base.vrf4 import Vrf4Path
|
||||
self.VRF_PATH_CLASS = Vrf4Path
|
||||
59
ryu/services/protocols/bgp/info_base/vpnv6.py
Normal file
59
ryu/services/protocols/bgp/info_base/vpnv6.py
Normal file
@ -0,0 +1,59 @@
|
||||
# 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 data types and models required specifically for VPNv6 support.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from ryu.services.protocols.bgp.protocols.bgp.nlri import Ipv6
|
||||
from ryu.services.protocols.bgp.protocols.bgp.nlri import RF_IPv6_VPN
|
||||
|
||||
from ryu.services.protocols.bgp.info_base.vpn import VpnDest
|
||||
from ryu.services.protocols.bgp.info_base.vpn import VpnPath
|
||||
from ryu.services.protocols.bgp.info_base.vpn import VpnTable
|
||||
|
||||
LOG = logging.getLogger('bgpspeaker.info_base.vpnv6')
|
||||
|
||||
|
||||
class Vpnv6Dest(VpnDest):
|
||||
"""VPNv6 destination
|
||||
|
||||
Stores IPv6 paths.
|
||||
"""
|
||||
ROUTE_FAMILY = RF_IPv6_VPN
|
||||
|
||||
|
||||
class Vpnv6Table(VpnTable):
|
||||
"""Global table to store VPNv6 routing information
|
||||
|
||||
Uses `Vpnv6Dest` to store destination information for each known vpnv6
|
||||
paths.
|
||||
"""
|
||||
ROUTE_FAMILY = RF_IPv6_VPN
|
||||
VPN_DEST_CLASS = Vpnv6Dest
|
||||
|
||||
|
||||
class Vpnv6Path(VpnPath):
|
||||
"""Represents a way of reaching an VPNv4 destination."""
|
||||
ROUTE_FAMILY = RF_IPv6_VPN
|
||||
VRF_PATH_CLASS = None # defined in init - anti cyclic import hack
|
||||
NLRI_CLASS = Ipv6
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(Vpnv6Path, self).__init__(*args, **kwargs)
|
||||
from ryu.services.protocols.bgp.speaker.info_base.vrf6 import Vrf6Path
|
||||
self.VRF_PATH_CLASS = Vrf6Path
|
||||
530
ryu/services/protocols/bgp/info_base/vrf.py
Normal file
530
ryu/services/protocols/bgp/info_base/vrf.py
Normal file
@ -0,0 +1,530 @@
|
||||
# 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 base data types and models required specifically for VRF support.
|
||||
"""
|
||||
|
||||
import abc
|
||||
import logging
|
||||
|
||||
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.info_base.base import Destination
|
||||
from ryu.services.protocols.bgp.info_base.base import Path
|
||||
from ryu.services.protocols.bgp.info_base.base import Table
|
||||
from ryu.services.protocols.bgp.utils.stats import LOCAL_ROUTES
|
||||
from ryu.services.protocols.bgp.utils.stats import REMOTE_ROUTES
|
||||
from ryu.services.protocols.bgp.utils.stats import RESOURCE_ID
|
||||
from ryu.services.protocols.bgp.utils.stats import RESOURCE_NAME
|
||||
from ryu.services.protocols.bgp.protocols.bgp import pathattr
|
||||
|
||||
LOG = logging.getLogger('bgpspeaker.info_base.vrf')
|
||||
|
||||
|
||||
class VrfTable(Table):
|
||||
"""Virtual Routing and Forwarding information base.
|
||||
Keeps destination imported to given vrf in represents.
|
||||
"""
|
||||
|
||||
__metaclass__ = abc.ABCMeta
|
||||
ROUTE_FAMILY = None
|
||||
VPN_ROUTE_FAMILY = None
|
||||
NLRI_CLASS = None
|
||||
VRF_PATH_CLASS = None
|
||||
VRF_DEST_CLASS = None
|
||||
|
||||
def __init__(self, vrf_conf, core_service, signal_bus):
|
||||
Table.__init__(self, vrf_conf.route_dist, core_service, signal_bus)
|
||||
self._vrf_conf = vrf_conf
|
||||
self._import_maps = []
|
||||
self.init_import_maps(vrf_conf.import_maps)
|
||||
|
||||
def init_import_maps(self, import_maps):
|
||||
LOG.debug(
|
||||
"Initializing import maps (%s) for %s" % (import_maps, repr(self))
|
||||
)
|
||||
del self._import_maps[:]
|
||||
importmap_manager = self._core_service.importmap_manager
|
||||
for name in import_maps:
|
||||
import_map = importmap_manager.get_import_map_by_name(name)
|
||||
if import_map is None:
|
||||
raise KeyError('No import map with name %s' % name)
|
||||
self._import_maps.append(import_map)
|
||||
|
||||
@property
|
||||
def import_rts(self):
|
||||
return self._vrf_conf.import_rts
|
||||
|
||||
@property
|
||||
def vrf_conf(self):
|
||||
return self._vrf_conf
|
||||
|
||||
def _table_key(self, nlri):
|
||||
"""Return a key that will uniquely identify this NLRI inside
|
||||
this table.
|
||||
"""
|
||||
return str(nlri)
|
||||
|
||||
def _create_dest(self, nlri):
|
||||
return self.VRF_DEST_CLASS(self, nlri)
|
||||
|
||||
def append_import_map(self, import_map):
|
||||
self._import_maps.append(import_map)
|
||||
|
||||
def remove_import_map(self, import_map):
|
||||
self._import_maps.remove(import_map)
|
||||
|
||||
def get_stats_summary_dict(self):
|
||||
"""Returns count of local and remote paths."""
|
||||
|
||||
remote_route_count = 0
|
||||
local_route_count = 0
|
||||
for dest in self.itervalues():
|
||||
for path in dest.known_path_list:
|
||||
if (hasattr(path.source, 'version_num')
|
||||
or path.source == VPN_TABLE):
|
||||
remote_route_count += 1
|
||||
else:
|
||||
local_route_count += 1
|
||||
return {RESOURCE_ID: self._vrf_conf.id,
|
||||
RESOURCE_NAME: self._vrf_conf.name,
|
||||
REMOTE_ROUTES: remote_route_count,
|
||||
LOCAL_ROUTES: local_route_count}
|
||||
|
||||
def import_vpn_paths_from_table(self, vpn_table, import_rts=None):
|
||||
for vpn_dest in vpn_table.itervalues():
|
||||
vpn_path = vpn_dest.best_path
|
||||
if not vpn_path:
|
||||
continue
|
||||
|
||||
if import_rts is None:
|
||||
import_rts = set(self.import_rts)
|
||||
else:
|
||||
import_rts = set(import_rts)
|
||||
|
||||
path_rts = vpn_path.get_rts()
|
||||
if import_rts.intersection(path_rts):
|
||||
# TODO(PH): When (re-)implementing extranet, check what should
|
||||
# be the label reported back to NC for local paths coming from
|
||||
# other VRFs.
|
||||
self.import_vpn_path(vpn_path)
|
||||
|
||||
def import_vpn_path(self, vpn_path):
|
||||
"""Imports `vpnv(4|6)_path` into `vrf(4|6)_table`.
|
||||
|
||||
:Parameters:
|
||||
- `vpn_path`: (Path) VPN path that will be cloned and imported
|
||||
into VRF.
|
||||
Note: Does not do any checking if this import is valid.
|
||||
"""
|
||||
assert vpn_path.route_family == self.VPN_ROUTE_FAMILY
|
||||
# If source of given vpnv4 path is NC we import it to given VRF
|
||||
# table because of extranet setting. Hence we identify source of
|
||||
# EXTRANET prefixes as VRF_TABLE, else VPN_TABLE.
|
||||
source = vpn_path.source
|
||||
if not source:
|
||||
source = VRF_TABLE
|
||||
|
||||
vrf_nlri = self.NLRI_CLASS(vpn_path.nlri.prefix)
|
||||
|
||||
vpn_nlri = vpn_path.nlri
|
||||
puid = self.VRF_PATH_CLASS.create_puid(vpn_nlri.route_disc,
|
||||
vpn_nlri.prefix)
|
||||
vrf_path = self.VRF_PATH_CLASS(
|
||||
puid,
|
||||
source,
|
||||
vrf_nlri,
|
||||
vpn_path.source_version_num,
|
||||
pattrs=vpn_path.pathattr_map,
|
||||
nexthop=vpn_path.nexthop,
|
||||
is_withdraw=vpn_path.is_withdraw,
|
||||
label_list=vpn_path.nlri.label_list
|
||||
)
|
||||
if self._is_vrf_path_already_in_table(vrf_path):
|
||||
return None
|
||||
|
||||
if self._is_vrf_path_filtered_out_by_import_maps(vrf_path):
|
||||
return None
|
||||
else:
|
||||
vrf_dest = self.insert(vrf_path)
|
||||
self._signal_bus.dest_changed(vrf_dest)
|
||||
|
||||
def _is_vrf_path_filtered_out_by_import_maps(self, vrf_path):
|
||||
for import_map in self._import_maps:
|
||||
if import_map.match(vrf_path):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _is_vrf_path_already_in_table(self, vrf_path):
|
||||
dest = self._get_dest(vrf_path.nlri)
|
||||
if dest is None:
|
||||
return False
|
||||
return vrf_path in dest.known_path_list
|
||||
|
||||
def apply_import_maps(self):
|
||||
changed_dests = []
|
||||
for dest in self.itervalues():
|
||||
assert isinstance(dest, VrfDest)
|
||||
for import_map in self._import_maps:
|
||||
for path in dest.known_path_list:
|
||||
if import_map.match(path):
|
||||
dest.withdraw_path(path)
|
||||
changed_dests.append(dest)
|
||||
return changed_dests
|
||||
|
||||
def insert_vrf_path(self, ip_nlri, next_hop=None,
|
||||
gen_lbl=False, is_withdraw=False):
|
||||
assert ip_nlri
|
||||
pattrs = None
|
||||
label_list = []
|
||||
vrf_conf = self.vrf_conf
|
||||
if not is_withdraw:
|
||||
# Create a dictionary for path-attrs.
|
||||
pattrs = OrderedDict()
|
||||
|
||||
# MpReachNlri and/or MpUnReachNlri attribute info. is contained
|
||||
# in the path. Hence we do not add these attributes here.
|
||||
from ryu.services.protocols.bgp.core import EXPECTED_ORIGIN
|
||||
|
||||
pattrs[pathattr.Origin.ATTR_NAME] = \
|
||||
pathattr.Origin(EXPECTED_ORIGIN)
|
||||
pattrs[pathattr.AsPath.ATTR_NAME] = pathattr.AsPath([])
|
||||
pattrs[pathattr.ExtCommunity.ATTR_NAME] = pathattr.ExtCommunity(
|
||||
rt_list=vrf_conf.export_rts, soo_list=vrf_conf.soo_list)
|
||||
if vrf_conf.multi_exit_disc:
|
||||
pattrs[pathattr.Med.ATTR_NAME] = pathattr.Med(
|
||||
vrf_conf.multi_exit_disc
|
||||
)
|
||||
|
||||
table_manager = self._core_service.table_manager
|
||||
if gen_lbl and next_hop:
|
||||
# Label per next_hop demands we use a different label
|
||||
# per next_hop. Here connected interfaces are advertised per
|
||||
# VRF.
|
||||
label_key = (vrf_conf.route_dist, next_hop)
|
||||
nh_label = table_manager.get_nexthop_label(label_key)
|
||||
if not nh_label:
|
||||
nh_label = table_manager.get_next_vpnv4_label()
|
||||
table_manager.set_nexthop_label(label_key, nh_label)
|
||||
label_list.append(nh_label)
|
||||
|
||||
elif gen_lbl:
|
||||
# If we do not have next_hop, get a new label.
|
||||
label_list.append(table_manager.get_next_vpnv4_label())
|
||||
|
||||
puid = self.VRF_PATH_CLASS.create_puid(
|
||||
vrf_conf.route_dist, ip_nlri.prefix
|
||||
)
|
||||
path = self.VRF_PATH_CLASS(
|
||||
puid, None, ip_nlri, 0, pattrs=pattrs,
|
||||
nexthop=next_hop, label_list=label_list,
|
||||
is_withdraw=is_withdraw
|
||||
)
|
||||
|
||||
# Insert the path into VRF table, get affected destination so that we
|
||||
# can process it further.
|
||||
eff_dest = self.insert(path)
|
||||
# Enqueue the eff_dest for further processing.
|
||||
self._signal_bus.dest_changed(eff_dest)
|
||||
return label_list
|
||||
|
||||
def clean_uninteresting_paths(self, interested_rts=None):
|
||||
if interested_rts is None:
|
||||
interested_rts = set(self.vrf_conf.import_rts)
|
||||
return super(VrfTable, self).clean_uninteresting_paths(interested_rts)
|
||||
|
||||
|
||||
class VrfDest(Destination):
|
||||
"""Base class for VRF destination."""
|
||||
__metaclass__ = abc.ABCMeta
|
||||
|
||||
def __init__(self, table, nlri):
|
||||
super(VrfDest, self).__init__(table, nlri)
|
||||
self._route_disc = self._table.vrf_conf.route_dist
|
||||
|
||||
def _best_path_lost(self):
|
||||
# Have to send update messages for withdraw of best-path to Network
|
||||
# controller or Global table.
|
||||
old_best_path = self._best_path
|
||||
self._best_path = None
|
||||
|
||||
if old_best_path is None:
|
||||
return
|
||||
|
||||
if old_best_path.source is not None:
|
||||
# Send update-withdraw msg. to Sink. Create withdraw path
|
||||
# out of old best path and queue it into flexinet sinks.
|
||||
old_best_path = old_best_path.clone(for_withdrawal=True)
|
||||
self._core_service.update_flexinet_peers(old_best_path,
|
||||
self._route_disc)
|
||||
else:
|
||||
# Create withdraw-path out of old best path.
|
||||
gpath = old_best_path.clone_to_vpn(self._route_disc,
|
||||
for_withdrawal=True)
|
||||
# Insert withdraw into global table and enqueue the destination
|
||||
# for further processing.
|
||||
tm = self._core_service.table_manager
|
||||
tm.learn_path(gpath)
|
||||
|
||||
def _new_best_path(self, best_path):
|
||||
LOG.debug('New best path selected for destination %s' % (self))
|
||||
|
||||
old_best_path = self._best_path
|
||||
assert (best_path != old_best_path)
|
||||
self._best_path = best_path
|
||||
# Distribute new best-path to flexinet-peers.
|
||||
if best_path.source is not None:
|
||||
# Since route-refresh just causes the version number to
|
||||
# go up and this changes best-path, we check if new-
|
||||
# best-path is really different than old-best-path that
|
||||
# warrants sending update to flexinet peers.
|
||||
|
||||
def really_diff():
|
||||
old_labels = old_best_path.label_list
|
||||
new_labels = best_path.label_list
|
||||
return old_best_path.nexthop != best_path.nexthop \
|
||||
or set(old_labels) != set(new_labels)
|
||||
|
||||
if not old_best_path or (old_best_path and really_diff()):
|
||||
# Create OutgoingRoute and queue it into NC sink.
|
||||
self._core_service.update_flexinet_peers(
|
||||
best_path, self._route_disc
|
||||
)
|
||||
else:
|
||||
# If NC is source, we create new path and insert into global
|
||||
# table.
|
||||
gpath = best_path.clone_to_vpn(self._route_disc)
|
||||
tm = self._core_service.table_manager
|
||||
tm.learn_path(gpath)
|
||||
LOG.debug('VRF table %s has new best path: %s' %
|
||||
(self._route_disc, self.best_path))
|
||||
|
||||
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 not 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 = []
|
||||
w_matches = []
|
||||
# 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.puid == withdraw.puid:
|
||||
match = path
|
||||
matches.append(path)
|
||||
w_matches.append(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.puid == path.puid):
|
||||
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 _validate_path(self, path):
|
||||
if not path or not hasattr(path, 'label_list'):
|
||||
raise ValueError('Invalid value of path. Expected type '
|
||||
'with attribute label_list got %s' % path)
|
||||
|
||||
|
||||
class VrfPath(Path):
|
||||
"""Represents a way of reaching an IP destination with a VPN.
|
||||
"""
|
||||
__slots__ = ('_label_list', '_puid')
|
||||
__metaclass__ = abc.ABCMeta
|
||||
|
||||
ROUTE_FAMILY = None
|
||||
VPN_PATH_CLASS = None
|
||||
VPN_NLRI_CLASS = None
|
||||
|
||||
def __init__(self, puid, source, nlri, src_ver_num,
|
||||
pattrs=None, nexthop=None,
|
||||
is_withdraw=False, label_list=None):
|
||||
"""Initializes a Vrf path.
|
||||
|
||||
Parameters:
|
||||
- `puid`: (str) path ID, identifies VPN path from which this
|
||||
VRF path was imported.
|
||||
- `label_list`: (list) List of labels for this path.
|
||||
Note: other parameters are as documented in super class.
|
||||
"""
|
||||
Path.__init__(self, source, nlri, src_ver_num, pattrs, nexthop,
|
||||
is_withdraw)
|
||||
if label_list is None:
|
||||
label_list = []
|
||||
self._label_list = label_list
|
||||
self._puid = puid
|
||||
|
||||
@property
|
||||
def puid(self):
|
||||
return self._puid
|
||||
|
||||
@property
|
||||
def origin_rd(self):
|
||||
tokens = self.puid.split(':')
|
||||
return tokens[0] + ':' + tokens[1]
|
||||
|
||||
@property
|
||||
def label_list(self):
|
||||
return self._label_list[:]
|
||||
|
||||
@staticmethod
|
||||
def create_puid(route_disc, ip_prefix):
|
||||
assert route_disc and ip_prefix
|
||||
return route_disc + ':' + ip_prefix
|
||||
|
||||
def clone(self, for_withdrawal=False):
|
||||
pathattrs = None
|
||||
if not for_withdrawal:
|
||||
pathattrs = self.pathattr_map
|
||||
|
||||
clone = self.__class__(
|
||||
self.puid,
|
||||
self._source,
|
||||
self.nlri,
|
||||
self.source_version_num,
|
||||
pattrs=pathattrs,
|
||||
nexthop=self.nexthop,
|
||||
is_withdraw=for_withdrawal,
|
||||
label_list=self.label_list
|
||||
)
|
||||
return clone
|
||||
|
||||
def clone_to_vpn(self, route_disc, for_withdrawal=False):
|
||||
vpn_nlri = self.VPN_NLRI_CLASS(
|
||||
self.label_list, route_disc, self._nlri.prefix
|
||||
)
|
||||
|
||||
pathattrs = None
|
||||
if not for_withdrawal:
|
||||
pathattrs = self.pathattr_map
|
||||
vpnv_path = self.VPN_PATH_CLASS(
|
||||
self.source, vpn_nlri,
|
||||
self.source_version_num,
|
||||
pattrs=pathattrs,
|
||||
nexthop=self.nexthop,
|
||||
is_withdraw=for_withdrawal
|
||||
)
|
||||
return vpnv_path
|
||||
|
||||
def __eq__(self, b_path):
|
||||
if not isinstance(b_path, self.__class__):
|
||||
return False
|
||||
if not self.route_family == b_path.route_family:
|
||||
return False
|
||||
if not self.puid == b_path.puid:
|
||||
return False
|
||||
if not self.label_list == b_path.label_list:
|
||||
return False
|
||||
if not self.nexthop == b_path.nexthop:
|
||||
return False
|
||||
if not self.pathattr_map == b_path.pathattr_map:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class ImportMap(object):
|
||||
def match(self, vrf_path):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class VrfNlriImportMap(ImportMap):
|
||||
VRF_PATH_CLASS = None
|
||||
NLRI_CLASS = None
|
||||
|
||||
def __init__(self, prefix):
|
||||
assert self.VRF_PATH_CLASS is not None
|
||||
assert self.NLRI_CLASS is not None
|
||||
self._nlri = self.NLRI_CLASS(prefix)
|
||||
|
||||
def match(self, vrf_path):
|
||||
if vrf_path.route_family != self.VRF_PATH_CLASS.ROUTE_FAMILY:
|
||||
LOG.error(
|
||||
"vrf_paths route_family doesn\'t match importmaps"
|
||||
"route_family. Applied to wrong table?")
|
||||
return False
|
||||
|
||||
return vrf_path.nlri == self._nlri
|
||||
|
||||
|
||||
class VrfRtImportMap(ImportMap):
|
||||
def __init__(self, rt):
|
||||
self._rt = rt
|
||||
|
||||
def match(self, vrf_path):
|
||||
extcomm = vrf_path.pathattr_map.get('extcommunity')
|
||||
return extcomm is not None and self._rt in extcomm.rt_list
|
||||
60
ryu/services/protocols/bgp/info_base/vrf4.py
Normal file
60
ryu/services/protocols/bgp/info_base/vrf4.py
Normal file
@ -0,0 +1,60 @@
|
||||
# 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 data types and models required specifically for VRF (for IPv4)
|
||||
support. Represents data structures for VRF not VPN/global.
|
||||
(Inside VRF you have IPv4 prefixes and inside VPN you have VPNv4 prefixes)
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from ryu.services.protocols.bgp.protocols.bgp.nlri import Ipv4
|
||||
from ryu.services.protocols.bgp.protocols.bgp.nlri import RF_IPv4_UC
|
||||
from ryu.services.protocols.bgp.protocols.bgp.nlri import RF_IPv4_VPN
|
||||
from ryu.services.protocols.bgp.protocols.bgp.nlri import Vpnv4
|
||||
|
||||
from ryu.services.protocols.bgp.info_base.vpnv4 import Vpnv4Path
|
||||
from ryu.services.protocols.bgp.info_base.vrf import VrfDest
|
||||
from ryu.services.protocols.bgp.info_base.vrf import VrfNlriImportMap
|
||||
from ryu.services.protocols.bgp.info_base.vrf import VrfPath
|
||||
from ryu.services.protocols.bgp.info_base.vrf import VrfTable
|
||||
|
||||
LOG = logging.getLogger('bgpspeaker.info_base.vrf4')
|
||||
|
||||
|
||||
class Vrf4Path(VrfPath):
|
||||
"""Represents a way of reaching an IP destination with a VPN."""
|
||||
ROUTE_FAMILY = RF_IPv4_UC
|
||||
VPN_PATH_CLASS = Vpnv4Path
|
||||
VPN_NLRI_CLASS = Vpnv4
|
||||
|
||||
|
||||
class Vrf4Dest(VrfDest):
|
||||
ROUTE_FAMILY = RF_IPv4_UC
|
||||
|
||||
|
||||
class Vrf4Table(VrfTable):
|
||||
"""Virtual Routing and Forwarding information base for IPv4."""
|
||||
ROUTE_FAMILY = RF_IPv4_UC
|
||||
VPN_ROUTE_FAMILY = RF_IPv4_VPN
|
||||
NLRI_CLASS = Ipv4
|
||||
VRF_PATH_CLASS = Vrf4Path
|
||||
VRF_DEST_CLASS = Vrf4Dest
|
||||
|
||||
|
||||
class Vrf4NlriImportMap(VrfNlriImportMap):
|
||||
VRF_PATH_CLASS = Vrf4Path
|
||||
NLRI_CLASS = Ipv4
|
||||
61
ryu/services/protocols/bgp/info_base/vrf6.py
Normal file
61
ryu/services/protocols/bgp/info_base/vrf6.py
Normal file
@ -0,0 +1,61 @@
|
||||
# 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 data types and models required specifically for VRF (for IPv6)
|
||||
support. Represents data structures for VRF not VPN/global.
|
||||
(Inside VRF you have IPv4 prefixes and inside VPN you have VPNv6 prefixes)
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from ryu.services.protocols.bgp.protocols.bgp.nlri import Ipv6
|
||||
from ryu.services.protocols.bgp.protocols.bgp.nlri import RF_IPv6_UC
|
||||
from ryu.services.protocols.bgp.protocols.bgp.nlri import RF_IPv6_VPN
|
||||
from ryu.services.protocols.bgp.protocols.bgp.nlri import Vpnv6
|
||||
|
||||
from ryu.services.protocols.bgp.info_base.vpnv6 import Vpnv6Path
|
||||
from ryu.services.protocols.bgp.info_base.vrf import VrfDest
|
||||
from ryu.services.protocols.bgp.info_base.vrf import VrfNlriImportMap
|
||||
from ryu.services.protocols.bgp.info_base.vrf import VrfPath
|
||||
from ryu.services.protocols.bgp.info_base.vrf import VrfTable
|
||||
|
||||
LOG = logging.getLogger('bgpspeaker.info_base.vrf6')
|
||||
|
||||
|
||||
class Vrf6Path(VrfPath):
|
||||
"""Represents a way of reaching an IP destination with a VPN."""
|
||||
ROUTE_FAMILY = RF_IPv6_UC
|
||||
VPN_PATH_CLASS = Vpnv6Path
|
||||
VPN_NLRI_CLASS = Vpnv6
|
||||
|
||||
|
||||
class Vrf6Dest(VrfDest):
|
||||
"""Destination for IPv6 VRFs."""
|
||||
ROUTE_FAMILY = RF_IPv6_UC
|
||||
|
||||
|
||||
class Vrf6Table(VrfTable):
|
||||
"""Virtual Routing and Forwarding information base for IPv6."""
|
||||
ROUTE_FAMILY = RF_IPv6_UC
|
||||
VPN_ROUTE_FAMILY = RF_IPv6_VPN
|
||||
NLRI_CLASS = Ipv6
|
||||
VRF_PATH_CLASS = Vrf6Path
|
||||
VRF_DEST_CLASS = Vrf6Dest
|
||||
|
||||
|
||||
class Vrf6NlriImportMap(VrfNlriImportMap):
|
||||
VRF_PATH_CLASS = Vrf6Path
|
||||
NLRI_CLASS = Ipv6
|
||||
148
ryu/services/protocols/bgp/model.py
Normal file
148
ryu/services/protocols/bgp/model.py
Normal file
@ -0,0 +1,148 @@
|
||||
# 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 logging
|
||||
|
||||
|
||||
LOG = logging.getLogger('bgpspeaker.model')
|
||||
|
||||
|
||||
class Counter(object):
|
||||
"""Simple counter for keeping count of several keys."""
|
||||
def __init__(self):
|
||||
self._counters = {}
|
||||
|
||||
def incr(self, counter_name, incr_by=1):
|
||||
self._counters[counter_name] = \
|
||||
self._counters.get(counter_name, 0) + incr_by
|
||||
|
||||
def get_count(self, counter_name):
|
||||
return self._counters.get(counter_name, 0)
|
||||
|
||||
def get_counters(self):
|
||||
return self._counters.copy()
|
||||
|
||||
|
||||
class OutgoingRoute(object):
|
||||
"""Holds state about a route that is queued for being sent to a given sink.
|
||||
"""
|
||||
|
||||
__slots__ = ('_path', '_for_route_refresh',
|
||||
'sink', 'next_outgoing_route', 'prev_outgoing_route',
|
||||
'next_sink_out_route', 'prev_sink_out_route')
|
||||
|
||||
def __init__(self, path, for_route_refresh=False):
|
||||
assert(path)
|
||||
|
||||
self.sink = None
|
||||
|
||||
self._path = path
|
||||
|
||||
# Is this update in response for route-refresh request.
|
||||
# No sent-route is queued for the destination for this update.
|
||||
self._for_route_refresh = for_route_refresh
|
||||
|
||||
# Automatically generated, for list off of Destination.
|
||||
#
|
||||
# self.next_outgoing_route
|
||||
# self.prev_outgoing_route
|
||||
|
||||
# Automatically generated for list off of sink.
|
||||
#
|
||||
# self.next_sink_out_route
|
||||
# self.prev_sink_out_route
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
return self._path
|
||||
|
||||
@property
|
||||
def for_route_refresh(self):
|
||||
return self._for_route_refresh
|
||||
|
||||
def __str__(self):
|
||||
return ('OutgoingRoute(path: %s, for_route_refresh: %s)' %
|
||||
(self.path, self.for_route_refresh))
|
||||
|
||||
|
||||
class FlexinetOutgoingRoute(object):
|
||||
"""Holds state about a route that is queued for being sent to a given sink.
|
||||
|
||||
In this case the sink is flexinet peer and this route information is from
|
||||
a VRF which holds Ipv4(v6) NLRIs.
|
||||
"""
|
||||
|
||||
__slots__ = ('_path', 'sink', 'next_outgoing_route', 'prev_outgoing_route',
|
||||
'next_sink_out_route', 'prev_sink_out_route', '_route_disc')
|
||||
|
||||
def __init__(self, path, route_disc):
|
||||
from ryu.services.protocols.bgp.speaker.info_base.vrf4 import Vrf4Path
|
||||
from ryu.services.protocols.bgp.speaker.info_base.vrf6 import Vrf6Path
|
||||
assert path.route_family in (Vrf4Path.ROUTE_FAMILY,
|
||||
Vrf6Path.ROUTE_FAMILY)
|
||||
|
||||
self.sink = None
|
||||
self._path = path
|
||||
self._route_disc = route_disc
|
||||
|
||||
# Automatically generated, for list off of Destination.
|
||||
#
|
||||
# self.next_outgoing_route
|
||||
# self.prev_outgoing_route
|
||||
|
||||
# Automatically generated for list off of sink.
|
||||
#
|
||||
# self.next_sink_out_route
|
||||
# self.prev_sink_out_route
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
return self._path
|
||||
|
||||
@property
|
||||
def route_disc(self):
|
||||
return self._route_disc
|
||||
|
||||
def __str__(self):
|
||||
return ('FlexinetOutgoingRoute(path: %s, route_disc: %s)' %
|
||||
(self.path, self.route_disc))
|
||||
|
||||
|
||||
class SentRoute(object):
|
||||
"""Holds the information that has been sent to one or more sinks
|
||||
about a particular BGP destination.
|
||||
"""
|
||||
|
||||
def __init__(self, path, peer):
|
||||
assert(path and hasattr(peer, 'version_num'))
|
||||
|
||||
self.path = path
|
||||
|
||||
# Peer to which this path was sent.
|
||||
self._sent_peer = peer
|
||||
|
||||
# Automatically generated.
|
||||
#
|
||||
# self.next_sent_route
|
||||
# self.prev_sent_route
|
||||
|
||||
@property
|
||||
def sent_peer(self):
|
||||
return self._sent_peer
|
||||
397
ryu/services/protocols/bgp/net_ctrl.py
Normal file
397
ryu/services/protocols/bgp/net_ctrl.py
Normal file
@ -0,0 +1,397 @@
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Network Controller interface to BGP.
|
||||
|
||||
Network controller w.r.t BGPS for APGW Automation project is named as APGW
|
||||
Agent and Route Server.
|
||||
"""
|
||||
import logging
|
||||
import socket
|
||||
import traceback
|
||||
|
||||
from ryu.services.protocols.bgp import api
|
||||
from ryu.services.protocols.bgp.api.base import ApiException
|
||||
from ryu.services.protocols.bgp.api.base import NEXT_HOP
|
||||
from ryu.services.protocols.bgp.api.base import ORIGIN_RD
|
||||
from ryu.services.protocols.bgp.api.base import PREFIX
|
||||
from ryu.services.protocols.bgp.api.base import ROUTE_DISTINGUISHER
|
||||
from ryu.services.protocols.bgp.api.base import VPN_LABEL
|
||||
from ryu.services.protocols.bgp.base import Activity
|
||||
from ryu.services.protocols.bgp.base import add_bgp_error_metadata
|
||||
from ryu.services.protocols.bgp.base import BGPSException
|
||||
from ryu.services.protocols.bgp.base import FlexinetPeer
|
||||
from ryu.services.protocols.bgp.base import NET_CTRL_ERROR_CODE
|
||||
from ryu.services.protocols.bgp.constants import VRF_TABLE
|
||||
from ryu.services.protocols.bgp.rtconf.vrfs import VRF_RF
|
||||
from ryu.services.protocols.bgp.rtconf.vrfs import VrfConf
|
||||
from ryu.services.protocols.bgp.utils.validation import is_valid_ipv4
|
||||
|
||||
|
||||
# Logger instance for this module.
|
||||
LOG = logging.getLogger('bgpspeaker.net_ctrl')
|
||||
|
||||
# Network controller service socket constants.
|
||||
NC_RPC_BIND_IP = 'apgw_rpc_bind_ip'
|
||||
NC_RPC_BIND_PORT = 'apgw_rpc_bind_port'
|
||||
|
||||
# Notification symbols
|
||||
NOTF_ADD_REMOTE_PREFX = 'prefix.add_remote'
|
||||
NOTF_DELETE_REMOTE_PREFX = 'prefix.delete_remote'
|
||||
NOTF_ADD_LOCAL_PREFX = 'prefix.add_local'
|
||||
NOTF_DELETE_LOCAL_PREFX = 'prefix.delete_local'
|
||||
NOTF_LOG = 'logging'
|
||||
|
||||
# MessagePackRPC message type constants
|
||||
RPC_MSG_REQUEST = 0
|
||||
RPC_MSG_RESPONSE = 1
|
||||
RPC_MSG_NOTIFY = 2
|
||||
|
||||
#
|
||||
# Indexes for various RPC message types.
|
||||
#
|
||||
RPC_IDX_MSG_TYP = 0
|
||||
RPC_IDX_MSG_ID = 1
|
||||
RPC_IDX_REQ_SYM = 2
|
||||
RPC_IDX_REQ_PARAM = 3
|
||||
RPC_IDX_RES_ERR = 2
|
||||
RPC_IDX_RES_RST = 3
|
||||
RPC_IDX_NTF_SYM = 1
|
||||
RPC_IDX_NTF_PARAM = 2
|
||||
|
||||
# RPC socket receive buffer size in bytes.
|
||||
RPC_SOCK_BUFF_SIZE = 4096
|
||||
|
||||
|
||||
@add_bgp_error_metadata(code=NET_CTRL_ERROR_CODE,
|
||||
sub_code=1,
|
||||
def_desc='Unknown Network controller exception')
|
||||
class NetworkControllerError(BGPSException):
|
||||
"""Common base class for exceptions related to RPC calls.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class RpcSession(Activity):
|
||||
"""Provides message-pack RPC abstraction for one session.
|
||||
|
||||
It contains message-pack packer, un-packer, message ID sequence
|
||||
and utilities that use these. It also cares about socket communication w/
|
||||
RPC peer.
|
||||
"""
|
||||
|
||||
def __init__(self, socket, outgoing_msg_sink_iter):
|
||||
super(RpcSession, self).__init__()
|
||||
import msgpack
|
||||
|
||||
self._packer = msgpack.Packer()
|
||||
self._unpacker = msgpack.Unpacker()
|
||||
self._next_msgid = 0
|
||||
self._socket = socket
|
||||
self._outgoing_msg_sink_iter = outgoing_msg_sink_iter
|
||||
|
||||
def stop(self):
|
||||
super(RpcSession, self).stop()
|
||||
LOG.critical(
|
||||
'RPC Session to %s stopped', str(self._socket.getpeername())
|
||||
)
|
||||
|
||||
def _run(self):
|
||||
# Process outgoing messages in new thread.
|
||||
green_out = self._spawn('net_ctrl._process_outgoing',
|
||||
self._process_outgoing_msg,
|
||||
self._outgoing_msg_sink_iter)
|
||||
# Process incoming messages in new thread.
|
||||
green_in = self._spawn('net_ctrl._process_incoming',
|
||||
self._process_incoming_msgs)
|
||||
LOG.critical(
|
||||
'RPC Session to %s started', str(self._socket.getpeername())
|
||||
)
|
||||
green_in.wait()
|
||||
green_out.wait()
|
||||
|
||||
def _next_msg_id(self):
|
||||
this_id = self._next_msgid
|
||||
self._next_msgid += 1
|
||||
return this_id
|
||||
|
||||
def create_request(self, method, params):
|
||||
msgid = self._next_msg_id()
|
||||
return self._packer.pack([RPC_MSG_REQUEST, msgid, method, params])
|
||||
|
||||
def create_error_response(self, msgid, error):
|
||||
if error is None:
|
||||
raise NetworkControllerError(desc='Creating error without body!')
|
||||
return self._packer.pack([RPC_MSG_RESPONSE, msgid, error, None])
|
||||
|
||||
def create_success_response(self, msgid, result):
|
||||
if result is None:
|
||||
raise NetworkControllerError(desc='Creating response without '
|
||||
'body!')
|
||||
return self._packer.pack([RPC_MSG_RESPONSE, msgid, None, result])
|
||||
|
||||
def create_notification(self, method, params):
|
||||
return self._packer.pack([RPC_MSG_NOTIFY, method, params])
|
||||
|
||||
def feed_and_get_messages(self, data):
|
||||
self._unpacker.feed(data)
|
||||
messages = []
|
||||
for msg in self._unpacker:
|
||||
messages.append(msg)
|
||||
return messages
|
||||
|
||||
def feed_and_get_first_message(self, data):
|
||||
self._unpacker.feed(data)
|
||||
for msg in self._unpacker:
|
||||
return msg
|
||||
|
||||
def send_notification(self, method, params):
|
||||
rpc_msg = self.create_notification(method, params)
|
||||
return self._sendall(rpc_msg)
|
||||
|
||||
def _process_incoming_msgs(self):
|
||||
LOG.debug('NetworkController started processing incoming messages')
|
||||
assert self._socket
|
||||
|
||||
while True:
|
||||
# Wait for request/response/notification from peer.
|
||||
msg_buff = self._recv()
|
||||
if len(msg_buff) == 0:
|
||||
LOG.info('Peer %r disconnected.' % self._socket)
|
||||
break
|
||||
messages = self.feed_and_get_messages(msg_buff)
|
||||
for msg in messages:
|
||||
if msg[0] == RPC_MSG_REQUEST:
|
||||
try:
|
||||
result = _handle_request(msg)
|
||||
_send_success_response(self, self._socket, msg, result)
|
||||
except BGPSException as e:
|
||||
_send_error_response(self, self._socket, msg,
|
||||
e.message)
|
||||
elif msg[0] == RPC_MSG_RESPONSE:
|
||||
_handle_response(msg)
|
||||
elif msg[0] == RPC_MSG_NOTIFY:
|
||||
_handle_notification(msg)
|
||||
else:
|
||||
LOG.error('Invalid message type: %r' % msg)
|
||||
self.pause(0)
|
||||
|
||||
def _process_outgoing_msg(self, sink_iter):
|
||||
"""For every message we construct a corresponding RPC message to be
|
||||
sent over the given socket inside given RPC session.
|
||||
|
||||
This function should be launched in a new green thread as
|
||||
it loops forever.
|
||||
"""
|
||||
LOG.debug('NetworkController processing outgoing request list.')
|
||||
# TODO(Team): handle un-expected exception breaking the loop in
|
||||
# graceful manner. Discuss this with other component developers.
|
||||
# TODO(PH): We should try not to sent routes from bgp peer that is not
|
||||
# in established state.
|
||||
from ryu.services.protocols.bgp.speaker.model import \
|
||||
FlexinetOutgoingRoute
|
||||
while True:
|
||||
# sink iter is Sink instance and next is blocking so this isn't
|
||||
# active wait.
|
||||
for outgoing_msg in sink_iter:
|
||||
if isinstance(outgoing_msg, FlexinetOutgoingRoute):
|
||||
rpc_msg = _create_prefix_notif(outgoing_msg, self)
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
'Do not handle out going message'
|
||||
' of type %s' %
|
||||
outgoing_msg.__class__)
|
||||
if rpc_msg:
|
||||
self._sendall(rpc_msg)
|
||||
self.pause(0)
|
||||
|
||||
def _recv(self):
|
||||
return self._sock_wrap(self._socket.recv)(RPC_SOCK_BUFF_SIZE)
|
||||
|
||||
def _sendall(self, msg):
|
||||
return self._sock_wrap(self._socket.sendall)(msg)
|
||||
|
||||
def _sock_wrap(self, func):
|
||||
def wrapper(*args, **kwargs):
|
||||
try:
|
||||
ret = func(*args, **kwargs)
|
||||
except socket.error:
|
||||
LOG.error(traceback.format_exc())
|
||||
self._socket_error()
|
||||
return
|
||||
return ret
|
||||
|
||||
return wrapper
|
||||
|
||||
def _socket_error(self):
|
||||
if self.started:
|
||||
self.stop()
|
||||
|
||||
|
||||
def _create_prefix_notif(outgoing_msg, rpc_session):
|
||||
"""Constructs prefix notification with data from given outgoing message.
|
||||
|
||||
Given RPC session is used to create RPC notification message.
|
||||
"""
|
||||
assert(outgoing_msg)
|
||||
path = outgoing_msg.path
|
||||
assert(path)
|
||||
vpn_nlri = path.nlri
|
||||
|
||||
rpc_msg = None
|
||||
assert path.source is not None
|
||||
if path.source != VRF_TABLE:
|
||||
# Extract relevant info for update-add/update-delete.
|
||||
params = [{ROUTE_DISTINGUISHER: outgoing_msg.route_disc,
|
||||
PREFIX: vpn_nlri.prefix,
|
||||
NEXT_HOP: path.nexthop,
|
||||
VPN_LABEL: path.label_list[0],
|
||||
VRF_RF: VrfConf.rf_2_vrf_rf(path.route_family)}]
|
||||
if not path.is_withdraw:
|
||||
# Create notification to NetworkController.
|
||||
rpc_msg = rpc_session.create_notification(NOTF_ADD_REMOTE_PREFX,
|
||||
params)
|
||||
else:
|
||||
# Create update-delete request to NetworkController.`
|
||||
rpc_msg = rpc_session.create_notification(NOTF_DELETE_REMOTE_PREFX,
|
||||
params)
|
||||
else:
|
||||
# Extract relevant info for update-add/update-delete.
|
||||
params = [{ROUTE_DISTINGUISHER: outgoing_msg.route_disc,
|
||||
PREFIX: vpn_nlri.prefix,
|
||||
NEXT_HOP: path.nexthop,
|
||||
VRF_RF: VrfConf.rf_2_vrf_rf(path.route_family),
|
||||
ORIGIN_RD: path.origin_rd}]
|
||||
if not path.is_withdraw:
|
||||
# Create notification to NetworkController.
|
||||
rpc_msg = rpc_session.create_notification(NOTF_ADD_LOCAL_PREFX,
|
||||
params)
|
||||
else:
|
||||
# Create update-delete request to NetworkController.`
|
||||
rpc_msg = rpc_session.create_notification(NOTF_DELETE_LOCAL_PREFX,
|
||||
params)
|
||||
|
||||
return rpc_msg
|
||||
|
||||
|
||||
def _validate_rpc_ip(rpc_server_ip):
|
||||
"""Validates given ip for use as rpc host bind address.
|
||||
"""
|
||||
if not is_valid_ipv4(rpc_server_ip):
|
||||
raise NetworkControllerError(desc='Invalid rpc ip address.')
|
||||
return rpc_server_ip
|
||||
|
||||
|
||||
def _validate_rpc_port(port):
|
||||
"""Validates give port for use as rpc server port.
|
||||
"""
|
||||
if not port:
|
||||
raise NetworkControllerError(desc='Invalid rpc port number.')
|
||||
if not isinstance(port, (int, long)) and isinstance(port, str):
|
||||
port = int(port)
|
||||
|
||||
if port <= 0:
|
||||
raise NetworkControllerError(desc='Invalid rpc port number %s' % port)
|
||||
return port
|
||||
|
||||
|
||||
class _NetworkController(FlexinetPeer, Activity):
|
||||
"""Network controller peer.
|
||||
|
||||
Provides MessagePackRPC interface for flexinet peers like Network
|
||||
controller to peer and have RPC session with BGPS process. This RPC
|
||||
interface provides access to BGPS API.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
FlexinetPeer.__init__(self)
|
||||
Activity.__init__(self, name='NETWORK_CONTROLLER')
|
||||
# Outstanding requests, i.e. requests for which we are yet to receive
|
||||
# response from peer. We currently do not have any requests going out.
|
||||
self._outstanding_reqs = {}
|
||||
self._rpc_session = None
|
||||
|
||||
def _run(self, *args, **kwargs):
|
||||
"""Runs RPC server.
|
||||
|
||||
Wait for peer to connect and start rpc session with it.
|
||||
For every connection we start and new rpc session.
|
||||
"""
|
||||
apgw_rpc_bind_ip = _validate_rpc_ip(kwargs.pop(NC_RPC_BIND_IP))
|
||||
apgw_rpc_bind_port = _validate_rpc_port(kwargs.pop(NC_RPC_BIND_PORT))
|
||||
|
||||
sock_addr = (apgw_rpc_bind_ip, apgw_rpc_bind_port)
|
||||
LOG.debug('NetworkController started listening for connections...')
|
||||
|
||||
server_thread = self._listen_tcp(sock_addr, self._start_rpc_session)
|
||||
self.pause(0)
|
||||
server_thread.wait()
|
||||
|
||||
def _start_rpc_session(self, socket):
|
||||
"""Starts a new RPC session with given connection.
|
||||
"""
|
||||
if self._rpc_session and self._rpc_session.started:
|
||||
self._rpc_session.stop()
|
||||
|
||||
self._rpc_session = RpcSession(socket, self)
|
||||
self._rpc_session.start()
|
||||
|
||||
def send_rpc_notification(self, method, params):
|
||||
if (self.started and self._rpc_session is not None and
|
||||
self._rpc_session.started):
|
||||
return self._rpc_session.send_notification(method, params)
|
||||
|
||||
|
||||
def _handle_response(response):
|
||||
raise NotImplementedError('BGPS is not making any request hence should not'
|
||||
' get any response. Response: %s' % response)
|
||||
|
||||
|
||||
def _handle_notification(notification):
|
||||
LOG.debug('Notification from NetworkController<<: %s %s' %
|
||||
(notification[RPC_IDX_NTF_SYM], notification[RPC_IDX_NTF_PARAM]))
|
||||
operation, params = notification[1], notification[2]
|
||||
return api.base.call(operation, **params[0])
|
||||
|
||||
|
||||
def _handle_request(request):
|
||||
LOG.debug('Request from NetworkController<<: %s %s' %
|
||||
(request[RPC_IDX_REQ_SYM], request[RPC_IDX_REQ_PARAM]))
|
||||
operation, params = request[2], request[3]
|
||||
kwargs = {}
|
||||
if len(params) > 0:
|
||||
kwargs = params[0]
|
||||
try:
|
||||
return api.base.call(operation, **kwargs)
|
||||
except TypeError:
|
||||
LOG.error(traceback.format_exc())
|
||||
raise ApiException(desc='Invalid type for RPC parameter.')
|
||||
|
||||
|
||||
def _send_success_response(rpc_session, socket, request, result):
|
||||
response = rpc_session.create_success_response(request[RPC_IDX_MSG_ID],
|
||||
result)
|
||||
socket.sendall(response)
|
||||
|
||||
|
||||
def _send_error_response(rpc_session, socket, request, emsg):
|
||||
response = rpc_session.create_error_response(request[RPC_IDX_MSG_ID],
|
||||
str(emsg))
|
||||
socket.sendall(response)
|
||||
|
||||
|
||||
# Network controller singleton
|
||||
NET_CONTROLLER = _NetworkController()
|
||||
269
ryu/services/protocols/bgp/operator/command.py
Normal file
269
ryu/services/protocols/bgp/operator/command.py
Normal file
@ -0,0 +1,269 @@
|
||||
from collections import namedtuple
|
||||
import json
|
||||
import logging
|
||||
import pprint
|
||||
import re
|
||||
|
||||
(STATUS_OK, STATUS_ERROR) = xrange(2)
|
||||
|
||||
CommandsResponse = namedtuple('CommandsResponse', ['status', 'value'])
|
||||
|
||||
LOG = logging.getLogger('bgpspeaker.operator.command')
|
||||
|
||||
|
||||
def default_help_formatter(quick_helps):
|
||||
"""Apply default formatting for help messages
|
||||
|
||||
:param quick_helps: list of tuples containing help info
|
||||
"""
|
||||
ret = ''
|
||||
for line in quick_helps:
|
||||
cmd_path, param_hlp, cmd_hlp = line
|
||||
ret += ' '.join(cmd_path) + ' '
|
||||
if param_hlp:
|
||||
ret += param_hlp + ' '
|
||||
ret += '- ' + cmd_hlp + '\n'
|
||||
return ret
|
||||
|
||||
|
||||
class Command(object):
|
||||
"""Command class is used as a node in tree of commands.
|
||||
|
||||
Each command can do some action or have some sub-commands, just like in IOS
|
||||
Command with it's sub-commands form tree.
|
||||
Each command can have one or more parameters. Parameters have to be
|
||||
distinguishable from sub-commands.
|
||||
One can inject dependency into command Cmd(api=my_object).
|
||||
This dependency will be injected to every sub-command. And can be used
|
||||
to interact with model/data etc.
|
||||
Example of path in command tree `show count all`.
|
||||
"""
|
||||
|
||||
help_msg = ''
|
||||
param_help_msg = None
|
||||
command = ''
|
||||
cli_resp_line_template = '{0}: {1}\n\n'
|
||||
|
||||
def __init__(self, api=None, parent=None,
|
||||
help_formatter=default_help_formatter,
|
||||
resp_formatter_name='cli'):
|
||||
""":param api: object which is saved as self.api
|
||||
and re-injected to every sub-command. You can use it to
|
||||
manipulate your model from inside Commands'
|
||||
:param parent: parent command instance.
|
||||
:param help_formatter: function used to format
|
||||
output of '?'command. Is re-injected to every
|
||||
sub-command as well.
|
||||
:param resp_formatter_name: used to select function to format
|
||||
output of _action. cli_resp_formatter and json_resp_formatter
|
||||
are defined by default, but you can define your own formatters.
|
||||
If you use custom formatter(not cli nor json) remember to
|
||||
implement it for every sub-command.
|
||||
"""
|
||||
|
||||
self.resp_formatter_name = resp_formatter_name
|
||||
|
||||
if hasattr(self, resp_formatter_name + '_resp_formatter'):
|
||||
self.resp_formatter = \
|
||||
getattr(self, resp_formatter_name + '_resp_formatter')
|
||||
else:
|
||||
self.resp_formatter = self.cli_resp_formatter
|
||||
|
||||
self.api = api
|
||||
self.parent_cmd = parent
|
||||
self.help_formatter = help_formatter
|
||||
if not hasattr(self, 'subcommands'):
|
||||
self.subcommands = {}
|
||||
|
||||
def __call__(self, params):
|
||||
"""You run command by calling it.
|
||||
|
||||
:param params: As params you give list of subcommand names
|
||||
and params to final subcommand. Kind of like in
|
||||
cisco ios cli, ie. show int eth1 / 1, where show is command,
|
||||
int subcommand and eth1 / 1 is param for subcommand.
|
||||
:return: returns tuple of CommandsResponse and class of
|
||||
sub - command on which _action was called. (last sub - command)
|
||||
CommandsResposne.status is action status,
|
||||
and CommandsResponse.value is formatted response.
|
||||
"""
|
||||
if len(params) == 0:
|
||||
return self._action_wrapper([])
|
||||
|
||||
first_param = params[0]
|
||||
|
||||
if first_param == '?':
|
||||
return self.question_mark()
|
||||
|
||||
if first_param in self.subcommands:
|
||||
return self._instantiate_subcommand(first_param)(params[1:])
|
||||
|
||||
return self._action_wrapper(params)
|
||||
|
||||
@classmethod
|
||||
def cli_resp_formatter(cls, resp):
|
||||
"""Override this method to provide custom formatting of cli response.
|
||||
"""
|
||||
if not resp.value:
|
||||
return ''
|
||||
|
||||
if resp.status == STATUS_OK:
|
||||
|
||||
if type(resp.value) in (str, bool, int, float, unicode):
|
||||
return str(resp.value)
|
||||
|
||||
ret = ''
|
||||
val = resp.value
|
||||
if not isinstance(val, list):
|
||||
val = [val]
|
||||
for line in val:
|
||||
for k, v in line.iteritems():
|
||||
if isinstance(v, dict):
|
||||
ret += cls.cli_resp_line_template.format(
|
||||
k, '\n' + pprint.pformat(v)
|
||||
)
|
||||
else:
|
||||
ret += cls.cli_resp_line_template.format(k, v)
|
||||
return ret
|
||||
else:
|
||||
return "Error: {0}".format(resp.value)
|
||||
|
||||
@classmethod
|
||||
def json_resp_formatter(cls, resp):
|
||||
"""Override this method to provide custom formatting of json response.
|
||||
"""
|
||||
return json.dumps(resp.value)
|
||||
|
||||
@classmethod
|
||||
def dict_resp_formatter(cls, resp):
|
||||
return resp.value
|
||||
|
||||
def _action_wrapper(self, params):
|
||||
filter_params = []
|
||||
if '|' in params:
|
||||
ind = params.index('|')
|
||||
new_params = params[:ind]
|
||||
filter_params = params[ind:]
|
||||
params = new_params
|
||||
|
||||
action_resp = self.action(params)
|
||||
if len(filter_params) > 1:
|
||||
# we don't pass '|' around so filter_params[1:]
|
||||
action_resp = self.filter_resp(action_resp, filter_params[1:])
|
||||
action_resp = CommandsResponse(
|
||||
action_resp.status,
|
||||
self.resp_formatter(action_resp)
|
||||
)
|
||||
return action_resp, self.__class__
|
||||
|
||||
def action(self, params):
|
||||
"""Override this method to define what command should do.
|
||||
|
||||
:param params: list of text parameters applied to this command.
|
||||
:return: returns CommandsResponse instance.
|
||||
CommandsResposne.status can be STATUS_OK or STATUS_ERROR
|
||||
CommandsResponse.value should be dict or str
|
||||
"""
|
||||
return CommandsResponse(STATUS_ERROR, 'Not implemented')
|
||||
|
||||
def filter_resp(self, action_resp, filter_params):
|
||||
"""Filter response of action. Used to make printed results more
|
||||
specific
|
||||
|
||||
:param action_resp: named tuple (CommandsResponse)
|
||||
containing response from action.
|
||||
:param filter_params: params used after '|' specific for given filter
|
||||
:return: filtered response.
|
||||
"""
|
||||
if action_resp.status == STATUS_OK:
|
||||
try:
|
||||
return CommandsResponse(
|
||||
STATUS_OK,
|
||||
TextFilter.filter(action_resp.value, filter_params)
|
||||
)
|
||||
except FilterError as e:
|
||||
return CommandsResponse(STATUS_ERROR, str(e))
|
||||
else:
|
||||
return action_resp
|
||||
|
||||
def question_mark(self):
|
||||
"""Shows help for this command and it's sub-commands.
|
||||
"""
|
||||
ret = []
|
||||
if self.param_help_msg or len(self.subcommands) == 0:
|
||||
ret.append(self._quick_help())
|
||||
|
||||
if len(self.subcommands) > 0:
|
||||
for k, _ in sorted(self.subcommands.iteritems()):
|
||||
command_path, param_help, cmd_help = \
|
||||
self._instantiate_subcommand(k)._quick_help(nested=True)
|
||||
if command_path or param_help or cmd_help:
|
||||
ret.append((command_path, param_help, cmd_help))
|
||||
|
||||
return (
|
||||
CommandsResponse(STATUS_OK, self.help_formatter(ret)),
|
||||
self.__class__
|
||||
)
|
||||
|
||||
def _quick_help(self, nested=False):
|
||||
""":param nested: True if help is requested directly for this command
|
||||
and False when help is requested for a list of possible
|
||||
completions.
|
||||
"""
|
||||
if nested:
|
||||
return self.command_path(), None, self.help_msg
|
||||
else:
|
||||
return self.command_path(), self.param_help_msg, self.help_msg
|
||||
|
||||
def command_path(self):
|
||||
if self.parent_cmd:
|
||||
return self.parent_cmd.command_path() + [self.command]
|
||||
else:
|
||||
return [self.command]
|
||||
|
||||
def _instantiate_subcommand(self, key):
|
||||
return self.subcommands[key](
|
||||
api=self.api,
|
||||
parent=self,
|
||||
help_formatter=self.help_formatter,
|
||||
resp_formatter_name=self.resp_formatter_name
|
||||
)
|
||||
|
||||
|
||||
class TextFilter(object):
|
||||
|
||||
@classmethod
|
||||
def filter(cls, action_resp_value, filter_params):
|
||||
try:
|
||||
action, expected_value = filter_params
|
||||
except ValueError:
|
||||
raise FilterError('Wrong number of filter parameters')
|
||||
if action == 'regexp':
|
||||
|
||||
if isinstance(action_resp_value, list):
|
||||
resp = list(action_resp_value)
|
||||
iterator = enumerate(action_resp_value)
|
||||
else:
|
||||
resp = dict(action_resp_value)
|
||||
iterator = action_resp_value.iteritems()
|
||||
|
||||
remove = []
|
||||
|
||||
for key, value in iterator:
|
||||
if not re.search(expected_value, str(value)):
|
||||
remove.append(key)
|
||||
|
||||
if isinstance(resp, list):
|
||||
resp = [resp[key] for key, value in enumerate(resp)
|
||||
if key not in remove]
|
||||
else:
|
||||
resp = {key: value for key, value in resp.iteritems()
|
||||
if key not in remove}
|
||||
|
||||
return resp
|
||||
else:
|
||||
raise FilterError('Unknown filter')
|
||||
|
||||
|
||||
class FilterError(Exception):
|
||||
pass
|
||||
54
ryu/services/protocols/bgp/operator/commands/clear.py
Normal file
54
ryu/services/protocols/bgp/operator/commands/clear.py
Normal file
@ -0,0 +1,54 @@
|
||||
from ryu.services.protocols.bgp.operator.command import Command
|
||||
from ryu.services.protocols.bgp.operator.command import CommandsResponse
|
||||
from ryu.services.protocols.bgp.operator.command import STATUS_OK
|
||||
from ryu.services.protocols.bgp.operator.commands.responses import \
|
||||
WrongParamResp
|
||||
|
||||
|
||||
class BGPCmd(Command):
|
||||
help_msg = ('reset bgp connections, no afi/safi is '
|
||||
'treated as - all supported address-families')
|
||||
param_help_msg = '<peer_ip> [<afi> <safi>]'
|
||||
command = 'bgp'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(BGPCmd, self).__init__(*args, **kwargs)
|
||||
|
||||
self.subcommands = {'all': self.All}
|
||||
|
||||
def action(self, params):
|
||||
if len(params) == 0:
|
||||
return WrongParamResp()
|
||||
peer = afi = safi = None
|
||||
try:
|
||||
peer = params[0]
|
||||
afi = params[1]
|
||||
safi = params[2]
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
self.api.route_refresh(peer, afi, safi)
|
||||
return CommandsResponse(STATUS_OK, '')
|
||||
|
||||
class All(Command):
|
||||
help_msg = 'reset all connections'
|
||||
param_help_msg = '[<afi=> <safi=>]'
|
||||
command = 'all'
|
||||
|
||||
def action(self, params):
|
||||
peer = afi = safi = None
|
||||
try:
|
||||
afi = params[0]
|
||||
safi = params[1]
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
self.api.route_refresh(peer, afi, safi)
|
||||
return CommandsResponse(STATUS_OK, '')
|
||||
|
||||
|
||||
class ClearCmd(Command):
|
||||
help_msg = 'allows to reset BGP connections'
|
||||
command = 'clear'
|
||||
|
||||
subcommands = {'bgp': BGPCmd}
|
||||
34
ryu/services/protocols/bgp/operator/commands/responses.py
Normal file
34
ryu/services/protocols/bgp/operator/commands/responses.py
Normal file
@ -0,0 +1,34 @@
|
||||
# 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 classes related to incorrect parameters.
|
||||
"""
|
||||
from ryu.services.protocols.bgp.operator.command import CommandsResponse
|
||||
from ryu.services.protocols.bgp.operator.command import STATUS_ERROR
|
||||
from ryu.services.protocols.bgp.operator.internal_api import WrongParamError
|
||||
|
||||
|
||||
class WrongParamResp(object):
|
||||
def __new__(cls, e=None):
|
||||
return cls.wrong_param_resp_factory(e)
|
||||
|
||||
@staticmethod
|
||||
def wrong_param_resp_factory(e=None):
|
||||
if not e:
|
||||
e = WrongParamError()
|
||||
desc = 'wrong parameters: %s' % str(e)
|
||||
|
||||
return CommandsResponse(STATUS_ERROR, desc)
|
||||
11
ryu/services/protocols/bgp/operator/commands/root.py
Normal file
11
ryu/services/protocols/bgp/operator/commands/root.py
Normal file
@ -0,0 +1,11 @@
|
||||
from ryu.services.protocols.bgp.operator.command import Command
|
||||
from ryu.services.protocols.bgp.operator.commands.clear import ClearCmd
|
||||
from ryu.services.protocols.bgp.operator.commands.set import SetCmd
|
||||
from ryu.services.protocols.bgp.operator.commands.show import ShowCmd
|
||||
|
||||
|
||||
class RootCmd(Command):
|
||||
subcommands = {
|
||||
'show': ShowCmd,
|
||||
'set': SetCmd,
|
||||
'clear': ClearCmd}
|
||||
65
ryu/services/protocols/bgp/operator/commands/set.py
Normal file
65
ryu/services/protocols/bgp/operator/commands/set.py
Normal file
@ -0,0 +1,65 @@
|
||||
import logging
|
||||
|
||||
from ryu.services.protocols.bgp.operator.command import Command
|
||||
from ryu.services.protocols.bgp.operator.command import CommandsResponse
|
||||
from ryu.services.protocols.bgp.operator.command import STATUS_OK
|
||||
from ryu.services.protocols.bgp.operator.commands.responses import \
|
||||
WrongParamResp
|
||||
|
||||
|
||||
class LoggingCmd(Command):
|
||||
command = 'logging'
|
||||
help_msg = 'turn on/off logging at current level'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(LoggingCmd, self).__init__(*args, **kwargs)
|
||||
self.subcommands = {
|
||||
'on': self.On,
|
||||
'off': self.Off,
|
||||
'level': self.Level
|
||||
}
|
||||
|
||||
class On(Command):
|
||||
command = 'on'
|
||||
help_msg = 'turn-on the logging at the current level'
|
||||
|
||||
def action(self, params):
|
||||
logging.getLogger('bgpspeaker').addHandler(self.api.log_handler)
|
||||
return CommandsResponse(STATUS_OK, True)
|
||||
|
||||
class Off(Command):
|
||||
command = 'off'
|
||||
help_msg = 'turn-off the logging'
|
||||
|
||||
def action(self, params):
|
||||
logging.getLogger('bgpspeaker').removeHandler(self.api.log_handler)
|
||||
return CommandsResponse(STATUS_OK, True)
|
||||
|
||||
class Level(Command):
|
||||
command = 'level'
|
||||
help_msg = 'set logging level'
|
||||
param_help_msg = '[debug/info/error]'
|
||||
|
||||
def action(self, params):
|
||||
lvls = {
|
||||
'debug': logging.DEBUG,
|
||||
'error': logging.ERROR,
|
||||
'info': logging.INFO
|
||||
}
|
||||
if len(params) == 1 and params[0] in lvls:
|
||||
self.api.log_handler.setLevel(
|
||||
lvls.get(params[0], logging.ERROR)
|
||||
)
|
||||
return CommandsResponse(STATUS_OK, True)
|
||||
else:
|
||||
return WrongParamResp()
|
||||
|
||||
|
||||
class SetCmd(Command):
|
||||
help_msg = 'allows to set runtime settings'
|
||||
command = 'set'
|
||||
|
||||
subcommands = {'logging': LoggingCmd}
|
||||
|
||||
def action(self, params):
|
||||
return CommandsResponse(STATUS_OK, True)
|
||||
@ -0,0 +1,56 @@
|
||||
from ryu.services.protocols.bgp.operator.command import Command
|
||||
from ryu.services.protocols.bgp.operator.command import CommandsResponse
|
||||
from ryu.services.protocols.bgp.operator.command import STATUS_OK
|
||||
from ryu.services.protocols.bgp.operator.commands.show import count
|
||||
from ryu.services.protocols.bgp.operator.commands.show import importmap
|
||||
from ryu.services.protocols.bgp.operator.commands.show import memory
|
||||
from ryu.services.protocols.bgp.operator.commands.show import neighbor
|
||||
from ryu.services.protocols.bgp.operator.commands.show import rib
|
||||
from ryu.services.protocols.bgp.operator.commands.show import vrf
|
||||
|
||||
|
||||
class ShowCmd(Command):
|
||||
help_msg = 'shows runtime state information'
|
||||
command = 'show'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ShowCmd, self).__init__(*args, **kwargs)
|
||||
self.subcommands = {
|
||||
'count': self.Count,
|
||||
'logging': self.Logging,
|
||||
'rib': self.Rib,
|
||||
'vrf': self.Vrf,
|
||||
'memory': self.Memory,
|
||||
'neighbor': self.Neighbor,
|
||||
'importmap': self.Importmap
|
||||
}
|
||||
|
||||
def action(self, params):
|
||||
return CommandsResponse(STATUS_OK, None)
|
||||
|
||||
class Count(count.Count):
|
||||
pass
|
||||
|
||||
class Rib(rib.Rib):
|
||||
pass
|
||||
|
||||
class Vrf(vrf.Vrf):
|
||||
pass
|
||||
|
||||
class Importmap(importmap.Importmap):
|
||||
pass
|
||||
|
||||
class Memory(memory.Memory):
|
||||
pass
|
||||
|
||||
class Neighbor(neighbor.Neighbor):
|
||||
pass
|
||||
|
||||
class Logging(Command):
|
||||
command = 'logging'
|
||||
help_msg = 'shows if logging is on/off and current logging level.'
|
||||
|
||||
def action(self, params):
|
||||
ret = {'logging': self.api.check_logging(),
|
||||
'level': self.api.check_logging_level()}
|
||||
return CommandsResponse(STATUS_OK, ret)
|
||||
53
ryu/services/protocols/bgp/operator/commands/show/count.py
Normal file
53
ryu/services/protocols/bgp/operator/commands/show/count.py
Normal file
@ -0,0 +1,53 @@
|
||||
import logging
|
||||
|
||||
from ryu.services.protocols.bgp.operator.command import Command
|
||||
from ryu.services.protocols.bgp.operator.command import CommandsResponse
|
||||
from ryu.services.protocols.bgp.operator.command import STATUS_ERROR
|
||||
from ryu.services.protocols.bgp.operator.command import STATUS_OK
|
||||
from ryu.services.protocols.bgp.operator.commands.responses import \
|
||||
WrongParamResp
|
||||
|
||||
LOG = logging.getLogger('bgpspeaker.operator.commands.show.count')
|
||||
|
||||
|
||||
class Count(Command):
|
||||
help_msg = 'show counters'
|
||||
param_help_msg = '<vpn-name> <route-family>{ipv4, ipv6}'
|
||||
command = 'count'
|
||||
cli_resp_line_template = 'BGP route count for VPN {0} is {1}\n'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(Count, self).__init__(*args, **kwargs)
|
||||
self.subcommands = {
|
||||
'all': self.All
|
||||
}
|
||||
|
||||
def action(self, params):
|
||||
if len(params) < 1:
|
||||
return CommandsResponse(STATUS_ERROR, 'Not enough params')
|
||||
else:
|
||||
vrf_name = params[0]
|
||||
if len(params) == 2:
|
||||
vrf_rf = params[1]
|
||||
else:
|
||||
vrf_rf = 'ipv4'
|
||||
|
||||
from ryu.services.protocols.bgp.operator.internal_api import \
|
||||
WrongParamError
|
||||
try:
|
||||
return CommandsResponse(
|
||||
STATUS_OK,
|
||||
self.api.count_single_vrf_routes(vrf_name, vrf_rf)
|
||||
)
|
||||
except WrongParamError as e:
|
||||
return WrongParamResp(e)
|
||||
|
||||
class All(Command):
|
||||
help_msg = 'shows number of routes for all VRFs'
|
||||
command = 'all'
|
||||
cli_resp_line_template = 'BGP route count for VPN {0} is {1}\n'
|
||||
|
||||
def action(self, params):
|
||||
if len(params) > 0:
|
||||
return WrongParamResp()
|
||||
return CommandsResponse(STATUS_OK, self.api.count_all_vrf_routes())
|
||||
@ -0,0 +1,42 @@
|
||||
from ryu.services.protocols.bgp.operator.command import Command
|
||||
from ryu.services.protocols.bgp.operator.command import CommandsResponse
|
||||
from ryu.services.protocols.bgp.operator.command import STATUS_ERROR
|
||||
from ryu.services.protocols.bgp.operator.command import STATUS_OK
|
||||
from ryu.services.protocols.bgp.operator.commands.responses import \
|
||||
WrongParamResp
|
||||
|
||||
from ryu.services.protocols.bgp.operator.views.bgp import CoreServiceDetailView
|
||||
|
||||
|
||||
class Importmap(Command):
|
||||
help_msg = 'show importmaps'
|
||||
param_help_msg = 'all | <name>'
|
||||
command = 'importmap'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(Importmap, self).__init__(*args, **kwargs)
|
||||
|
||||
def action(self, params):
|
||||
if len(params) != 1:
|
||||
return WrongParamResp()
|
||||
|
||||
core_service = self.api.get_core_service()
|
||||
core_service_view = CoreServiceDetailView(core_service)
|
||||
importmap_manager = core_service_view.rel('importmap_manager')
|
||||
importmaps_view = importmap_manager.rel('importmaps')
|
||||
|
||||
importmap_name = params[0]
|
||||
if importmap_name == 'all':
|
||||
encoded = importmaps_view.encode()
|
||||
else:
|
||||
encoded = importmaps_view.encode().get(importmap_name)
|
||||
if encoded is None:
|
||||
return CommandsResponse(
|
||||
STATUS_ERROR,
|
||||
'Wrong importmap name.'
|
||||
)
|
||||
|
||||
return CommandsResponse(
|
||||
STATUS_OK,
|
||||
encoded
|
||||
)
|
||||
89
ryu/services/protocols/bgp/operator/commands/show/memory.py
Normal file
89
ryu/services/protocols/bgp/operator/commands/show/memory.py
Normal file
@ -0,0 +1,89 @@
|
||||
import gc
|
||||
import sys
|
||||
|
||||
from ryu.services.protocols.bgp.operator.command import Command
|
||||
from ryu.services.protocols.bgp.operator.command import CommandsResponse
|
||||
from ryu.services.protocols.bgp.operator.command import STATUS_ERROR
|
||||
from ryu.services.protocols.bgp.operator.command import STATUS_OK
|
||||
|
||||
|
||||
class Memory(Command):
|
||||
help_msg = 'show memory information'
|
||||
command = 'memory'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(Memory, self).__init__(*args, **kwargs)
|
||||
self.subcommands = {
|
||||
'summary': self.Summary}
|
||||
|
||||
class Summary(Command):
|
||||
help_msg = 'shows total memory used and how it is getting used'
|
||||
command = 'summary'
|
||||
|
||||
def action(self, params):
|
||||
count = {}
|
||||
size = {}
|
||||
total_size = 0
|
||||
unreachable = gc.collect()
|
||||
for obj in gc.get_objects():
|
||||
inst_name = type(obj).__name__
|
||||
c = count.get(inst_name, None)
|
||||
if not c:
|
||||
count[inst_name] = 0
|
||||
s = size.get(inst_name, None)
|
||||
if not s:
|
||||
size[inst_name] = 0
|
||||
|
||||
count[inst_name] += 1
|
||||
s = sys.getsizeof(obj)
|
||||
size[inst_name] += s
|
||||
total_size += s
|
||||
|
||||
# Total size in MB
|
||||
|
||||
total_size = total_size / 1000000
|
||||
ret = {
|
||||
'unreachable': unreachable,
|
||||
'total': total_size,
|
||||
'summary': []}
|
||||
|
||||
for class_name, s in size.items():
|
||||
# Calculate size in MB
|
||||
size_mb = s / 1000000
|
||||
# We are only interested in class which take-up more than a MB
|
||||
if size_mb > 0:
|
||||
ret['summary'].append(
|
||||
{
|
||||
'class': class_name,
|
||||
'instances': count.get(class_name, None),
|
||||
'size': size_mb
|
||||
}
|
||||
)
|
||||
|
||||
return CommandsResponse(STATUS_OK, ret)
|
||||
|
||||
@classmethod
|
||||
def cli_resp_formatter(cls, resp):
|
||||
if resp.status == STATUS_ERROR:
|
||||
return Command.cli_resp_formatter(resp)
|
||||
val = resp.value
|
||||
ret = 'Unreachable objects: {0}\n'.format(
|
||||
val.get('unreachable', None)
|
||||
)
|
||||
ret += 'Total memory used (MB): {0}\n'.format(
|
||||
val.get('total', None)
|
||||
)
|
||||
ret += 'Classes with instances that take-up more than one MB:\n'
|
||||
ret += '{0:<20s} {1:>16s} {2:>16s}\n'.format(
|
||||
'Class',
|
||||
'#Instance',
|
||||
'Size(MB)'
|
||||
)
|
||||
|
||||
for s in val.get('summary', []):
|
||||
ret += '{0:<20s} {1:>16d} {2:>16d}\n'.format(
|
||||
s.get('class', None), s.get('instances', None),
|
||||
s.get('size', None)
|
||||
)
|
||||
|
||||
return ret
|
||||
135
ryu/services/protocols/bgp/operator/commands/show/neighbor.py
Normal file
135
ryu/services/protocols/bgp/operator/commands/show/neighbor.py
Normal file
@ -0,0 +1,135 @@
|
||||
import logging
|
||||
import pprint
|
||||
|
||||
from ryu.services.protocols.bgp.operator.command import Command
|
||||
from ryu.services.protocols.bgp.operator.command import CommandsResponse
|
||||
from ryu.services.protocols.bgp.operator.command import STATUS_ERROR
|
||||
from ryu.services.protocols.bgp.operator.command import STATUS_OK
|
||||
from ryu.services.protocols.bgp.operator.commands.responses import \
|
||||
WrongParamResp
|
||||
from ryu.services.protocols.bgp.operator.views.bgp import CoreServiceDetailView
|
||||
from ryu.services.protocols.bgp.protocols.bgp.nlri import RF_IPv4_UC
|
||||
from ryu.services.protocols.bgp.protocols.bgp.nlri import RF_IPv4_VPN
|
||||
from ryu.services.protocols.bgp.protocols.bgp.nlri import RF_IPv6_UC
|
||||
from ryu.services.protocols.bgp.protocols.bgp.nlri import RF_IPv6_VPN
|
||||
|
||||
LOG = logging.getLogger('bgpspeaker.operator.commands.show.summary')
|
||||
|
||||
|
||||
class NeighborSummary(Command):
|
||||
help_msg = 'show summarized neighbor information'
|
||||
command = 'summary'
|
||||
|
||||
def action(self, params):
|
||||
requested_peers = []
|
||||
if len(params) > 0:
|
||||
requested_peers = [str(p) for p in params]
|
||||
|
||||
core_service = self.api.get_core_service()
|
||||
core_service_view = CoreServiceDetailView(core_service)
|
||||
peers_view = core_service_view.rel('peer_manager').rel('peers_summary')
|
||||
|
||||
def filter_requested(peer_id, peer_obj):
|
||||
return not requested_peers or peer_id in requested_peers
|
||||
|
||||
peers_view.apply_filter(filter_requested)
|
||||
ret = peers_view.encode()
|
||||
return CommandsResponse(STATUS_OK, ret)
|
||||
|
||||
|
||||
class SentRoutes(Command):
|
||||
help_msg = 'paths sent and not withdrawn to given peer'
|
||||
command = 'sent-routes'
|
||||
param_help_msg = '<ip_addr> <addr_family>{vpnv4, vpnv6, ipv4, ipv6, all}'
|
||||
|
||||
def action(self, params):
|
||||
if len(params) != 2:
|
||||
return WrongParamResp()
|
||||
ip_addr, addr_family = params
|
||||
|
||||
if addr_family == 'ipv4':
|
||||
rf = RF_IPv4_UC
|
||||
elif addr_family == 'ipv6':
|
||||
rf = RF_IPv6_UC
|
||||
elif addr_family == 'vpnv4':
|
||||
rf = RF_IPv4_VPN
|
||||
elif addr_family == 'vpnv6':
|
||||
rf = RF_IPv6_VPN
|
||||
elif addr_family == 'all':
|
||||
rf = None
|
||||
else:
|
||||
return WrongParamResp('wrong addr_family name')
|
||||
|
||||
ret = self._retrieve_paths(addr_family, rf, ip_addr).encode()
|
||||
ret = {
|
||||
path['nlri']['formatted_nlri']: path
|
||||
for path in ret
|
||||
}
|
||||
|
||||
return CommandsResponse(STATUS_OK, ret)
|
||||
|
||||
def _retrieve_paths(self, addr_family, route_family, ip_addr):
|
||||
global_tables_view = self._retrieve_global_tables_view(
|
||||
addr_family,
|
||||
route_family
|
||||
)
|
||||
sent = global_tables_view.c_rel('destinations').c_rel('sent_routes')
|
||||
sent.apply_filter(
|
||||
lambda route: route.sent_peer.ip_address == ip_addr
|
||||
)
|
||||
paths = sent.c_rel('path')
|
||||
paths.apply_filter(
|
||||
lambda path: not path.is_withdraw
|
||||
)
|
||||
return paths
|
||||
|
||||
def _retrieve_global_tables_view(self, addr_family, route_family):
|
||||
core_service = self.api.get_core_service()
|
||||
core_sv = CoreServiceDetailView(core_service)
|
||||
table_manager_view = core_sv.rel('table_manager')
|
||||
global_tables_view = table_manager_view.rel('global_tables')
|
||||
global_tables_view.apply_filter(
|
||||
lambda k, v: addr_family == 'all' or k == route_family
|
||||
)
|
||||
return global_tables_view
|
||||
|
||||
@classmethod
|
||||
def cli_resp_formatter(cls, resp):
|
||||
if resp.status == STATUS_ERROR:
|
||||
return Command.cli_resp_formatter(resp)
|
||||
|
||||
return '\n{0}'.format(pprint.pformat(resp.value))
|
||||
|
||||
|
||||
class ReceivedRoutes(SentRoutes):
|
||||
help_msg = 'paths received and not withdrawn by given peer'
|
||||
command = 'received-routes'
|
||||
|
||||
def _retrieve_paths(self, addr_family, route_family, ip_addr):
|
||||
global_tables_view = self._retrieve_global_tables_view(
|
||||
addr_family,
|
||||
route_family
|
||||
)
|
||||
paths = global_tables_view.c_rel(
|
||||
'destinations'
|
||||
).c_rel('known_path_list')
|
||||
|
||||
def path_filter(path):
|
||||
return path.source is not None and \
|
||||
path.source.ip_address == ip_addr and \
|
||||
not path.is_withdraw
|
||||
|
||||
paths.apply_filter(
|
||||
path_filter
|
||||
)
|
||||
return paths
|
||||
|
||||
|
||||
class Neighbor(Command):
|
||||
help_msg = 'show neighbor information'
|
||||
command = 'neighbor'
|
||||
subcommands = {
|
||||
'summary': NeighborSummary,
|
||||
'sent-routes': SentRoutes,
|
||||
'received-routes': ReceivedRoutes
|
||||
}
|
||||
65
ryu/services/protocols/bgp/operator/commands/show/rib.py
Normal file
65
ryu/services/protocols/bgp/operator/commands/show/rib.py
Normal file
@ -0,0 +1,65 @@
|
||||
from route_formatter_mixin import RouteFormatterMixin
|
||||
|
||||
from ryu.services.protocols.bgp.operator.command import Command
|
||||
from ryu.services.protocols.bgp.operator.command import CommandsResponse
|
||||
from ryu.services.protocols.bgp.operator.command import STATUS_ERROR
|
||||
from ryu.services.protocols.bgp.operator.command import STATUS_OK
|
||||
|
||||
from ryu.services.protocols.bgp.operator.commands.responses import \
|
||||
WrongParamResp
|
||||
|
||||
|
||||
class RibBase(Command, RouteFormatterMixin):
|
||||
supported_families = ['vpnv4', 'rtfilter', 'vpnv6']
|
||||
|
||||
|
||||
class Rib(RibBase):
|
||||
help_msg = 'show all routes for address family (only vpnv4 supported)'
|
||||
param_help_msg = '<address-family>'
|
||||
command = 'rib'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(Rib, self).__init__(*args, **kwargs)
|
||||
self.subcommands = {
|
||||
'all': self.All}
|
||||
|
||||
def action(self, params):
|
||||
if len(params) != 1 or params[0] not in self.supported_families:
|
||||
return WrongParamResp()
|
||||
from ryu.services.protocols.bgp.speaker.operator.internal_api \
|
||||
import WrongParamError
|
||||
try:
|
||||
return CommandsResponse(
|
||||
STATUS_OK,
|
||||
self.api.get_single_rib_routes(params[0])
|
||||
)
|
||||
except WrongParamError as e:
|
||||
return WrongParamResp(e)
|
||||
|
||||
@classmethod
|
||||
def cli_resp_formatter(cls, resp):
|
||||
if resp.status == STATUS_ERROR:
|
||||
return RibBase.cli_resp_formatter(resp)
|
||||
return cls._format_family_header() + cls._format_family(resp.value)
|
||||
|
||||
class All(RibBase):
|
||||
help_msg = 'show routes for all RIBs'
|
||||
command = 'all'
|
||||
|
||||
def action(self, params):
|
||||
if len(params) != 0:
|
||||
return WrongParamResp()
|
||||
ret = {}
|
||||
for family in self.supported_families:
|
||||
ret[family] = self.api.get_single_rib_routes(family)
|
||||
return CommandsResponse(STATUS_OK, ret)
|
||||
|
||||
@classmethod
|
||||
def cli_resp_formatter(cls, resp):
|
||||
if resp.status == STATUS_ERROR:
|
||||
return RibBase.cli_resp_formatter(resp)
|
||||
ret = cls._format_family_header()
|
||||
for family, data in resp.value.iteritems():
|
||||
ret += 'Family: {0}\n'.format(family)
|
||||
ret += cls._format_family(data)
|
||||
return ret
|
||||
@ -0,0 +1,43 @@
|
||||
import StringIO
|
||||
|
||||
|
||||
class RouteFormatterMixin(object):
|
||||
|
||||
@classmethod
|
||||
def _format_family_header(cls):
|
||||
ret = ''
|
||||
ret += ('Status codes: * valid, > best\n')
|
||||
ret += ' {0:<3s} {1:<32s} {2:<20s} {3:<10s} {4:<20s} {5:<}\n'.format(
|
||||
'', 'Network', 'Next Hop', 'Reason', 'Metric', 'Path')
|
||||
return ret
|
||||
|
||||
@classmethod
|
||||
def _format_family(cls, dest_list):
|
||||
msg = StringIO.StringIO()
|
||||
|
||||
def _append_path_info(buff, path, is_best, show_prefix):
|
||||
aspath = path.get('aspath')
|
||||
bpr = path.get('bpr')
|
||||
next_hop = path.get('nexthop')
|
||||
med = path.get('metric')
|
||||
# Construct path status string.
|
||||
path_status = '*'
|
||||
if is_best:
|
||||
path_status += '>'
|
||||
|
||||
# Check if we want to show prefix.
|
||||
prefix = ''
|
||||
if show_prefix:
|
||||
prefix = path.get('prefix')
|
||||
|
||||
# Append path info to String buffer.
|
||||
buff.write(' {0:<3s} {1:<32s} {2:<20s} {3:<20s} {4:<10s} {5:<}\n'.
|
||||
format(path_status, prefix, next_hop, bpr, str(med),
|
||||
', '.join(map(str, aspath))))
|
||||
|
||||
for dist in dest_list:
|
||||
for idx, path in enumerate(dist.get('paths')):
|
||||
_append_path_info(msg, path, path['best'], (idx == 0))
|
||||
ret = msg.getvalue()
|
||||
msg.close()
|
||||
return ret
|
||||
162
ryu/services/protocols/bgp/operator/commands/show/vrf.py
Normal file
162
ryu/services/protocols/bgp/operator/commands/show/vrf.py
Normal file
@ -0,0 +1,162 @@
|
||||
import logging
|
||||
import pprint
|
||||
|
||||
from ryu.services.protocols.bgp.operator.command import Command
|
||||
from ryu.services.protocols.bgp.operator.command import CommandsResponse
|
||||
from ryu.services.protocols.bgp.operator.command import STATUS_ERROR
|
||||
from ryu.services.protocols.bgp.operator.command import STATUS_OK
|
||||
from ryu.services.protocols.bgp.operator.commands.responses import \
|
||||
WrongParamResp
|
||||
from ryu.services.protocols.bgp.operator.views.conf import ConfDetailView
|
||||
from ryu.services.protocols.bgp.operator.views.conf import ConfDictView
|
||||
from route_formatter_mixin import RouteFormatterMixin
|
||||
|
||||
LOG = logging.getLogger('bgpspeaker.operator.commands.show.vrf')
|
||||
|
||||
|
||||
class Routes(Command, RouteFormatterMixin):
|
||||
help_msg = 'show routes present for vrf'
|
||||
param_help_msg = '<vpn-name> <route-family>(ipv4, ipv6)'
|
||||
command = 'routes'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(Routes, self).__init__(*args, **kwargs)
|
||||
self.subcommands = {
|
||||
'all': self.All,
|
||||
}
|
||||
|
||||
def action(self, params):
|
||||
if len(params) != 2:
|
||||
return WrongParamResp()
|
||||
vrf_name = params[0]
|
||||
vrf_rf = params[1]
|
||||
if vrf_rf not in ('ipv4', 'ipv6'):
|
||||
return WrongParamResp('route-family not one of (ipv4, ipv6)')
|
||||
|
||||
from ryu.services.protocols.bgp.speaker.operator.internal_api import \
|
||||
WrongParamError
|
||||
|
||||
try:
|
||||
return CommandsResponse(
|
||||
STATUS_OK,
|
||||
self.api.get_single_vrf_routes(vrf_name, vrf_rf)
|
||||
)
|
||||
except WrongParamError as e:
|
||||
return CommandsResponse(
|
||||
STATUS_ERROR,
|
||||
'wrong parameters: %s' % str(e)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def cli_resp_formatter(cls, resp):
|
||||
if resp.status == STATUS_ERROR:
|
||||
return super(Routes, cls).cli_resp_formatter(resp)
|
||||
return cls._format_family_header() + cls._format_family(resp.value)
|
||||
|
||||
class All(Command, RouteFormatterMixin):
|
||||
help_msg = 'show routes for all VRFs'
|
||||
command = 'all'
|
||||
|
||||
def action(self, params):
|
||||
if len(params) != 0:
|
||||
return WrongParamResp()
|
||||
return CommandsResponse(
|
||||
STATUS_OK,
|
||||
self.api.get_all_vrf_routes()
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def cli_resp_formatter(cls, resp):
|
||||
if resp.status == STATUS_ERROR:
|
||||
return Command.cli_resp_formatter(resp)
|
||||
ret = cls._format_family_header()
|
||||
for family, data in resp.value.iteritems():
|
||||
ret += 'VPN: {0}\n'.format(family)
|
||||
ret += cls._format_family(data)
|
||||
return ret
|
||||
|
||||
|
||||
class CountRoutesMixin(object):
|
||||
def _count_routes(self, vrf_name, vrf_rf):
|
||||
return len(self.api.get_single_vrf_routes(vrf_name, vrf_rf))
|
||||
|
||||
|
||||
class Summary(Command, CountRoutesMixin):
|
||||
help_msg = 'show configuration and summary of vrf'
|
||||
param_help_msg = '<rd> <route_family>| all'
|
||||
command = 'summary'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(Summary, self).__init__(*args, **kwargs)
|
||||
self.subcommands = {
|
||||
'all': self.All
|
||||
}
|
||||
|
||||
def action(self, params):
|
||||
if len(params) == 0:
|
||||
return WrongParamResp('Not enough params')
|
||||
|
||||
vrf_confs = self.api.get_vrfs_conf()
|
||||
if len(params) < 2:
|
||||
vrf_rf = 'ipv4'
|
||||
else:
|
||||
vrf_rf = params[1]
|
||||
|
||||
vrf_key = params[0], vrf_rf
|
||||
|
||||
if vrf_key in vrf_confs:
|
||||
view = ConfDetailView(vrf_confs[vrf_key])
|
||||
encoded = view.encode()
|
||||
encoded['routes_count'] = self._count_routes(params[0], vrf_rf)
|
||||
else:
|
||||
return WrongParamResp('No vrf matched by %s' % str(vrf_key))
|
||||
|
||||
return CommandsResponse(
|
||||
STATUS_OK,
|
||||
encoded
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def cli_resp_formatter(cls, resp):
|
||||
if resp.status == STATUS_ERROR:
|
||||
return Command.cli_resp_formatter(resp)
|
||||
return pprint.pformat(resp.value)
|
||||
|
||||
class All(Command, CountRoutesMixin):
|
||||
command = 'all'
|
||||
help_msg = 'shows all vrfs configurations and summary'
|
||||
|
||||
def action(self, params):
|
||||
vrf_confs = self.api.get_vrfs_conf()
|
||||
view = ConfDictView(vrf_confs)
|
||||
encoded = view.encode()
|
||||
for vrf_key, conf in encoded.iteritems():
|
||||
vrf_name, vrf_rf = vrf_key
|
||||
conf['routes_count'] = self._count_routes(
|
||||
vrf_name,
|
||||
vrf_rf
|
||||
)
|
||||
|
||||
encoded = {str(k): v for k, v in encoded.iteritems()}
|
||||
return CommandsResponse(
|
||||
STATUS_OK,
|
||||
encoded
|
||||
)
|
||||
|
||||
def _count_routes(self, vrf_name, vrf_rf):
|
||||
return len(self.api.get_single_vrf_routes(vrf_name, vrf_rf))
|
||||
|
||||
|
||||
class Vrf(Routes):
|
||||
"""Main node for vrf related commands. Acts also as Routes node (that's why
|
||||
it inherits from it) for legacy reasons.
|
||||
"""
|
||||
help_msg = 'vrf related commands subtree'
|
||||
command = 'vrf'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(Vrf, self).__init__(*args, **kwargs)
|
||||
self.subcommands.update({
|
||||
'routes': Routes,
|
||||
'summary': Summary
|
||||
})
|
||||
157
ryu/services/protocols/bgp/operator/internal_api.py
Normal file
157
ryu/services/protocols/bgp/operator/internal_api.py
Normal file
@ -0,0 +1,157 @@
|
||||
import logging
|
||||
import traceback
|
||||
|
||||
from ryu.services.protocols.bgp.protocols.bgp import nlri
|
||||
from ryu.services.protocols.bgp.protocols.bgp.pathattr import AsPath
|
||||
from ryu.services.protocols.bgp.protocols.bgp.pathattr import Med
|
||||
from ryu.services.protocols.bgp.base import add_bgp_error_metadata
|
||||
from ryu.services.protocols.bgp.base import BGPSException
|
||||
from ryu.services.protocols.bgp.base import SUPPORTED_GLOBAL_RF
|
||||
from ryu.services.protocols.bgp.core_manager import CORE_MANAGER
|
||||
|
||||
|
||||
LOG = logging.getLogger('bgpspeaker.operator.internal_api')
|
||||
|
||||
INTERNAL_API_ERROR = 100
|
||||
INTERNAL_API_SUB_ERROR = 101
|
||||
|
||||
|
||||
class InternalApi(object):
|
||||
|
||||
def __init__(self, log_handler=None):
|
||||
self.log_handler = log_handler
|
||||
|
||||
def count_all_vrf_routes(self):
|
||||
vrf_tables = self._get_vrf_tables()
|
||||
ret = {}
|
||||
for key in vrf_tables.keys():
|
||||
vrf_name, vrf_rf = key
|
||||
ret.update(self.count_single_vrf_routes(vrf_name, vrf_rf))
|
||||
return ret
|
||||
|
||||
def count_single_vrf_routes(self, vrf_name, vrf_rf):
|
||||
vrf = self._get_vrf_tables().get((vrf_name, vrf_rf))
|
||||
if vrf is None:
|
||||
raise WrongParamError('wrong vpn key %s' % str((vrf_name, vrf_rf)))
|
||||
vrf_name = vrf_name.encode('ascii', 'ignore')
|
||||
|
||||
route_count = \
|
||||
len([d for d in vrf.itervalues() if d.best_path])
|
||||
return {str((vrf_name, vrf_rf)): route_count}
|
||||
|
||||
def get_vrfs_conf(self):
|
||||
return CORE_MANAGER.vrfs_conf.vrfs_by_rd_rf_id
|
||||
|
||||
def get_all_vrf_routes(self):
|
||||
vrfs = self._get_vrf_tables()
|
||||
ret = {}
|
||||
for (vrf_id, vrf_rf), table in sorted(vrfs.iteritems()):
|
||||
ret[str((vrf_id, vrf_rf))] = self._get_single_vrf_routes(table)
|
||||
return ret
|
||||
|
||||
def get_single_vrf_routes(self, vrf_id, vrf_rf):
|
||||
vrf = self._get_vrf_table(vrf_id, vrf_rf)
|
||||
if not vrf:
|
||||
raise WrongParamError('wrong vpn name %s' % str((vrf_id, vrf_rf)))
|
||||
return [self._dst_to_dict(d) for d in vrf.itervalues()]
|
||||
|
||||
def _get_single_vrf_routes(self, vrf_table):
|
||||
return [self._dst_to_dict(d) for d in vrf_table.itervalues()]
|
||||
|
||||
def _get_vrf_table(self, vrf_name, vrf_rf):
|
||||
return CORE_MANAGER.get_core_service()\
|
||||
.table_manager.get_vrf_table(vrf_name, vrf_rf)
|
||||
|
||||
def _get_vrf_tables(self):
|
||||
return CORE_MANAGER.get_core_service().table_manager.get_vrf_tables()
|
||||
|
||||
def get_single_rib_routes(self, addr_family):
|
||||
rfs = {
|
||||
'vpnv4': nlri.get_rf(1, 128),
|
||||
'vpnv6': nlri.get_rf(2, 128),
|
||||
'rtfilter': nlri.get_rf(1, 132)
|
||||
}
|
||||
if addr_family not in rfs:
|
||||
raise WrongParamError('Unknown or unsupported family')
|
||||
|
||||
rf = rfs.get(addr_family)
|
||||
table_manager = self.get_core_service().table_manager
|
||||
gtable = table_manager.get_global_table_by_route_family(rf)
|
||||
if gtable is not None:
|
||||
return [self._dst_to_dict(dst)
|
||||
for dst in sorted(gtable.itervalues())]
|
||||
else:
|
||||
return []
|
||||
|
||||
def _dst_to_dict(self, dst):
|
||||
ret = {'paths': [],
|
||||
'prefix': dst.nlri.formatted_nlri_str}
|
||||
|
||||
def _path_to_dict(dst, path):
|
||||
aspath = path.get_pattr(AsPath.ATTR_NAME).path_seg_list
|
||||
if aspath is None or len(aspath) == 0:
|
||||
aspath = ''
|
||||
|
||||
nexthop = path.nexthop
|
||||
# Get the MED path attribute
|
||||
med = path.get_pattr(Med.ATTR_NAME)
|
||||
med = med.value if med else ''
|
||||
# Get best path reason
|
||||
bpr = dst.best_path_reason if path == dst.best_path else ''
|
||||
return {'best': (path == dst.best_path),
|
||||
'bpr': bpr,
|
||||
'prefix': path.nlri.formatted_nlri_str,
|
||||
'nexthop': nexthop,
|
||||
'metric': med,
|
||||
'aspath': aspath}
|
||||
|
||||
for path in dst.known_path_list:
|
||||
ret['paths'].append(_path_to_dict(dst, path))
|
||||
|
||||
return ret
|
||||
|
||||
def check_logging(self):
|
||||
if self.log_handler and self._has_log_handler(self.log_handler):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def check_logging_level(self):
|
||||
return logging.getLevelName(self.log_handler.level)
|
||||
|
||||
def _has_log_handler(self, log_handler):
|
||||
if log_handler in logging.getLogger('bgpspeaker').handlers:
|
||||
return True
|
||||
return False
|
||||
|
||||
def route_refresh(self, peer_ip=None, afi=None, safi=None):
|
||||
if not peer_ip:
|
||||
peer_ip = 'all'
|
||||
|
||||
try:
|
||||
route_families = []
|
||||
if afi is None and safi is None:
|
||||
route_families.extend(SUPPORTED_GLOBAL_RF)
|
||||
else:
|
||||
route_family = nlri.get_rf(afi, safi)
|
||||
if (route_family not in SUPPORTED_GLOBAL_RF):
|
||||
raise WrongParamError('Not supported address-family'
|
||||
' %s, %s' % (afi, safi))
|
||||
route_families.append(route_family)
|
||||
|
||||
pm = CORE_MANAGER.get_core_service().peer_manager
|
||||
pm.make_route_refresh_request(peer_ip, *route_families)
|
||||
except Exception as e:
|
||||
LOG.error(traceback.format_exc())
|
||||
raise WrongParamError(str(e))
|
||||
return None
|
||||
|
||||
def get_core_service(self):
|
||||
return CORE_MANAGER.get_core_service()
|
||||
|
||||
|
||||
@add_bgp_error_metadata(code=INTERNAL_API_ERROR,
|
||||
sub_code=INTERNAL_API_SUB_ERROR,
|
||||
def_desc='Unknown internal api exception.')
|
||||
class WrongParamError(BGPSException):
|
||||
pass
|
||||
1
ryu/services/protocols/bgp/operator/views/__init__.py
Normal file
1
ryu/services/protocols/bgp/operator/views/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
__author__ = 'yak'
|
||||
302
ryu/services/protocols/bgp/operator/views/base.py
Normal file
302
ryu/services/protocols/bgp/operator/views/base.py
Normal file
@ -0,0 +1,302 @@
|
||||
"""
|
||||
This module's purpose is to enable us to present internals of objects
|
||||
in well-defined way to operator. To do this we can define "views"
|
||||
on some objects. View is a definition of how to present object
|
||||
and relations to other objects which also have their views defined.
|
||||
|
||||
By using views we can avoid making all interesting internal values
|
||||
public. They will stay private and only "view" will access them
|
||||
(think friend-class from C++)
|
||||
"""
|
||||
import logging
|
||||
import types
|
||||
|
||||
from ryu.services.protocols.bgp.operator.views import fields
|
||||
|
||||
LOG = logging.getLogger('bgpspeaker.operator.views.base')
|
||||
|
||||
|
||||
class RdyToFlattenCollection(object):
|
||||
pass
|
||||
|
||||
|
||||
class RdyToFlattenList(list, RdyToFlattenCollection):
|
||||
pass
|
||||
|
||||
|
||||
class RdyToFlattenDict(dict, RdyToFlattenCollection):
|
||||
pass
|
||||
|
||||
|
||||
class OperatorAbstractView(object):
|
||||
"""Abstract base class for operator views. It isn't meant to be
|
||||
instantiated.
|
||||
"""
|
||||
|
||||
def __init__(self, obj, filter_func=None):
|
||||
"""Init
|
||||
|
||||
:param obj: data model for view. In other words object we
|
||||
are creating view for. In case of ListView it should be
|
||||
a list and in case of DictView it should be a dict.
|
||||
:param filter_func: function to filter models
|
||||
"""
|
||||
self._filter_func = filter_func
|
||||
self._fields = self._collect_fields()
|
||||
self._obj = obj
|
||||
|
||||
@classmethod
|
||||
def _collect_fields(cls):
|
||||
names = [attr for attr in dir(cls)
|
||||
if isinstance(getattr(cls, attr), fields.Field)]
|
||||
return {name: getattr(cls, name) for name in names}
|
||||
|
||||
def combine_related(self, field_name):
|
||||
"""Combines related views. In case of DetailView it just returns
|
||||
one-element list containing related view wrapped in
|
||||
CombinedViewsWrapper.
|
||||
|
||||
In case of ListView and DictView it returns a list of related views
|
||||
for every element of model collection also wrapped
|
||||
in CombinedViewsWrapper.
|
||||
|
||||
:param field_name: field name of related view
|
||||
:returns: vectorized form of related views. You can access them
|
||||
as if you had only one view and you will receive flattened list
|
||||
of responses from related views. Look at docstring of
|
||||
CombinedViewsWrapper
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def c_rel(self, *args, **kwargs):
|
||||
"""Shortcut for combine_related. Look above
|
||||
"""
|
||||
return self.combine_related(*args, **kwargs)
|
||||
|
||||
def get_field(self, field_name):
|
||||
"""Get value of data field.
|
||||
|
||||
:return: value of data-field of this view
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def encode(self):
|
||||
"""Representation of view which is using only python standard types.
|
||||
|
||||
:return: dict representation of this views data. However it
|
||||
doesn't have to be a dict. In case of ListView it would
|
||||
return a list. It should return wrapped types
|
||||
for list - RdyToFlattenList, for dict - RdyToFlattenDict
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def model(self):
|
||||
"""Getter for data model being presented by this view. Every view is
|
||||
associatetd with some data model.
|
||||
|
||||
:return: underlaying data of this view
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def apply_filter(self, filter_func):
|
||||
"""Sets filter function to apply on model
|
||||
|
||||
:param filter_func: function which takes the model and returns it
|
||||
filtered
|
||||
"""
|
||||
self._filter_func = filter_func
|
||||
|
||||
def clear_filter(self):
|
||||
self._filter_func = None
|
||||
|
||||
|
||||
class OperatorDetailView(OperatorAbstractView):
|
||||
def combine_related(self, field_name):
|
||||
f = self._fields[field_name]
|
||||
return CombinedViewsWrapper([f.retrieve_and_wrap(self._obj)])
|
||||
|
||||
def get_field(self, field_name):
|
||||
f = self._fields[field_name]
|
||||
return f.get(self._obj)
|
||||
|
||||
def encode(self):
|
||||
return {field_name: field.get(self._obj)
|
||||
for field_name, field in self._fields.iteritems()
|
||||
if isinstance(field, fields.DataField)}
|
||||
|
||||
def rel(self, field_name):
|
||||
f = self._fields[field_name]
|
||||
return f.retrieve_and_wrap(self._obj)
|
||||
|
||||
@property
|
||||
def model(self):
|
||||
return self._obj
|
||||
|
||||
|
||||
class OperatorListView(OperatorAbstractView):
|
||||
def __init__(self, obj, filter_func=None):
|
||||
assert isinstance(obj, list)
|
||||
obj = RdyToFlattenList(obj)
|
||||
super(OperatorListView, self).__init__(obj, filter_func)
|
||||
|
||||
def combine_related(self, field_name):
|
||||
f = self._fields[field_name]
|
||||
return CombinedViewsWrapper(RdyToFlattenList(
|
||||
map(lambda obj: f.retrieve_and_wrap(obj), self.model)
|
||||
))
|
||||
|
||||
def get_field(self, field_name):
|
||||
f = self._fields[field_name]
|
||||
return RdyToFlattenList([f.get(obj) for obj in self.model])
|
||||
|
||||
def encode(self):
|
||||
return RdyToFlattenList(
|
||||
[{field_name: field.get(obj)
|
||||
for field_name, field in self._fields.iteritems()
|
||||
if isinstance(field, fields.DataField)}
|
||||
for obj in self.model]
|
||||
)
|
||||
|
||||
@property
|
||||
def model(self):
|
||||
if self._filter_func is not None:
|
||||
return RdyToFlattenList(filter(self._filter_func, self._obj))
|
||||
else:
|
||||
return self._obj
|
||||
|
||||
|
||||
class OperatorDictView(OperatorAbstractView):
|
||||
def __init__(self, obj, filter_func=None):
|
||||
assert isinstance(obj, dict)
|
||||
obj = RdyToFlattenDict(obj)
|
||||
super(OperatorDictView, self).__init__(obj, filter_func)
|
||||
|
||||
def combine_related(self, field_name):
|
||||
f = self._fields[field_name]
|
||||
return CombinedViewsWrapper(RdyToFlattenList(
|
||||
map(lambda obj: f.retrieve_and_wrap(obj), self.model.itervalues()))
|
||||
)
|
||||
|
||||
def get_field(self, field_name):
|
||||
f = self._fields[field_name]
|
||||
return RdyToFlattenDict(
|
||||
{key: f.get(obj) for key, obj in self.model.iteritems()}
|
||||
)
|
||||
|
||||
def encode(self):
|
||||
return RdyToFlattenDict(
|
||||
{key: {field_name: field.get(obj)
|
||||
for field_name, field in self._fields.iteritems()
|
||||
if isinstance(field, fields.DataField)}
|
||||
for key, obj in self.model.iteritems()}
|
||||
)
|
||||
|
||||
@property
|
||||
def model(self):
|
||||
if self._filter_func is not None:
|
||||
new_model = RdyToFlattenDict()
|
||||
for k, v in self._obj.iteritems():
|
||||
if self._filter_func(k, v):
|
||||
new_model[k] = v
|
||||
return new_model
|
||||
else:
|
||||
return self._obj
|
||||
|
||||
|
||||
class CombinedViewsWrapper(RdyToFlattenList):
|
||||
"""List-like wrapper for views. It provides same interface as any other
|
||||
views but enables as to access all views in bulk.
|
||||
It wraps and return responses from all views as a list. Be aware that
|
||||
in case of DictViews wrapped in CombinedViewsWrapper you loose
|
||||
information about dict keys.
|
||||
"""
|
||||
|
||||
def __init__(self, obj):
|
||||
super(CombinedViewsWrapper, self).__init__(obj)
|
||||
self._obj = obj
|
||||
|
||||
def combine_related(self, field_name):
|
||||
return CombinedViewsWrapper(
|
||||
list(_flatten(
|
||||
[obj.combine_related(field_name) for obj in self._obj]
|
||||
))
|
||||
)
|
||||
|
||||
def c_rel(self, *args, **kwargs):
|
||||
return self.combine_related(*args, **kwargs)
|
||||
|
||||
def encode(self):
|
||||
return list(_flatten([obj.encode() for obj in self._obj]))
|
||||
|
||||
def get_field(self, field_name):
|
||||
return list(_flatten([obj.get_field(field_name) for obj in self._obj]))
|
||||
|
||||
@property
|
||||
def model(self):
|
||||
return list(_flatten([obj.model for obj in self._obj]))
|
||||
|
||||
def apply_filter(self, filter_func):
|
||||
for obj in self._obj:
|
||||
obj.apply_filter(filter_func)
|
||||
|
||||
def clear_filter(self):
|
||||
for obj in self._obj:
|
||||
obj.clear_filter()
|
||||
|
||||
|
||||
def _flatten(l, max_level=10):
|
||||
"""Generator function going deep in tree-like structures
|
||||
(i.e. dicts in dicts or lists in lists etc.) and returning all elements as
|
||||
a flat list. It's flattening only lists and dicts which are subclasses of
|
||||
RdyToFlattenCollection. Regular lists and dicts are treated as a
|
||||
single items.
|
||||
|
||||
:param l: some iterable to be flattened
|
||||
:return: flattened iterator
|
||||
"""
|
||||
if max_level >= 0:
|
||||
_iter = l.values() if isinstance(l, types.DictType) else l
|
||||
for el in _iter:
|
||||
if isinstance(el, RdyToFlattenCollection):
|
||||
for sub in _flatten(el, max_level=max_level - 1):
|
||||
yield sub
|
||||
else:
|
||||
yield el
|
||||
else:
|
||||
yield l
|
||||
|
||||
|
||||
def _create_collection_view(detail_view_class, name, encode=None,
|
||||
view_class=None):
|
||||
assert issubclass(detail_view_class, OperatorDetailView)
|
||||
class_fields = detail_view_class._collect_fields()
|
||||
if encode is not None:
|
||||
class_fields.update({'encode': encode})
|
||||
return type(name, (view_class,), class_fields)
|
||||
|
||||
|
||||
# function creating ListView from DetailView
|
||||
def create_dict_view_class(detail_view_class, name):
|
||||
encode = None
|
||||
if 'encode' in dir(detail_view_class):
|
||||
def encode(self):
|
||||
return RdyToFlattenDict({key: detail_view_class(obj).encode()
|
||||
for key, obj in self.model.iteritems()})
|
||||
|
||||
return _create_collection_view(
|
||||
detail_view_class, name, encode, OperatorDictView
|
||||
)
|
||||
|
||||
|
||||
# function creating DictView from DetailView
|
||||
def create_list_view_class(detail_view_class, name):
|
||||
encode = None
|
||||
if 'encode' in dir(detail_view_class):
|
||||
def encode(self):
|
||||
return RdyToFlattenList([detail_view_class(obj).encode()
|
||||
for obj in self.model])
|
||||
|
||||
return _create_collection_view(
|
||||
detail_view_class, name, encode, OperatorListView
|
||||
)
|
||||
273
ryu/services/protocols/bgp/operator/views/bgp.py
Normal file
273
ryu/services/protocols/bgp/operator/views/bgp.py
Normal file
@ -0,0 +1,273 @@
|
||||
from ryu.services.protocols.bgp.operator.views.base import \
|
||||
create_dict_view_class
|
||||
from ryu.services.protocols.bgp.operator.views.base import \
|
||||
create_list_view_class
|
||||
from ryu.services.protocols.bgp.operator.views.base import OperatorDetailView
|
||||
from ryu.services.protocols.bgp.operator.views import fields
|
||||
|
||||
from ryu.services.protocols.bgp.protocols.bgp import pathattr
|
||||
|
||||
|
||||
class CoreServiceDetailView(OperatorDetailView):
|
||||
rf_state = fields.RelatedViewField(
|
||||
'rf_state',
|
||||
'bgpspeaker.operator.views.bgp.RfStateDetailView'
|
||||
)
|
||||
importmap_manager = fields.RelatedDictViewField(
|
||||
'_importmap_manager',
|
||||
'bgpspeaker.operator.views.other.ImportmapManagerDetailView'
|
||||
)
|
||||
table_manager = fields.RelatedViewField(
|
||||
'_table_manager',
|
||||
'bgpspeaker.operator.views.bgp.TableManagerDetailView'
|
||||
)
|
||||
peer_manager = fields.RelatedViewField(
|
||||
'_peer_manager',
|
||||
'bgpspeaker.operator.views.bgp.PeerManagerDetailView'
|
||||
)
|
||||
router_id = fields.DataField('router_id')
|
||||
|
||||
|
||||
class TableManagerDetailView(OperatorDetailView):
|
||||
tables = fields.RelatedDictViewField(
|
||||
'_tables',
|
||||
'bgpspeaker.operator.views.bgp.TableDictView'
|
||||
)
|
||||
tables_for_rt = fields.RelatedDictViewField(
|
||||
'_tables_for_rt',
|
||||
'bgpspeaker.operator.views.bgp.TableDictView'
|
||||
)
|
||||
global_tables = fields.RelatedDictViewField(
|
||||
'_global_tables',
|
||||
'bgpspeaker.operator.views.bgp.TableDictView'
|
||||
)
|
||||
asbr_label_range = fields.DataField('_asbr_label_range')
|
||||
next_hop_label = fields.DataField('_next_hop_label')
|
||||
next_vpnv4_label = fields.DataField('_next_vpnv4_label')
|
||||
|
||||
|
||||
class PeerManagerDetailView(OperatorDetailView):
|
||||
peers = fields.RelatedListViewField(
|
||||
'_peers',
|
||||
'bgpspeaker.operator.views.bgp.PeerDictView'
|
||||
)
|
||||
peers_summary = fields.RelatedListViewField(
|
||||
'_peers',
|
||||
'bgpspeaker.operator.views.bgp.PeerDictSummaryView'
|
||||
)
|
||||
|
||||
|
||||
class RfStateDetailView(OperatorDetailView):
|
||||
pass
|
||||
|
||||
|
||||
class PeerStateDetailView(OperatorDetailView):
|
||||
bgp_state = fields.DataField('_bgp_state')
|
||||
last_error = fields.DataField('_last_bgp_error')
|
||||
|
||||
def encode(self):
|
||||
ret = super(PeerStateDetailView, self).encode()
|
||||
ret.update(self._obj.get_stats_summary_dict())
|
||||
return ret
|
||||
|
||||
|
||||
class PeerDetailView(OperatorDetailView):
|
||||
remote_as = fields.DataField('remote_as')
|
||||
ip_address = fields.DataField('ip_address')
|
||||
enabled = fields.DataField('enabled')
|
||||
neigh_conf = fields.RelatedViewField(
|
||||
'_neigh_conf',
|
||||
'bgpspeaker.operator.views.conf.ConfDetailView'
|
||||
)
|
||||
common_conf = fields.RelatedViewField(
|
||||
'_common_conf',
|
||||
'bgpspeaker.operator.views.conf.ConfDetailView'
|
||||
)
|
||||
state = fields.RelatedViewField(
|
||||
'state',
|
||||
'bgpspeaker.operator.views.bgp.PeerStateDetailView'
|
||||
)
|
||||
|
||||
def encode(self):
|
||||
ret = super(PeerDetailView, self).encode()
|
||||
ret.update({
|
||||
'stats': self.rel('state').encode(),
|
||||
'settings': self.rel('neigh_conf').encode()
|
||||
})
|
||||
return ret
|
||||
|
||||
|
||||
class PeerDetailSummaryView(PeerDetailView):
|
||||
def encode(self):
|
||||
return {
|
||||
'conf': self.rel('neigh_conf').encode(),
|
||||
'info': self.rel('state').encode()
|
||||
}
|
||||
|
||||
|
||||
class PeerRfDetailView(OperatorDetailView):
|
||||
rf = fields.DataField('rf')
|
||||
enabled = fields.DataField('enabled')
|
||||
peer = fields.RelatedViewField(
|
||||
'peer',
|
||||
'bgpspeaker.operator.views.bgp.PeerDetailView'
|
||||
)
|
||||
|
||||
|
||||
class TableDetailView(OperatorDetailView):
|
||||
scope_id = fields.DataField('scope_id')
|
||||
route_family = fields.DataField('route_family')
|
||||
destinations = fields.RelatedDictViewField(
|
||||
'_destinations',
|
||||
'bgpspeaker.operator.views.bgp.DestinationDictView'
|
||||
)
|
||||
|
||||
|
||||
class PathDetailView(OperatorDetailView):
|
||||
source_version_num = fields.DataField('source_version_num')
|
||||
route_family = fields.RelatedViewField(
|
||||
'route_family', 'bgpspeaker.operator.views.bgp.RouteFamilyView'
|
||||
)
|
||||
nlri = fields.RelatedViewField(
|
||||
'nlri',
|
||||
'bgpspeaker.operator.views.bgp.NlriDetailView'
|
||||
)
|
||||
is_withdraw = fields.DataField('is_withdraw')
|
||||
nexthop = fields.DataField('nexthop')
|
||||
pathattr_map = fields.DataField('pathattr_map')
|
||||
source = fields.RelatedViewField(
|
||||
'source',
|
||||
'bgpspeaker.operator.views.bgp.PeerDetailView'
|
||||
)
|
||||
|
||||
def encode(self):
|
||||
ret = super(PathDetailView, self).encode()
|
||||
ret['nlri'] = self.rel('nlri').encode()
|
||||
ret['route_family'] = self.rel('route_family').encode()
|
||||
as_path = self.get_field('pathattr_map').get(pathattr.AsPath.ATTR_NAME)
|
||||
origin = self.get_field('pathattr_map').get(pathattr.Origin.ATTR_NAME)
|
||||
metric = self.get_field('pathattr_map').get(pathattr.Med.ATTR_NAME)
|
||||
local_pref = self.get_field('pathattr_map').get(
|
||||
pathattr.LocalPref.ATTR_NAME
|
||||
)
|
||||
|
||||
ret['as_path'] = as_path.value if as_path else None
|
||||
ret['origin'] = origin.value if origin else None
|
||||
ret['metric'] = metric.value if metric else None
|
||||
ret['local_pref'] = local_pref.value if local_pref else None
|
||||
ext = ret['pathattr_map'].get(pathattr.ExtCommunity.ATTR_NAME)
|
||||
del ret['pathattr_map']
|
||||
if ext:
|
||||
ret['rt_list'] = ext.rt_list
|
||||
ret['soo_list'] = ext.soo_list
|
||||
return ret
|
||||
|
||||
|
||||
class SentRouteDetailView(OperatorDetailView):
|
||||
path = fields.RelatedViewField(
|
||||
'path',
|
||||
'bgpspeaker.operator.views.bgp.PathDetailView',
|
||||
)
|
||||
peer = fields.RelatedViewField(
|
||||
'_sent_peer',
|
||||
'bgpspeaker.operator.views.bgp.PeerDetailView'
|
||||
)
|
||||
|
||||
|
||||
class DestinationDetailView(OperatorDetailView):
|
||||
table = fields.RelatedViewField(
|
||||
'_table',
|
||||
'bgpspeaker.operator.views.bgp.TableDetailView',
|
||||
)
|
||||
best_path = fields.RelatedViewField(
|
||||
'best_path',
|
||||
'bgpspeaker.operator.views.bgp.PathDetailView'
|
||||
)
|
||||
known_path_list = fields.RelatedListViewField(
|
||||
'known_path_list',
|
||||
'bgpspeaker.operator.views.bgp.PathListView'
|
||||
)
|
||||
new_path_list = fields.RelatedListViewField(
|
||||
'_new_path_list',
|
||||
'bgpspeaker.operator.views.bgp.PathListView'
|
||||
)
|
||||
withdraw_list = fields.RelatedListViewField(
|
||||
'_withdraw_list',
|
||||
'bgpspeaker.operator.views.bgp.PathListView'
|
||||
)
|
||||
sent_routes = fields.RelatedListViewField(
|
||||
'sent_routes',
|
||||
'bgpspeaker.operator.views.bgp.SentRouteListView'
|
||||
)
|
||||
nlri = fields.DataField('nlri')
|
||||
route_family = fields.DataField('route_family')
|
||||
|
||||
|
||||
class IpNlriDetailView(OperatorDetailView):
|
||||
formatted_nlri = fields.DataField('formatted_nlri_str')
|
||||
prefix = fields.DataField('prefix')
|
||||
|
||||
|
||||
class VpnNlriDetailView(IpNlriDetailView):
|
||||
labels = fields.DataField('label_list')
|
||||
rd = fields.DataField('route_disc')
|
||||
|
||||
|
||||
class NlriDetailView(OperatorDetailView):
|
||||
def __new__(cls, obj, filter_func=None):
|
||||
from ryu.services.protocols.bgp.protocols.bgp.nlri import Vpnv4, Vpnv6
|
||||
from ryu.services.protocols.bgp.protocols.bgp.nlri import Ipv4, Ipv6
|
||||
if isinstance(obj, (Vpnv4, Vpnv6)):
|
||||
return VpnNlriDetailView(obj)
|
||||
elif isinstance(obj, (Ipv4, Ipv6)):
|
||||
return IpNlriDetailView(obj)
|
||||
else:
|
||||
return OperatorDetailView(obj, filter_func)
|
||||
|
||||
def encode(self):
|
||||
return self._obj.formatted_nlri_str
|
||||
|
||||
|
||||
class RouteFamilyView(OperatorDetailView):
|
||||
afi = fields.DataField('afi')
|
||||
safi = fields.DataField('safi')
|
||||
|
||||
##################################################################
|
||||
# Declarations of list and dict views based on detail views above
|
||||
##################################################################
|
||||
PeerListView = create_list_view_class(PeerDetailView, 'PeerListView')
|
||||
PeerDictView = create_dict_view_class(PeerDetailView, 'PeerDictView')
|
||||
|
||||
PeerListSummaryView = create_list_view_class(
|
||||
PeerDetailSummaryView,
|
||||
'PeerListSummaryView'
|
||||
)
|
||||
|
||||
PeerDictSummaryView = create_dict_view_class(
|
||||
PeerDetailSummaryView,
|
||||
'PeerDictSummaryView'
|
||||
)
|
||||
|
||||
TableDictView = create_dict_view_class(TableDetailView, 'TableDictView')
|
||||
|
||||
|
||||
DestinationListView = create_list_view_class(
|
||||
DestinationDetailView, 'DestinationListView'
|
||||
)
|
||||
|
||||
DestinationDictView = create_dict_view_class(
|
||||
DestinationDetailView, 'DestinationDictView'
|
||||
)
|
||||
|
||||
PathListView = create_list_view_class(PathDetailView, 'PathListView')
|
||||
PathDictView = create_dict_view_class(PathDetailView, 'PathDictView')
|
||||
|
||||
SentRouteListView = create_list_view_class(
|
||||
SentRouteDetailView,
|
||||
'SentRouteListView'
|
||||
)
|
||||
|
||||
SentRouteDictView = create_dict_view_class(
|
||||
SentRouteDetailView,
|
||||
'SentRouteDictView'
|
||||
)
|
||||
14
ryu/services/protocols/bgp/operator/views/conf.py
Normal file
14
ryu/services/protocols/bgp/operator/views/conf.py
Normal file
@ -0,0 +1,14 @@
|
||||
from ryu.services.protocols.bgp.operator.views.base import \
|
||||
create_dict_view_class
|
||||
from ryu.services.protocols.bgp.operator.views.base import OperatorDetailView
|
||||
from ryu.services.protocols.bgp.operator.views import fields
|
||||
|
||||
|
||||
class ConfDetailView(OperatorDetailView):
|
||||
settings = fields.DataField('_settings')
|
||||
|
||||
def encode(self):
|
||||
return self.get_field('settings')
|
||||
|
||||
|
||||
ConfDictView = create_dict_view_class(ConfDetailView, 'ConfDictView')
|
||||
69
ryu/services/protocols/bgp/operator/views/fields.py
Normal file
69
ryu/services/protocols/bgp/operator/views/fields.py
Normal file
@ -0,0 +1,69 @@
|
||||
import importlib
|
||||
import inspect
|
||||
|
||||
|
||||
class Field(object):
|
||||
def __init__(self, field_name):
|
||||
self.field_name = field_name
|
||||
|
||||
def get(self, obj):
|
||||
return getattr(obj, self.field_name)
|
||||
|
||||
|
||||
class RelatedViewField(Field):
|
||||
def __init__(self, field_name, operator_view_class):
|
||||
super(RelatedViewField, self).__init__(field_name)
|
||||
self.__operator_view_class = operator_view_class
|
||||
|
||||
@property
|
||||
def _operator_view_class(self):
|
||||
if inspect.isclass(self.__operator_view_class):
|
||||
return self.__operator_view_class
|
||||
elif isinstance(self.__operator_view_class, basestring):
|
||||
try:
|
||||
module_name, class_name =\
|
||||
self.__operator_view_class.rsplit('.', 1)
|
||||
return class_for_name(module_name, class_name)
|
||||
except (AttributeError, ValueError, ImportError):
|
||||
raise WrongOperatorViewClassError(
|
||||
'There is no "%s" class' % self.__operator_view_class
|
||||
)
|
||||
|
||||
def retrieve_and_wrap(self, obj):
|
||||
related_obj = self.get(obj)
|
||||
return self.wrap(related_obj)
|
||||
|
||||
def wrap(self, obj):
|
||||
return self._operator_view_class(obj)
|
||||
|
||||
|
||||
class RelatedListViewField(RelatedViewField):
|
||||
pass
|
||||
|
||||
|
||||
class RelatedDictViewField(RelatedViewField):
|
||||
pass
|
||||
|
||||
|
||||
class DataField(Field):
|
||||
pass
|
||||
|
||||
|
||||
class OptionalDataField(DataField):
|
||||
def get(self, obj):
|
||||
if hasattr(obj, self.field_name):
|
||||
return getattr(obj, self.field_name)
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
class WrongOperatorViewClassError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def class_for_name(module_name, class_name):
|
||||
# load the module, will raise ImportError if module cannot be loaded
|
||||
m = importlib.import_module(module_name)
|
||||
# get the class, will raise AttributeError if class cannot be found
|
||||
c = getattr(m, class_name)
|
||||
return c
|
||||
34
ryu/services/protocols/bgp/operator/views/other.py
Normal file
34
ryu/services/protocols/bgp/operator/views/other.py
Normal file
@ -0,0 +1,34 @@
|
||||
from ryu.services.protocols.bgp.operator.views.base import \
|
||||
create_dict_view_class
|
||||
from ryu.services.protocols.bgp.operator.views.base import OperatorDetailView
|
||||
from ryu.services.protocols.bgp.operator.views import fields
|
||||
|
||||
|
||||
class ImportmapManagerDetailView(OperatorDetailView):
|
||||
importmaps = fields.RelatedDictViewField(
|
||||
'_import_maps_by_name',
|
||||
'bgpspeaker.operator.views.other.ImportmapDictView'
|
||||
)
|
||||
|
||||
|
||||
class ImportmapDetailView(OperatorDetailView):
|
||||
nlri = fields.OptionalDataField('_nlri')
|
||||
rt = fields.OptionalDataField('_rt')
|
||||
|
||||
def encode(self):
|
||||
ret = {}
|
||||
nlri = self.get_field('nlri')
|
||||
if nlri is not None:
|
||||
ret.update({'nlri': nlri})
|
||||
|
||||
rt = self.get_field('rt')
|
||||
if rt is not None:
|
||||
ret.update({'rt': rt})
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
ImportmapDictView = create_dict_view_class(
|
||||
ImportmapDetailView,
|
||||
'ImportmapDictView'
|
||||
)
|
||||
1481
ryu/services/protocols/bgp/peer.py
Normal file
1481
ryu/services/protocols/bgp/peer.py
Normal file
File diff suppressed because it is too large
Load Diff
512
ryu/services/protocols/bgp/processor.py
Normal file
512
ryu/services/protocols/bgp/processor.py
Normal file
@ -0,0 +1,512 @@
|
||||
# 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.
|
||||
"""
|
||||
Module related to processing bgp paths.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from ryu.services.protocols.bgp.protocols.bgp.nlri import RF_RTC_UC
|
||||
from ryu.services.protocols.bgp.protocols.bgp.pathattr import AsPath
|
||||
from ryu.services.protocols.bgp.protocols.bgp.pathattr import LocalPref
|
||||
from ryu.services.protocols.bgp.protocols.bgp.pathattr import Med
|
||||
from ryu.services.protocols.bgp.protocols.bgp.pathattr import Origin
|
||||
|
||||
from ryu.services.protocols.bgp.base import Activity
|
||||
from ryu.services.protocols.bgp.base import add_bgp_error_metadata
|
||||
from ryu.services.protocols.bgp.base import BGP_PROCESSOR_ERROR_CODE
|
||||
from ryu.services.protocols.bgp.base import BGPSException
|
||||
from ryu.services.protocols.bgp.utils import circlist
|
||||
from ryu.services.protocols.bgp.utils.evtlet import EventletIOFactory
|
||||
|
||||
|
||||
LOG = logging.getLogger('bgpspeaker.processor')
|
||||
|
||||
|
||||
@add_bgp_error_metadata(code=BGP_PROCESSOR_ERROR_CODE, sub_code=1,
|
||||
def_desc='Error occurred when processing bgp '
|
||||
'destination.')
|
||||
class BgpProcessorError(BGPSException):
|
||||
"""Base exception related to all destination path processing errors.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
# Disabling known bug in pylint.
|
||||
# pylint: disable=R0921
|
||||
class BgpProcessor(Activity):
|
||||
"""Worker that processes queued `Destination'.
|
||||
|
||||
`Destination` that have updates related to its paths need to be
|
||||
(re)processed. Only one instance of this processor is enough for normal
|
||||
cases. If you want more control on which destinations get processed faster
|
||||
compared to other destinations, you can create several instance of this
|
||||
works to achieve the desired work flow.
|
||||
"""
|
||||
|
||||
# Max. number of destinations processed per cycle.
|
||||
MAX_DEST_PROCESSED_PER_CYCLE = 100
|
||||
|
||||
#
|
||||
# DestQueue
|
||||
#
|
||||
# A circular list type in which objects are linked to each
|
||||
# other using the 'next_dest_to_process' and 'prev_dest_to_process'
|
||||
# attributes.
|
||||
#
|
||||
_DestQueue = circlist.CircularListType(
|
||||
next_attr_name='next_dest_to_process',
|
||||
prev_attr_name='prev_dest_to_process')
|
||||
|
||||
def __init__(self, core_service, work_units_per_cycle=None):
|
||||
Activity.__init__(self)
|
||||
# Back pointer to core service instance that created this processor.
|
||||
self._core_service = core_service
|
||||
self._dest_queue = BgpProcessor._DestQueue()
|
||||
self._rtdest_queue = BgpProcessor._DestQueue()
|
||||
self.dest_que_evt = EventletIOFactory.create_custom_event()
|
||||
self.work_units_per_cycle =\
|
||||
work_units_per_cycle or BgpProcessor.MAX_DEST_PROCESSED_PER_CYCLE
|
||||
|
||||
def _run(self, *args, **kwargs):
|
||||
# Sit in tight loop, getting destinations from the queue and processing
|
||||
# one at a time.
|
||||
while True:
|
||||
LOG.debug('Starting new processing run...')
|
||||
# We process all RT destination first so that we get a new RT
|
||||
# filter that apply for each peer
|
||||
self._process_rtdest()
|
||||
|
||||
# We then process a batch of other destinations (we do not process
|
||||
# all destination here as we want to give change to other
|
||||
# greenthread to run)
|
||||
self._process_dest()
|
||||
|
||||
if self._dest_queue.is_empty():
|
||||
# If we have no destinations queued for processing, we wait.
|
||||
self.dest_que_evt.clear()
|
||||
self.dest_que_evt.wait()
|
||||
else:
|
||||
self.pause(0)
|
||||
|
||||
def _process_dest(self):
|
||||
dest_processed = 0
|
||||
LOG.debug('Processing destination...')
|
||||
while (dest_processed < self.work_units_per_cycle and
|
||||
not self._dest_queue.is_empty()):
|
||||
# We process the first destination in the queue.
|
||||
next_dest = self._dest_queue.pop_first()
|
||||
if next_dest:
|
||||
next_dest.process()
|
||||
dest_processed += 1
|
||||
|
||||
def _process_rtdest(self):
|
||||
LOG.debug('Processing RT NLRI destination...')
|
||||
if self._rtdest_queue.is_empty():
|
||||
return
|
||||
else:
|
||||
processed_any = False
|
||||
while not self._rtdest_queue.is_empty():
|
||||
# We process the first destination in the queue.
|
||||
next_dest = self._rtdest_queue.pop_first()
|
||||
if next_dest:
|
||||
next_dest.process()
|
||||
processed_any = True
|
||||
|
||||
if processed_any:
|
||||
# Since RT destination were updated we update RT filters
|
||||
self._core_service.update_rtfilters()
|
||||
|
||||
def enqueue(self, destination):
|
||||
"""Enqueues given destination for processing.
|
||||
|
||||
Given instance should be a valid destination.
|
||||
"""
|
||||
if not destination:
|
||||
raise BgpProcessorError('Invalid destination %s.' % destination)
|
||||
|
||||
dest_queue = self._dest_queue
|
||||
# RtDest are queued in a separate queue
|
||||
if destination.route_family == RF_RTC_UC:
|
||||
dest_queue = self._rtdest_queue
|
||||
|
||||
# We do not add given destination to the queue for processing if
|
||||
# it is already on the queue.
|
||||
if not dest_queue.is_on_list(destination):
|
||||
dest_queue.append(destination)
|
||||
|
||||
# Wake-up processing thread if sleeping.
|
||||
self.dest_que_evt.set()
|
||||
|
||||
#==============================================================================
|
||||
# Best path computation related utilities.
|
||||
#==============================================================================
|
||||
|
||||
# Various reasons a path is chosen as best path.
|
||||
BPR_UNKNOWN = 'Unknown'
|
||||
BPR_ONLY_PATH = 'Only Path'
|
||||
BPR_REACHABLE_NEXT_HOP = 'Reachable Next Hop'
|
||||
BPR_HIGHEST_WEIGHT = 'Highest Weight'
|
||||
BPR_LOCAL_PREF = 'Local Pref.'
|
||||
BPR_LOCAL_ORIGIN = 'Local Origin'
|
||||
BPR_ASPATH = 'AS Path'
|
||||
BPR_ORIGIN = 'Origin'
|
||||
BPR_MED = 'MED'
|
||||
BPR_ASN = 'ASN'
|
||||
BPR_IGP_COST = 'IGP Cost'
|
||||
BPR_ROUTER_ID = 'Router ID'
|
||||
|
||||
|
||||
def _compare_by_version(path1, path2):
|
||||
"""Returns the current/latest learned path.
|
||||
|
||||
Checks if given paths are from same source/peer and then compares their
|
||||
version number to determine which path is received later. If paths are from
|
||||
different source/peer return None.
|
||||
"""
|
||||
if path1.source == path2.source:
|
||||
if path1.source_version_num > path2.source_version_num:
|
||||
return path1
|
||||
else:
|
||||
return path2
|
||||
return None
|
||||
|
||||
|
||||
def compute_best_path(local_asn, path1, path2):
|
||||
"""Compares given paths and returns best path.
|
||||
|
||||
Parameters:
|
||||
-`local_asn`: asn of local bgpspeaker
|
||||
-`path1`: first path to compare
|
||||
-`path2`: second path to compare
|
||||
|
||||
Best path processing will involve following steps:
|
||||
1. Select a path with a reachable next hop.
|
||||
2. Select the path with the highest weight.
|
||||
3. If path weights are the same, select the path with the highest
|
||||
local preference value.
|
||||
4. Prefer locally originated routes (network routes, redistributed
|
||||
routes, or aggregated routes) over received routes.
|
||||
5. Select the route with the shortest AS-path length.
|
||||
6. If all paths have the same AS-path length, select the path based
|
||||
on origin: IGP is preferred over EGP; EGP is preferred over
|
||||
Incomplete.
|
||||
7. If the origins are the same, select the path with lowest MED
|
||||
value.
|
||||
8. If the paths have the same MED values, select the path learned
|
||||
via EBGP over one learned via IBGP.
|
||||
9. Select the route with the lowest IGP cost to the next hop.
|
||||
10. Select the route received from the peer with the lowest BGP
|
||||
router ID.
|
||||
|
||||
Returns None if best-path among given paths cannot be computed else best
|
||||
path.
|
||||
Assumes paths from NC has source equal to None.
|
||||
"""
|
||||
best_path = None
|
||||
best_path_reason = BPR_UNKNOWN
|
||||
|
||||
# Follow best path calculation algorithm steps.
|
||||
if best_path is None:
|
||||
best_path = _cmp_by_reachable_nh(path1, path2)
|
||||
best_path_reason = BPR_REACHABLE_NEXT_HOP
|
||||
if best_path is None:
|
||||
best_path = _cmp_by_higest_wg(path1, path2)
|
||||
best_path_reason = BPR_HIGHEST_WEIGHT
|
||||
if best_path is None:
|
||||
best_path = _cmp_by_local_pref(path1, path2)
|
||||
best_path_reason = BPR_LOCAL_PREF
|
||||
if best_path is None:
|
||||
best_path = _cmp_by_local_origin(path1, path2)
|
||||
best_path_reason = BPR_LOCAL_ORIGIN
|
||||
if best_path is None:
|
||||
best_path = _cmp_by_aspath(path1, path2)
|
||||
best_path_reason = BPR_ASPATH
|
||||
if best_path is None:
|
||||
best_path = _cmp_by_origin(path1, path2)
|
||||
best_path_reason = BPR_ORIGIN
|
||||
if best_path is None:
|
||||
best_path = _cmp_by_med(path1, path2)
|
||||
best_path_reason = BPR_MED
|
||||
if best_path is None:
|
||||
best_path = _cmp_by_asn(local_asn, path1, path2)
|
||||
best_path_reason = BPR_ASN
|
||||
if best_path is None:
|
||||
best_path = _cmp_by_igp_cost(path1, path2)
|
||||
best_path_reason = BPR_IGP_COST
|
||||
if best_path is None:
|
||||
best_path = _cmp_by_router_id(local_asn, path1, path2)
|
||||
best_path_reason = BPR_ROUTER_ID
|
||||
if best_path is None:
|
||||
best_path_reason = BPR_UNKNOWN
|
||||
|
||||
return (best_path, best_path_reason)
|
||||
|
||||
|
||||
def _cmp_by_reachable_nh(path1, path2):
|
||||
"""Compares given paths and selects best path based on reachable next-hop.
|
||||
|
||||
If no path matches this criteria, return None.
|
||||
"""
|
||||
# TODO(PH): Currently we do not have a way to check if a IGP route to
|
||||
# NEXT_HOP exists from BGPS.
|
||||
return None
|
||||
|
||||
|
||||
def _cmp_by_higest_wg(path1, path2):
|
||||
"""Selects a path with highest weight.
|
||||
|
||||
Weight is BGPS specific parameter. It is local to the router on which it
|
||||
is configured.
|
||||
Return:
|
||||
None if best path among given paths cannot be decided, else best path.
|
||||
"""
|
||||
# TODO(PH): Revisit this when BGPS has concept of policy to be applied to
|
||||
# in-bound NLRIs.
|
||||
return None
|
||||
|
||||
|
||||
def _cmp_by_local_pref(path1, path2):
|
||||
"""Selects a path with highest local-preference.
|
||||
|
||||
Unlike the weight attribute, which is only relevant to the local
|
||||
router, local preference is an attribute that routers exchange in the
|
||||
same AS. Highest local-pref is preferred. If we cannot decide,
|
||||
we return None.
|
||||
"""
|
||||
# TODO(PH): Revisit this when BGPS has concept of policy to be applied to
|
||||
# in-bound NLRIs.
|
||||
# Default local-pref values is 100
|
||||
lp1 = path1.get_pattr(LocalPref.ATTR_NAME)
|
||||
lp2 = path2.get_pattr(LocalPref.ATTR_NAME)
|
||||
if not (lp1 and lp2):
|
||||
return None
|
||||
|
||||
# Highest local-preference value is preferred.
|
||||
lp1 = lp1.value
|
||||
lp2 = lp2.value
|
||||
if lp1 > lp2:
|
||||
return path1
|
||||
elif lp2 > lp1:
|
||||
return path2
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def _cmp_by_local_origin(path1, path2):
|
||||
"""Select locally originating path as best path.
|
||||
|
||||
Locally originating routes are network routes, redistributed routes,
|
||||
or aggregated routes. For now we are going to prefer routes received
|
||||
through a Flexinet-Peer as locally originating route compared to routes
|
||||
received from a BGP peer.
|
||||
Returns None if given paths have same source.
|
||||
"""
|
||||
# If both paths are from same sources we cannot compare them here.
|
||||
if path1.source == path2.source:
|
||||
return None
|
||||
|
||||
# Here we consider prefix from NC as locally originating static route.
|
||||
# Hence it is preferred.
|
||||
if path1.source is None:
|
||||
return path1
|
||||
|
||||
if path2.source is None:
|
||||
return path2
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _cmp_by_aspath(path1, path2):
|
||||
"""Calculated the best-paths by comparing as-path lengths.
|
||||
|
||||
Shortest as-path length is preferred. If both path have same lengths,
|
||||
we return None.
|
||||
"""
|
||||
as_path1 = path1.get_pattr(AsPath.ATTR_NAME)
|
||||
as_path2 = path2.get_pattr(AsPath.ATTR_NAME)
|
||||
assert as_path1 and as_path2
|
||||
l1 = as_path1.get_as_path_len()
|
||||
l2 = as_path2.get_as_path_len()
|
||||
assert l1 is not None and l2 is not None
|
||||
if l1 > l2:
|
||||
return path2
|
||||
elif l2 > l1:
|
||||
return path1
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def _cmp_by_origin(path1, path2):
|
||||
"""Select the best path based on origin attribute.
|
||||
|
||||
IGP is preferred over EGP; EGP is preferred over Incomplete.
|
||||
If both paths have same origin, we return None.
|
||||
"""
|
||||
def get_origin_pref(origin):
|
||||
if origin.value == Origin.IGP:
|
||||
return 3
|
||||
elif origin.value == Origin.EGP:
|
||||
return 2
|
||||
elif origin.value == Origin.INCOMPLETE:
|
||||
return 1
|
||||
else:
|
||||
LOG.error('Invalid origin value encountered %s.' % origin)
|
||||
return 0
|
||||
|
||||
origin1 = path1.get_pattr(Origin.ATTR_NAME)
|
||||
origin2 = path2.get_pattr(Origin.ATTR_NAME)
|
||||
assert origin1 is not None and origin2 is not None
|
||||
|
||||
# If both paths have same origins
|
||||
if origin1.value == origin2.value:
|
||||
return None
|
||||
|
||||
# Translate origin values to preference.
|
||||
origin1 = get_origin_pref(origin1)
|
||||
origin2 = get_origin_pref(origin2)
|
||||
# Return preferred path.
|
||||
if origin1 == origin2:
|
||||
return None
|
||||
elif origin1 > origin2:
|
||||
return path1
|
||||
return path2
|
||||
|
||||
|
||||
def _cmp_by_med(path1, path2):
|
||||
"""Select the path based with lowest MED value.
|
||||
|
||||
If both paths have same MED, return None.
|
||||
By default, a route that arrives with no MED value is treated as if it
|
||||
had a MED of 0, the most preferred value.
|
||||
RFC says lower MED is preferred over higher MED value.
|
||||
"""
|
||||
def get_path_med(path):
|
||||
med = path.get_pattr(Med.ATTR_NAME)
|
||||
if not med:
|
||||
return 0
|
||||
return med.value
|
||||
|
||||
med1 = get_path_med(path1)
|
||||
med2 = get_path_med(path2)
|
||||
|
||||
if med1 == med2:
|
||||
return None
|
||||
elif med1 < med2:
|
||||
return path1
|
||||
return path2
|
||||
|
||||
|
||||
def _cmp_by_asn(local_asn, path1, path2):
|
||||
"""Select the path based on source (iBGP/eBGP) peer.
|
||||
|
||||
eBGP path is preferred over iBGP. If both paths are from same kind of
|
||||
peers, return None.
|
||||
"""
|
||||
def get_path_source_asn(path):
|
||||
asn = None
|
||||
if path.source is None:
|
||||
asn = local_asn
|
||||
else:
|
||||
asn = path.source.remote_as
|
||||
return asn
|
||||
|
||||
p1_asn = get_path_source_asn(path1)
|
||||
p2_asn = get_path_source_asn(path2)
|
||||
# If path1 is from ibgp peer and path2 is from ebgp peer.
|
||||
if (p1_asn == local_asn) and (p2_asn != local_asn):
|
||||
return path2
|
||||
|
||||
# If path2 is from ibgp peer and path1 is from ebgp peer,
|
||||
if (p2_asn == local_asn) and (p1_asn != local_asn):
|
||||
return path1
|
||||
|
||||
# If both paths are from ebgp or ibpg peers, we cannot decide.
|
||||
return None
|
||||
|
||||
|
||||
def _cmp_by_igp_cost(path1, path2):
|
||||
"""Select the route with the lowest IGP cost to the next hop.
|
||||
|
||||
Return None if igp cost is same.
|
||||
"""
|
||||
# TODO(PH): Currently BGPS has no concept of IGP and IGP cost.
|
||||
return None
|
||||
|
||||
|
||||
def _cmp_by_router_id(local_asn, path1, path2):
|
||||
"""Select the route received from the peer with the lowest BGP router ID.
|
||||
|
||||
If both paths are eBGP paths, then we do not do any tie breaking, i.e we do
|
||||
not pick best-path based on this criteria.
|
||||
RFC: http://tools.ietf.org/html/rfc5004
|
||||
We pick best path between two iBGP paths as usual.
|
||||
"""
|
||||
def get_asn(path_source):
|
||||
if path_source is None:
|
||||
return local_asn
|
||||
else:
|
||||
return path_source.remote_as
|
||||
|
||||
def get_router_id(path_source, local_bgp_id):
|
||||
if path_source is None:
|
||||
return local_bgp_id
|
||||
else:
|
||||
return path_source.protocol.recv_open.bgpid
|
||||
|
||||
path_source1 = path1.source
|
||||
path_source2 = path2.source
|
||||
|
||||
# If both paths are from NC we have same router Id, hence cannot compare.
|
||||
if path_source1 is None and path_source2 is None:
|
||||
return None
|
||||
|
||||
asn1 = get_asn(path_source1)
|
||||
asn2 = get_asn(path_source2)
|
||||
|
||||
is_ebgp1 = asn1 != local_asn
|
||||
is_ebgp2 = asn2 != local_asn
|
||||
# If both paths are from eBGP peers, then according to RFC we need
|
||||
# not tie break using router id.
|
||||
if (is_ebgp1 and is_ebgp2):
|
||||
return None
|
||||
|
||||
if ((is_ebgp1 is True and is_ebgp2 is False) or
|
||||
(is_ebgp1 is False and is_ebgp2 is True)):
|
||||
raise ValueError('This method does not support comparing ebgp with'
|
||||
' ibgp path')
|
||||
|
||||
# At least one path is not coming from NC, so we get local bgp id.
|
||||
if path_source1 is not None:
|
||||
local_bgp_id = path_source1.protocol.sent_open.bgpid
|
||||
else:
|
||||
local_bgp_id = path_source2.protocol.sent_open.bgpid
|
||||
|
||||
# Get router ids.
|
||||
router_id1 = get_router_id(path_source1, local_bgp_id)
|
||||
router_id2 = get_router_id(path_source2, local_bgp_id)
|
||||
|
||||
# If both router ids are same/equal we cannot decide.
|
||||
# This case is possible since router ids are arbitrary.
|
||||
if router_id1 == router_id2:
|
||||
return None
|
||||
|
||||
# Select the path with lowest router Id.
|
||||
from ryu.services.protocols.bgp.ker.utils.bgp import from_inet_ptoi
|
||||
if (from_inet_ptoi(router_id1) <
|
||||
from_inet_ptoi(router_id2)):
|
||||
return path1
|
||||
else:
|
||||
return path2
|
||||
87
ryu/services/protocols/bgp/protocol.py
Normal file
87
ryu/services/protocols/bgp/protocol.py
Normal file
@ -0,0 +1,87 @@
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Module defines protocol based classes and utils.
|
||||
"""
|
||||
|
||||
from abc import ABCMeta
|
||||
from abc import abstractmethod
|
||||
|
||||
|
||||
class Protocol(object):
|
||||
"""Interface for various protocols.
|
||||
|
||||
Protocol usually encloses a transport/connection/socket to
|
||||
peer/client/server and encodes and decodes communication/messages. Protocol
|
||||
can also maintain any related state machine, protocol message encoding or
|
||||
decoding utilities. This interface identifies minimum methods to support to
|
||||
facilitate or provide hooks to sub-classes to override behavior as
|
||||
appropriate.
|
||||
"""
|
||||
__metaclass__ = ABCMeta
|
||||
|
||||
@abstractmethod
|
||||
def data_received(self, data):
|
||||
"""Handler for date received over connection/transport.
|
||||
|
||||
Here *data* is in raw bytes. This *data* should further be converted to
|
||||
protocol specific messages and as appropriate transition to new state
|
||||
machine state or send appropriate response.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def connection_made(self):
|
||||
"""Called when connection has been established according to protocol.
|
||||
|
||||
This is the right place to do some initialization or sending initial
|
||||
hello messages.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def connection_lost(self, reason):
|
||||
"""Handler called when connection to peer/remote according to protocol
|
||||
has been lost.
|
||||
|
||||
Here we can do any clean-up related to connection/transport/timers/etc.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class Factory(object):
|
||||
"""This is a factory which produces protocols.
|
||||
|
||||
Can also act as context for protocols.
|
||||
"""
|
||||
__metaclass__ = ABCMeta
|
||||
|
||||
# Put a subclass of Protocol here:
|
||||
protocol = None
|
||||
|
||||
@abstractmethod
|
||||
def build_protocol(self, socket):
|
||||
"""Create an instance of a subclass of Protocol.
|
||||
|
||||
Override this method to alter how Protocol instances get created.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
def start_protocol(self, socket):
|
||||
"""Launch protocol instance to handle input on an incoming connection.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
7
ryu/services/protocols/bgp/protocols/bgp/__init__.py
Normal file
7
ryu/services/protocols/bgp/protocols/bgp/__init__.py
Normal file
@ -0,0 +1,7 @@
|
||||
try:
|
||||
from collections import OrderedDict
|
||||
except ImportError:
|
||||
from ordereddict import OrderedDict
|
||||
|
||||
# Pointer to active/available OrderedDict.
|
||||
OrderedDict = OrderedDict
|
||||
280
ryu/services/protocols/bgp/protocols/bgp/capabilities.py
Normal file
280
ryu/services/protocols/bgp/protocols/bgp/capabilities.py
Normal file
@ -0,0 +1,280 @@
|
||||
# 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.
|
||||
|
||||
"""
|
||||
This module provides BGP protocol capabilities classes and utility methods to
|
||||
encode and decode them.
|
||||
"""
|
||||
|
||||
from abc import ABCMeta
|
||||
from abc import abstractmethod
|
||||
import logging
|
||||
import struct
|
||||
|
||||
from ryu.services.protocols.bgp.protocols.bgp.exceptions import \
|
||||
MalformedOptionalParam
|
||||
from ryu.services.protocols.bgp.protocols.bgp.nlri import get_rf
|
||||
from ryu.services.protocols.bgp.protocols.bgp.nlri import \
|
||||
RouteFamily as route_fmly
|
||||
|
||||
|
||||
# Logger instance for this module
|
||||
LOG = logging.getLogger('bgpspeaker.bgp.proto.capabilities')
|
||||
|
||||
# Registry for bgp capability class by their code.
|
||||
# <Key>: <Value> - <capability-code>: <capability-class>
|
||||
_BGP_CAPABILITY_REGISTRY = {}
|
||||
|
||||
|
||||
def _register_bgp_capabilities(cls):
|
||||
"""Utility decorator used to register bgp supported/recognized
|
||||
capabilities.
|
||||
|
||||
Capabilities classes are registered by their capability-code.
|
||||
"""
|
||||
assert issubclass(cls, Capability)
|
||||
assert hasattr(cls, 'CODE')
|
||||
assert _BGP_CAPABILITY_REGISTRY.get(cls.CODE) is None
|
||||
_BGP_CAPABILITY_REGISTRY[cls.CODE] = cls
|
||||
return cls
|
||||
|
||||
|
||||
def is_recognized_cap_codes(cap_code):
|
||||
return cap_code in _BGP_CAPABILITY_REGISTRY
|
||||
|
||||
|
||||
def decode(byte_value):
|
||||
"""Decodes given `byte_value` into appropriate capabilities.
|
||||
|
||||
Parameter:
|
||||
- `byte_value`: (str) byte representation of one capability
|
||||
advertisement
|
||||
Returns:
|
||||
- list of capabilities decoded from given bytes
|
||||
Note: Different routers pack capability in one capability
|
||||
advertisement/optional parameter or group them into several capability
|
||||
advertisements. Hence we return a list of one or more decoded
|
||||
capabilities.
|
||||
"""
|
||||
idx = 0
|
||||
total_len = len(byte_value)
|
||||
caps = []
|
||||
# Parse one of more capabilities packed inside given capability-
|
||||
# advertisement payload
|
||||
while idx < total_len:
|
||||
cap_code, clen = struct.unpack_from('BB', byte_value, idx)
|
||||
idx += 2
|
||||
cap = byte_value[idx:idx + clen]
|
||||
idx += clen
|
||||
|
||||
cap_cls = _BGP_CAPABILITY_REGISTRY.get(cap_code)
|
||||
if cap_cls:
|
||||
cap = cap_cls.from_bytes(cap)
|
||||
caps.append(cap)
|
||||
else:
|
||||
# RFC 5492 says: If a BGP speaker receives from its peer a
|
||||
# capability that it does not itself support or recognize, it MUST
|
||||
# ignore that capability. In particular, the Unsupported
|
||||
# Capability NOTIFICATION message MUST NOT be generated and the BGP
|
||||
# session MUST NOT be terminated in response to reception of a
|
||||
# capability that is not supported by the local speaker.
|
||||
cap = UnSupportedCap(cap_code, cap)
|
||||
|
||||
return caps
|
||||
|
||||
|
||||
class Capability(object):
|
||||
"""Super class of all bgp capability optional parameters.
|
||||
"""
|
||||
__metaclass__ = ABCMeta
|
||||
CODE = -1
|
||||
NAME = 'abstract-cap'
|
||||
|
||||
@abstractmethod
|
||||
def packvalue(self):
|
||||
"""Encode this bgp capability."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def encode(self):
|
||||
"""Encodes this bgp capability with header and body."""
|
||||
body = self.packvalue()
|
||||
return struct.pack('BB', self.__class__.CODE, len(body)) + body
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s>' % self.__class__.NAME
|
||||
|
||||
|
||||
class UnSupportedCap(Capability):
|
||||
"""Represents unknown capability.
|
||||
|
||||
According to RFC 5492 it is recommended to that we do not sent NOTIFICATION
|
||||
message for "Unsupported Capability".
|
||||
"""
|
||||
NAME = 'unsupported-cap'
|
||||
|
||||
def __init__(self, code, value):
|
||||
self.CODE = code
|
||||
self._value = value
|
||||
|
||||
def packvalue(self):
|
||||
return self._value
|
||||
|
||||
def __repr__(self):
|
||||
return '<UnSupportedCap(code=%s)>' % self.CODE
|
||||
|
||||
|
||||
@_register_bgp_capabilities
|
||||
class MultiprotocolExtentionCap(Capability):
|
||||
"""This class represents bgp multi-protocol extension capability.
|
||||
"""
|
||||
CODE = 1
|
||||
NAME = 'mbgp'
|
||||
|
||||
def __init__(self, route_family):
|
||||
if not route_fmly.is_valid(route_family):
|
||||
raise ValueError('Invalid argument %s' % route_family)
|
||||
|
||||
Capability.__init__(self)
|
||||
self.route_family = route_family
|
||||
|
||||
def packvalue(self):
|
||||
return struct.pack('!HH', self.route_family.afi,
|
||||
self.route_family.safi)
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, value):
|
||||
afi, _, safi = struct.unpack_from('!HBB', value)
|
||||
return cls(get_rf(afi, safi))
|
||||
|
||||
def __repr__(self):
|
||||
return ('<MultiprotocolExtenstionCap(af=%s, saf=%s)>' %
|
||||
(self.route_family.afi, self.route_family.safi))
|
||||
|
||||
def __eq__(self, other):
|
||||
if (other.__class__.CODE == self.__class__.CODE and
|
||||
other.route_family.afi == self.route_family.afi and
|
||||
other.route_family.safi == self.route_family.safi):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class ZeroLengthCap(Capability):
|
||||
"""This is a super class represent all bgp capability with zero length."""
|
||||
CODE = -1
|
||||
NAME = 'zero-length'
|
||||
|
||||
def packvalue(self):
|
||||
return ''
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, value):
|
||||
if len(value) > 0:
|
||||
LOG.error('Zero length capability has non-zero length value!')
|
||||
raise MalformedOptionalParam()
|
||||
return cls.get_singleton()
|
||||
|
||||
@staticmethod
|
||||
def get_singleton():
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
@_register_bgp_capabilities
|
||||
class RouteRefreshCap(ZeroLengthCap):
|
||||
CODE = 2
|
||||
NAME = 'route-refresh'
|
||||
|
||||
def __str__(self):
|
||||
return RouteRefreshCap.NAME
|
||||
|
||||
@staticmethod
|
||||
def get_singleton():
|
||||
return _ROUTE_REFRESH_CAP
|
||||
|
||||
|
||||
@_register_bgp_capabilities
|
||||
class OldRouteRefreshCap(ZeroLengthCap):
|
||||
CODE = 128
|
||||
NAME = 'old-route-refresh'
|
||||
|
||||
def __str__(self):
|
||||
return OldRouteRefreshCap.NAME
|
||||
|
||||
@staticmethod
|
||||
def get_singleton():
|
||||
return _OLD_ROUTE_REFRESH_CAP
|
||||
|
||||
|
||||
# Since four byte as capability is not fully supported, we do not register it
|
||||
# as supported/recognized capability.
|
||||
@_register_bgp_capabilities
|
||||
class GracefulRestartCap(Capability):
|
||||
CODE = 64
|
||||
NAME = 'graceful-restart'
|
||||
|
||||
def __init__(self, value):
|
||||
# TODO(PH): Provide implementation
|
||||
Capability.__init__(self)
|
||||
self.value = value
|
||||
|
||||
def packvalue(self):
|
||||
# TODO(PH): Provide implementation
|
||||
return self.value
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, value):
|
||||
return cls(value)
|
||||
|
||||
|
||||
# Since four byte as capability is not fully supported, we do not register it
|
||||
# as supported/recognized capability.
|
||||
@_register_bgp_capabilities
|
||||
class FourByteAsCap(Capability):
|
||||
CODE = 65
|
||||
NAME = '4byteas'
|
||||
|
||||
def __init__(self, four_byte_as):
|
||||
Capability.__init__(self)
|
||||
self.four_byte_as = four_byte_as
|
||||
|
||||
def packvalue(self):
|
||||
return struct.pack('!I', self.four_byte_as)
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, value):
|
||||
value, = struct.unpack('!I', value)
|
||||
return cls(value)
|
||||
|
||||
def __repr__(self):
|
||||
return '<FourByteAsCap(%s)>' % self.four_byte_as
|
||||
|
||||
def __eq__(self, other):
|
||||
if (other and other.four_byte_as == self.four_byte_as):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
@_register_bgp_capabilities
|
||||
class EnhancedRouteRefreshCap(ZeroLengthCap):
|
||||
CODE = 70
|
||||
NAME = 'enhanced-refresh'
|
||||
|
||||
@staticmethod
|
||||
def get_singleton():
|
||||
return _ENHANCED_ROUTE_REFRESH_CAP
|
||||
|
||||
# Zero length capability singletons
|
||||
_ROUTE_REFRESH_CAP = RouteRefreshCap()
|
||||
_ENHANCED_ROUTE_REFRESH_CAP = EnhancedRouteRefreshCap()
|
||||
_OLD_ROUTE_REFRESH_CAP = OldRouteRefreshCap()
|
||||
349
ryu/services/protocols/bgp/protocols/bgp/exceptions.py
Normal file
349
ryu/services/protocols/bgp/protocols/bgp/exceptions.py
Normal file
@ -0,0 +1,349 @@
|
||||
import struct
|
||||
|
||||
|
||||
class BgpExc(Exception):
|
||||
"""Base bgp exception."""
|
||||
|
||||
CODE = 0
|
||||
"""BGP error code."""
|
||||
|
||||
SUB_CODE = 0
|
||||
"""BGP error sub-code."""
|
||||
|
||||
SEND_ERROR = True
|
||||
"""Flag if set indicates Notification message should be sent to peer."""
|
||||
|
||||
def __init__(self, data=''):
|
||||
self.data = data
|
||||
|
||||
def __str__(self):
|
||||
return '<%s %r>' % (self.__class__.__name__, self.data)
|
||||
|
||||
|
||||
class BadNotification(BgpExc):
|
||||
SEND_ERROR = False
|
||||
|
||||
#=============================================================================
|
||||
# Message Header Errors
|
||||
#=============================================================================
|
||||
|
||||
|
||||
class NotSync(BgpExc):
|
||||
CODE = 1
|
||||
SUB_CODE = 1
|
||||
|
||||
|
||||
class BadLen(BgpExc):
|
||||
CODE = 1
|
||||
SUB_CODE = 2
|
||||
|
||||
def __init__(self, msg_type_code, message_length):
|
||||
self.msg_type_code = msg_type_code
|
||||
self.length = message_length
|
||||
self.data = struct.pack('!H', self.length)
|
||||
|
||||
def __str__(self):
|
||||
return '<BadLen %d msgtype=%d>' % (self.length, self.msg_type_code)
|
||||
|
||||
|
||||
class BadMsg(BgpExc):
|
||||
"""Error to indicate un-recognized message type.
|
||||
|
||||
RFC says: If the Type field of the message header is not recognized, then
|
||||
the Error Subcode MUST be set to Bad Message Type. The Data field MUST
|
||||
contain the erroneous Type field.
|
||||
"""
|
||||
CODE = 1
|
||||
SUB_CODE = 3
|
||||
|
||||
def __init__(self, msg_type):
|
||||
self.msg_type = msg_type
|
||||
self.data = struct.pack('B', msg_type)
|
||||
|
||||
def __str__(self):
|
||||
return '<BadMsg %d>' % (self.msg,)
|
||||
|
||||
#=============================================================================
|
||||
# OPEN Message Errors
|
||||
#=============================================================================
|
||||
|
||||
|
||||
class MalformedOptionalParam(BgpExc):
|
||||
"""If recognized optional parameters are malformed.
|
||||
|
||||
RFC says: If one of the Optional Parameters in the OPEN message is
|
||||
recognized, but is malformed, then the Error Subcode MUST be set to 0
|
||||
(Unspecific).
|
||||
"""
|
||||
CODE = 2
|
||||
SUB_CODE = 0
|
||||
|
||||
|
||||
class UnsupportedVersion(BgpExc):
|
||||
"""Error to indicate unsupport bgp version number.
|
||||
|
||||
RFC says: If the version number in the Version field of the received OPEN
|
||||
message is not supported, then the Error Subcode MUST be set to Unsupported
|
||||
Version Number. The Data field is a 2-octet unsigned integer, which
|
||||
indicates the largest, locally-supported version number less than the
|
||||
version the remote BGP peer bid (as indicated in the received OPEN
|
||||
message), or if the smallest, locally-supported version number is greater
|
||||
than the version the remote BGP peer bid, then the smallest, locally-
|
||||
supported version number.
|
||||
"""
|
||||
CODE = 2
|
||||
SUB_CODE = 1
|
||||
|
||||
def __init__(self, locally_support_version):
|
||||
self.data = struct.pack('H', locally_support_version)
|
||||
|
||||
|
||||
class BadPeerAs(BgpExc):
|
||||
"""Error to indicate open message has incorrect AS number.
|
||||
|
||||
RFC says: If the Autonomous System field of the OPEN message is
|
||||
unacceptable, then the Error Subcode MUST be set to Bad Peer AS. The
|
||||
determination of acceptable Autonomous System numbers is configure peer AS.
|
||||
"""
|
||||
CODE = 2
|
||||
SUB_CODE = 2
|
||||
|
||||
|
||||
class BadBgpId(BgpExc):
|
||||
"""Error to indicate incorrect BGP Identifier.
|
||||
|
||||
RFC says: If the BGP Identifier field of the OPEN message is syntactically
|
||||
incorrect, then the Error Subcode MUST be set to Bad BGP Identifier.
|
||||
Syntactic correctness means that the BGP Identifier field represents a
|
||||
valid unicast IP host address.
|
||||
"""
|
||||
CODE = 2
|
||||
SUB_CODE = 3
|
||||
|
||||
|
||||
class UnsupportedOptParam(BgpExc):
|
||||
"""Error to indicate unsupported optional parameters.
|
||||
|
||||
RFC says: If one of the Optional Parameters in the OPEN message is not
|
||||
recognized, then the Error Subcode MUST be set to Unsupported Optional
|
||||
Parameters.
|
||||
"""
|
||||
CODE = 2
|
||||
SUB_CODE = 4
|
||||
|
||||
|
||||
class AuthFailure(BgpExc):
|
||||
CODE = 2
|
||||
SUB_CODE = 5
|
||||
|
||||
|
||||
class UnacceptableHoldTime(BgpExc):
|
||||
"""Error to indicate Unacceptable Hold Time in open message.
|
||||
|
||||
RFC says: If the Hold Time field of the OPEN message is unacceptable, then
|
||||
the Error Subcode MUST be set to Unacceptable Hold Time.
|
||||
"""
|
||||
CODE = 2
|
||||
SUB_CODE = 6
|
||||
|
||||
#=============================================================================
|
||||
# UPDATE message related errors
|
||||
#=============================================================================
|
||||
|
||||
|
||||
class MalformedAttrList(BgpExc):
|
||||
"""Error to indicate UPDATE message is malformed.
|
||||
|
||||
RFC says: Error checking of an UPDATE message begins by examining the path
|
||||
attributes. If the Withdrawn Routes Length or Total Attribute Length is
|
||||
too large (i.e., if Withdrawn Routes Length + Total Attribute Length + 23
|
||||
exceeds the message Length), then the Error Subcode MUST be set to
|
||||
Malformed Attribute List.
|
||||
"""
|
||||
CODE = 3
|
||||
SUB_CODE = 1
|
||||
|
||||
|
||||
class UnRegWellKnowAttr(BgpExc):
|
||||
CODE = 3
|
||||
SUB_CODE = 2
|
||||
|
||||
|
||||
class MissingWellKnown(BgpExc):
|
||||
"""Error to indicate missing well-known attribute.
|
||||
|
||||
RFC says: If any of the well-known mandatory attributes are not present,
|
||||
then the Error Subcode MUST be set to Missing Well-known Attribute. The
|
||||
Data field MUST contain the Attribute Type Code of the missing, well-known
|
||||
attribute.
|
||||
"""
|
||||
CODE = 3
|
||||
SUB_CODE = 3
|
||||
|
||||
def __init__(self, pattr_type_code):
|
||||
self.pattr_type_code = pattr_type_code
|
||||
self.data = struct.pack('B', pattr_type_code)
|
||||
|
||||
|
||||
class AttrFlagError(BgpExc):
|
||||
"""Error to indicate recognized path attributes have incorrect flags.
|
||||
|
||||
RFC says: If any recognized attribute has Attribute Flags that conflict
|
||||
with the Attribute Type Code, then the Error Subcode MUST be set to
|
||||
Attribute Flags Error. The Data field MUST contain the erroneous attribute
|
||||
(type, length, and value).
|
||||
"""
|
||||
CODE = 3
|
||||
SUB_CODE = 4
|
||||
|
||||
|
||||
class AttrLenError(BgpExc):
|
||||
CODE = 3
|
||||
SUB_CODE = 5
|
||||
|
||||
|
||||
class InvalidOriginError(BgpExc):
|
||||
"""Error indicates undefined Origin attribute value.
|
||||
|
||||
RFC says: If the ORIGIN attribute has an undefined value, then the Error
|
||||
Sub- code MUST be set to Invalid Origin Attribute. The Data field MUST
|
||||
contain the unrecognized attribute (type, length, and value).
|
||||
"""
|
||||
CODE = 3
|
||||
SUB_CODE = 6
|
||||
|
||||
|
||||
class RoutingLoop(BgpExc):
|
||||
CODE = 3
|
||||
SUB_CODE = 7
|
||||
|
||||
|
||||
class InvalidNextHop(BgpExc):
|
||||
CODE = 3
|
||||
SUB_CODE = 8
|
||||
|
||||
|
||||
class OptAttrError(BgpExc):
|
||||
"""Error indicates Optional Attribute is malformed.
|
||||
|
||||
RFC says: If an optional attribute is recognized, then the value of this
|
||||
attribute MUST be checked. If an error is detected, the attribute MUST be
|
||||
discarded, and the Error Subcode MUST be set to Optional Attribute Error.
|
||||
The Data field MUST contain the attribute (type, length, and value).
|
||||
"""
|
||||
CODE = 3
|
||||
SUB_CODE = 9
|
||||
|
||||
|
||||
class InvalidNetworkField(BgpExc):
|
||||
CODE = 3
|
||||
SUB_CODE = 10
|
||||
|
||||
|
||||
class MalformedAsPath(BgpExc):
|
||||
"""Error to indicate if AP_PATH attribute is syntactically incorrect.
|
||||
|
||||
RFC says: The AS_PATH attribute is checked for syntactic correctness. If
|
||||
the path is syntactically incorrect, then the Error Subcode MUST be set to
|
||||
Malformed AS_PATH.
|
||||
"""
|
||||
CODE = 3
|
||||
SUB_CODE = 11
|
||||
|
||||
|
||||
#=============================================================================
|
||||
# Hold Timer Expired
|
||||
#=============================================================================
|
||||
|
||||
|
||||
class HoldTimerExpired(BgpExc):
|
||||
"""Error to indicate Hold Timer expired.
|
||||
|
||||
RFC says: If a system does not receive successive KEEPALIVE, UPDATE, and/or
|
||||
NOTIFICATION messages within the period specified in the Hold Time field of
|
||||
the OPEN message, then the NOTIFICATION message with the Hold Timer Expired
|
||||
Error Code is sent and the BGP connection is closed.
|
||||
"""
|
||||
CODE = 4
|
||||
SUB_CODE = 1
|
||||
|
||||
#=============================================================================
|
||||
# Finite State Machine Error
|
||||
#=============================================================================
|
||||
|
||||
|
||||
class FiniteStateMachineError(BgpExc):
|
||||
"""Error to indicate any Finite State Machine Error.
|
||||
|
||||
RFC says: Any error detected by the BGP Finite State Machine (e.g., receipt
|
||||
of an unexpected event) is indicated by sending the NOTIFICATION message
|
||||
with the Error Code Finite State Machine Error.
|
||||
"""
|
||||
CODE = 5
|
||||
SUB_CODE = 1
|
||||
|
||||
|
||||
#=============================================================================
|
||||
# Cease Errors
|
||||
#=============================================================================
|
||||
|
||||
class MaxPrefixReached(BgpExc):
|
||||
CODE = 6
|
||||
SUB_CODE = 1
|
||||
|
||||
|
||||
class AdminShutdown(BgpExc):
|
||||
"""Error to indicate Administrative shutdown.
|
||||
|
||||
RFC says: If a BGP speaker decides to administratively shut down its
|
||||
peering with a neighbor, then the speaker SHOULD send a NOTIFICATION
|
||||
message with the Error Code Cease and the Error Subcode 'Administrative
|
||||
Shutdown'.
|
||||
"""
|
||||
CODE = 6
|
||||
SUB_CODE = 2
|
||||
|
||||
|
||||
class PeerDeConfig(BgpExc):
|
||||
CODE = 6
|
||||
SUB_CODE = 3
|
||||
|
||||
|
||||
class AdminReset(BgpExc):
|
||||
CODE = 6
|
||||
SUB_CODE = 4
|
||||
|
||||
|
||||
class ConnRejected(BgpExc):
|
||||
"""Error to indicate Connection Rejected.
|
||||
|
||||
RFC says: If a BGP speaker decides to disallow a BGP connection (e.g., the
|
||||
peer is not configured locally) after the speaker accepts a transport
|
||||
protocol connection, then the BGP speaker SHOULD send a NOTIFICATION
|
||||
message with the Error Code Cease and the Error Subcode "Connection
|
||||
Rejected".
|
||||
"""
|
||||
CODE = 6
|
||||
SUB_CODE = 5
|
||||
|
||||
|
||||
class OtherConfChange(BgpExc):
|
||||
CODE = 6
|
||||
SUB_CODE = 6
|
||||
|
||||
|
||||
class CollisionResolution(BgpExc):
|
||||
"""Error to indicate Connection Collision Resolution.
|
||||
|
||||
RFC says: If a BGP speaker decides to send a NOTIFICATION message with the
|
||||
Error Code Cease as a result of the collision resolution procedure (as
|
||||
described in [BGP-4]), then the subcode SHOULD be set to "Connection
|
||||
Collision Resolution".
|
||||
"""
|
||||
CODE = 6
|
||||
SUB_CODE = 7
|
||||
|
||||
|
||||
class OutOfResource(BgpExc):
|
||||
CODE = 6
|
||||
SUB_CODE = 8
|
||||
540
ryu/services/protocols/bgp/protocols/bgp/messages.py
Normal file
540
ryu/services/protocols/bgp/protocols/bgp/messages.py
Normal file
@ -0,0 +1,540 @@
|
||||
# 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.
|
||||
|
||||
"""
|
||||
This module provides BGP protocol message classes and utility methods to encode
|
||||
and decode them.
|
||||
|
||||
This file is adapted from pybgp open source project.
|
||||
"""
|
||||
from abc import ABCMeta
|
||||
from abc import abstractmethod
|
||||
from copy import copy
|
||||
import cStringIO
|
||||
import logging
|
||||
import socket
|
||||
import struct
|
||||
|
||||
from ryu.services.protocols.bgp.protocols.bgp import capabilities
|
||||
from ryu.services.protocols.bgp.protocols.bgp.exceptions import BadBgpId
|
||||
from ryu.services.protocols.bgp.protocols.bgp.exceptions import BadLen
|
||||
from ryu.services.protocols.bgp.protocols.bgp.exceptions import BadMsg
|
||||
from ryu.services.protocols.bgp.protocols.bgp.exceptions import BadNotification
|
||||
from ryu.services.protocols.bgp.protocols.bgp.exceptions import \
|
||||
MalformedAttrList
|
||||
from ryu.services.protocols.bgp.protocols.bgp.exceptions import \
|
||||
UnacceptableHoldTime
|
||||
from ryu.services.protocols.bgp.protocols.bgp import nlri
|
||||
from ryu.services.protocols.bgp.protocols.bgp.nlri import get_rf
|
||||
from ryu.services.protocols.bgp.protocols.bgp import OrderedDict
|
||||
from ryu.services.protocols.bgp.protocols.bgp import pathattr
|
||||
from ryu.services.protocols.bgp.utils.validation import is_valid_ipv4
|
||||
from ryu.services.protocols.bgp.utils.validation import is_valid_old_asn
|
||||
|
||||
|
||||
LOG = logging.getLogger('bgpspeaker.bgp.proto.messages')
|
||||
|
||||
# BGP capability optional parameter type
|
||||
CAP_OPT_PARA_TYPE = 2
|
||||
|
||||
# Registry for bgp message class by their type code.
|
||||
# <key>: <value> - <type-code>: <msg class>
|
||||
_BGP_MESSAGE_REGISTRY = {}
|
||||
|
||||
|
||||
def _register_bgp_message(cls):
|
||||
"""Used as class decorator for registering bgp message class by their
|
||||
type-code.
|
||||
"""
|
||||
assert _BGP_MESSAGE_REGISTRY.get(cls.TYPE_CODE) is None
|
||||
assert hasattr(cls, 'from_bytes')
|
||||
_BGP_MESSAGE_REGISTRY[cls.TYPE_CODE] = cls
|
||||
return cls
|
||||
|
||||
|
||||
class BgpMessage(object):
|
||||
"""Super class of all bgp messages.
|
||||
"""
|
||||
__metaclass__ = ABCMeta
|
||||
TYPE_CODE = 0
|
||||
MSG_NAME = 'abstract-msg'
|
||||
HEADER_SIZE = 19
|
||||
|
||||
@abstractmethod
|
||||
def packvalue(self):
|
||||
"""Encodes the body of this bgp message."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def encode(self):
|
||||
"""Encodes this bgp message with header and body."""
|
||||
body = self.packvalue()
|
||||
return struct.pack('!16sHB', '\xff' * 16, 19 + len(body),
|
||||
self.__class__.TYPE_CODE) + body
|
||||
|
||||
|
||||
class RecognizedBgpMessage(BgpMessage):
|
||||
"""Represents recognized/supported bgp message.
|
||||
|
||||
Declares a factory method to create an instance from bytes.
|
||||
"""
|
||||
@classmethod
|
||||
def from_bytes(cls, recv_bytes, total_msg_length):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
@_register_bgp_message
|
||||
class Open(RecognizedBgpMessage):
|
||||
"""Represents bgp OPEN message.
|
||||
|
||||
This is the first message sent by each peer after TCP connection is
|
||||
established.
|
||||
"""
|
||||
MSG_NAME = 'open'
|
||||
TYPE_CODE = 1
|
||||
MIN_LENGTH = 29
|
||||
|
||||
def __init__(self, version, asnum, holdtime, bgpid, caps,
|
||||
unrec_params=None):
|
||||
# Validate arguments.
|
||||
if version < 1:
|
||||
raise ValueError('Invalid version number %s' % version)
|
||||
if not is_valid_old_asn(asnum):
|
||||
raise ValueError('Invalid AS number %s' % asnum)
|
||||
if holdtime <= 2:
|
||||
raise ValueError('Holdtime has to be greater than 2 sec.')
|
||||
if not caps:
|
||||
raise ValueError('Invalid capabilities.')
|
||||
if not is_valid_ipv4(bgpid):
|
||||
raise ValueError('Invalid bgp ID, should be valid IPv4, '
|
||||
'but given %s' % bgpid)
|
||||
|
||||
BgpMessage.__init__(self)
|
||||
self._version = version
|
||||
self._holdtime = holdtime
|
||||
self._asnum = asnum
|
||||
self._bgpid = bgpid
|
||||
self._caps = caps
|
||||
self._unrec_params = unrec_params
|
||||
if not unrec_params:
|
||||
self._unrec_params = OrderedDict()
|
||||
|
||||
@property
|
||||
def version(self):
|
||||
return self._version
|
||||
|
||||
@property
|
||||
def holdtime(self):
|
||||
return self._holdtime
|
||||
|
||||
@property
|
||||
def asnum(self):
|
||||
return self._asnum
|
||||
|
||||
@property
|
||||
def bgpid(self):
|
||||
return self._bgpid
|
||||
|
||||
@property
|
||||
def caps(self):
|
||||
return copy(self._caps)
|
||||
|
||||
@property
|
||||
def unrec_params(self):
|
||||
return copy(self._unrec_params)
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, recv_bytes, total_msg_len):
|
||||
# Validate OPEN message length.
|
||||
if len(recv_bytes) < 10:
|
||||
raise BadLen(Open.TYPE_CODE, len(recv_bytes) + cls.HEADER_SIZE)
|
||||
|
||||
version, asnum, holdtime, bgpid, paramlen = \
|
||||
struct.unpack_from('!BHH4sB', recv_bytes)
|
||||
|
||||
if len(recv_bytes) != (10 + paramlen):
|
||||
# TODO(PH): Check what RFC says to do here.
|
||||
LOG.debug('Open message: too short.')
|
||||
|
||||
offset = 10
|
||||
|
||||
# BGP implementation MUST reject Hold Time values of one or two
|
||||
# seconds.
|
||||
if holdtime <= 2:
|
||||
raise UnacceptableHoldTime()
|
||||
|
||||
# BGP Identifier field MUST represents a valid unicast IP host address.
|
||||
bgpid = socket.inet_ntoa(bgpid)
|
||||
if not is_valid_ipv4(bgpid):
|
||||
raise BadBgpId()
|
||||
|
||||
# Parse optional parameters.
|
||||
caps = OrderedDict()
|
||||
unrec_params = OrderedDict()
|
||||
while offset < len(recv_bytes):
|
||||
ptype, plen = struct.unpack_from('BB', recv_bytes, offset)
|
||||
offset += 2
|
||||
value = recv_bytes[offset:offset + plen]
|
||||
offset += plen
|
||||
|
||||
# Parse capabilities optional parameter.
|
||||
if ptype == CAP_OPT_PARA_TYPE:
|
||||
bgp_caps = capabilities.decode(value)
|
||||
# store decoded bgp capabilities by their capability-code
|
||||
for cap in bgp_caps:
|
||||
cap_code = cap.CODE
|
||||
if cap_code in caps:
|
||||
caps[cap_code].append(cap)
|
||||
else:
|
||||
caps[cap_code] = [cap]
|
||||
else:
|
||||
# Other unrecognized optional parameters.
|
||||
unrec_params[ptype] = value
|
||||
|
||||
# Un-recognized capabilities are passed on, its up to application to
|
||||
# check if unrec-optional-paramters are a problem and send NOTIFICATION
|
||||
return cls(version, asnum, holdtime, bgpid, caps, unrec_params)
|
||||
|
||||
def packvalue(self):
|
||||
params = cStringIO.StringIO()
|
||||
# Capabilities optional parameters.
|
||||
for capability in self.caps.itervalues():
|
||||
for cap in capability:
|
||||
encoded_cap = cStringIO.StringIO()
|
||||
encoded_cap.write(cap.encode())
|
||||
encoded_cap_value = encoded_cap.getvalue()
|
||||
encoded_cap.close()
|
||||
params.write(struct.pack('BB',
|
||||
CAP_OPT_PARA_TYPE,
|
||||
len(encoded_cap_value)))
|
||||
params.write(encoded_cap_value)
|
||||
|
||||
# Other optional parameters.
|
||||
for ptype, pvalue in self.unrec_params.items():
|
||||
params.write(struct.pack('BB', ptype, len(pvalue)))
|
||||
params.write(pvalue)
|
||||
|
||||
bgpid = socket.inet_aton(self.bgpid)
|
||||
params_value = params.getvalue()
|
||||
params.close()
|
||||
return struct.pack('!BHH4sB', self.version, self.asnum, self.holdtime,
|
||||
bgpid, len(params_value)) + params_value
|
||||
|
||||
def __str__(self):
|
||||
str_rep = cStringIO.StringIO()
|
||||
str_rep.write('Open message Ver=%s As#=%s Hold Time=%s Bgp Id=%s' %
|
||||
(self.version, self.asnum, self.holdtime, self.bgpid))
|
||||
for param, value in self.unrec_params.items():
|
||||
str_rep.write(' unrec_param %s=%r' % (param, value))
|
||||
for cap, value in self.caps.items():
|
||||
str_rep.write(' cap %s=%r' % (cap, value))
|
||||
return str_rep.getvalue()
|
||||
|
||||
|
||||
@_register_bgp_message
|
||||
class Keepalive(BgpMessage):
|
||||
MSG_NAME = 'keepalive'
|
||||
TYPE_CODE = 4
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, recv_bytes, total_msg_len):
|
||||
# Validate KeepAlive msg. length
|
||||
if len(recv_bytes):
|
||||
LOG.info("Received keepalive msg. with data! %r" % (recv_bytes,))
|
||||
raise BadLen(
|
||||
Keepalive.TYPE_CODE,
|
||||
len(recv_bytes) + cls.HEADER_SIZE
|
||||
)
|
||||
|
||||
self = cls()
|
||||
return self
|
||||
|
||||
def packvalue(self):
|
||||
return ''
|
||||
|
||||
def __str__(self):
|
||||
return 'Keepalive message'
|
||||
|
||||
|
||||
@_register_bgp_message
|
||||
class RouteRefresh(BgpMessage):
|
||||
MSG_NAME = 'route-refresh'
|
||||
TYPE_CODE = 5
|
||||
|
||||
def __init__(self, route_family, demarcation=0):
|
||||
BgpMessage.__init__(self)
|
||||
self._route_family = route_family
|
||||
self._demarcation = demarcation
|
||||
self.eor_sent = False
|
||||
|
||||
@property
|
||||
def route_family(self):
|
||||
return self._route_family
|
||||
|
||||
@property
|
||||
def demarcation(self):
|
||||
return self._demarcation
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, recv_bytes, total_msg_len):
|
||||
# Validate length of RouteRefresh message.
|
||||
if len(recv_bytes) != 4:
|
||||
raise BadLen(
|
||||
RouteRefresh.TYPE_CODE,
|
||||
len(recv_bytes) + cls.HEADER_SIZE
|
||||
)
|
||||
|
||||
afi, reserved, safi = struct.unpack_from('!HBB', recv_bytes)
|
||||
route_family = get_rf(afi, safi)
|
||||
return cls(route_family, reserved)
|
||||
|
||||
def packvalue(self):
|
||||
return struct.pack('!HBB', self.route_family.afi, self.demarcation,
|
||||
self._route_family.safi)
|
||||
|
||||
def __str__(self):
|
||||
return 'Route-refresh message (%s, %s)' % \
|
||||
(self.route_family, self.demarcation)
|
||||
|
||||
|
||||
@_register_bgp_message
|
||||
class Notification(BgpMessage):
|
||||
MSG_NAME = 'notification'
|
||||
TYPE_CODE = 3
|
||||
REASONS = {
|
||||
(1, 1): 'Message Header Error: not synchronised',
|
||||
(1, 2): 'Message Header Error: bad message len',
|
||||
(1, 3): 'Message Header Error: bad message type',
|
||||
(2, 1): 'Open Message Error: unsupported version',
|
||||
(2, 2): 'Open Message Error: bad peer AS',
|
||||
(2, 3): 'Open Message Error: bad BGP identifier',
|
||||
(2, 4): 'Open Message Error: unsupported optional param',
|
||||
(2, 5): 'Open Message Error: authentication failure',
|
||||
(2, 6): 'Open Message Error: unacceptable hold time',
|
||||
(2, 7): 'Open Message Error: Unsupported Capability',
|
||||
(2, 8): 'Open Message Error: Unassigned',
|
||||
(3, 1): 'Update Message Error: malformed attribute list',
|
||||
(3, 2): 'Update Message Error: unrecognized well-known attr',
|
||||
(3, 3): 'Update Message Error: missing well-known attr',
|
||||
(3, 4): 'Update Message Error: attribute flags error',
|
||||
(3, 5): 'Update Message Error: attribute length error',
|
||||
(3, 6): 'Update Message Error: invalid origin attr',
|
||||
(3, 7): 'Update Message Error: as routing loop',
|
||||
(3, 8): 'Update Message Error: invalid next hop attr',
|
||||
(3, 9): 'Update Message Error: optional attribute error',
|
||||
(3, 10): 'Update Message Error: invalid network field',
|
||||
(3, 11): 'Update Message Error: malformed AS_PATH',
|
||||
(4, 1): 'Hold Timer Expired',
|
||||
(5, 1): 'Finite State Machine Error',
|
||||
(6, 1): 'Cease: Maximum Number of Prefixes Reached',
|
||||
(6, 2): 'Cease: Administrative Shutdown',
|
||||
(6, 3): 'Cease: Peer De-configured',
|
||||
(6, 4): 'Cease: Administrative Reset',
|
||||
(6, 5): 'Cease: Connection Rejected',
|
||||
(6, 6): 'Cease: Other Configuration Change',
|
||||
(6, 7): 'Cease: Connection Collision Resolution',
|
||||
(6, 8): 'Cease: Out of Resources',
|
||||
}
|
||||
|
||||
def __init__(self, code, subcode, data=''):
|
||||
BgpMessage.__init__(self)
|
||||
self._code = code
|
||||
self._subcode = subcode
|
||||
self._data = data
|
||||
|
||||
@property
|
||||
def code(self):
|
||||
return self._code
|
||||
|
||||
@property
|
||||
def subcode(self):
|
||||
return self._subcode
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
return self._data
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, recv_bytes, total_msg_len):
|
||||
# Validate NOTIFICATION msg. length.
|
||||
if len(recv_bytes) < 2:
|
||||
LOG.error('Received NOTIFICATION msg. with bad length %s' %
|
||||
(len(recv_bytes) + cls.HEADER_SIZE))
|
||||
raise BadNotification()
|
||||
|
||||
code, subcode = struct.unpack_from('BB', recv_bytes)
|
||||
data = recv_bytes[2:]
|
||||
|
||||
# Check code or sub-code are recognized.
|
||||
if not Notification.REASONS.get((code, subcode)):
|
||||
LOG.error('Received notification msg. with unrecognized Error '
|
||||
'code or Sub-code (%s, %s)' % (code, subcode))
|
||||
raise BadNotification()
|
||||
|
||||
return cls(code, subcode, data)
|
||||
|
||||
def __str__(self):
|
||||
c, s = self.code, self.subcode
|
||||
if (c, s) in self.REASONS:
|
||||
return ('Notification "%s" params %r' %
|
||||
(self.REASONS[c, s], self.data))
|
||||
return ('Notification message code=%d subcode=%d params=%r' %
|
||||
(self.code, self.subcode, self.data))
|
||||
|
||||
def packvalue(self):
|
||||
v = struct.pack('BB', self.code, self.subcode)
|
||||
if self.data:
|
||||
v += self.data
|
||||
return v
|
||||
|
||||
|
||||
@_register_bgp_message
|
||||
class Update(BgpMessage):
|
||||
MSG_NAME = 'update'
|
||||
TYPE_CODE = 2
|
||||
WITHDRAW_NLRI = 'withdraw_nlri'
|
||||
PATH_ATTR_AND_NLRI = 'path_attr_and_nlri'
|
||||
MIN_LENGTH = 23
|
||||
|
||||
def __init__(self, pathattr_map=None, nlri_list=None, withdraw_list=None):
|
||||
"""Initailizes a new `Update` instance.
|
||||
|
||||
Parameter:
|
||||
- `pathattr_map`: (OrderedDict) key -> attribute name,
|
||||
value -> attribute.
|
||||
- `nlri_list`: (list/iterable) NLRIs.
|
||||
- `withdraw_list`: (list/iterable) Withdraw routes.
|
||||
"""
|
||||
if nlri_list is None:
|
||||
nlri_list = []
|
||||
if withdraw_list is None:
|
||||
withdraw_list = []
|
||||
if not pathattr_map:
|
||||
pathattr_map = OrderedDict()
|
||||
|
||||
self._nlri_list = list(nlri_list)
|
||||
self._withdraw_list = list(withdraw_list)
|
||||
self._pathattr_map = copy(pathattr_map)
|
||||
|
||||
@property
|
||||
def nlri_list(self):
|
||||
return self._nlri_list[:]
|
||||
|
||||
@property
|
||||
def withdraw_list(self):
|
||||
return self._withdraw_list[:]
|
||||
|
||||
@property
|
||||
def pathattr_map(self):
|
||||
return copy(self._pathattr_map)
|
||||
|
||||
def get_path_attr(self, attr_name):
|
||||
return self._pathattr_map.get(attr_name)
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, recv_bytes, total_msg_len):
|
||||
# Validate UPDATE message length
|
||||
if len(recv_bytes) < 4:
|
||||
raise BadLen(Update.TYPE_CODE, len(recv_bytes) + cls.HEADER_SIZE)
|
||||
withdraw_list = None
|
||||
nlri_list = None
|
||||
pathattr_map = OrderedDict()
|
||||
|
||||
d = {}
|
||||
idx = 0
|
||||
# Compute withdraw route length + total attribute length.
|
||||
recv_len = 0
|
||||
for kind in (Update.WITHDRAW_NLRI, Update.PATH_ATTR_AND_NLRI):
|
||||
plen, = struct.unpack_from('!H', recv_bytes, idx)
|
||||
idx += 2
|
||||
d[kind] = recv_bytes[idx: (idx + plen)]
|
||||
idx += plen
|
||||
recv_len += plen
|
||||
|
||||
# Check if length of received bytes is valid.
|
||||
if (recv_len + Update.MIN_LENGTH) < total_msg_len:
|
||||
raise MalformedAttrList()
|
||||
|
||||
if d[Update.WITHDRAW_NLRI]:
|
||||
withdraw_list = nlri.parse(d[Update.WITHDRAW_NLRI])
|
||||
# TODO(PH): We have to test how ipv4 nlri packed after path-attr are
|
||||
# getting parsed.
|
||||
nlri_list = nlri.parse(recv_bytes[idx:])
|
||||
|
||||
idx = 0
|
||||
recv_bytes = d[Update.PATH_ATTR_AND_NLRI]
|
||||
while idx < len(recv_bytes):
|
||||
used, pattr = pathattr.decode(recv_bytes, idx)
|
||||
# TODO(PH) Can optimize here by checking if path attribute is
|
||||
# MpReachNlri and stop parsing if RT are not interesting.
|
||||
idx += used
|
||||
pathattr_map[pattr.ATTR_NAME] = pattr
|
||||
|
||||
return cls(pathattr_map=pathattr_map,
|
||||
nlri_list=nlri_list, withdraw_list=withdraw_list)
|
||||
|
||||
def __repr__(self):
|
||||
str_rep = cStringIO.StringIO()
|
||||
str_rep.write('<Update message withdraw=%r' % (self._withdraw_list,))
|
||||
for ptype, pattr in self._pathattr_map.items():
|
||||
str_rep.write('\n path attr %s, %s' % (ptype, pattr,))
|
||||
# if ptype in (MpReachNlri.ATTR_NAME, MpUnreachNlri):
|
||||
# for nnlri in pattr.nlri_list:
|
||||
# str_rep.write('\n nlri=%s' % (nnlri,))
|
||||
for nnlri in self._nlri_list:
|
||||
str_rep.write('\nmp nlri %s' % (nnlri,))
|
||||
|
||||
str_rep.write('>')
|
||||
|
||||
return str_rep.getvalue()
|
||||
|
||||
def __cmp__(self, other):
|
||||
if isinstance(other, Update):
|
||||
return cmp(
|
||||
(self._pathattr_map, self._withdraw_list, self._nlri_list),
|
||||
(other.pathattr_map, other.withdraw_list, other.nlri_list),
|
||||
)
|
||||
return -1
|
||||
|
||||
def packvalue(self):
|
||||
bvalue = ''
|
||||
|
||||
bwithdraw = ''
|
||||
for awithdraw in self._withdraw_list:
|
||||
bwithdraw += awithdraw.encode()
|
||||
|
||||
bvalue += struct.pack('!H', len(bwithdraw))
|
||||
bvalue += bwithdraw
|
||||
|
||||
pattr = ''
|
||||
for _, attr in self._pathattr_map.items():
|
||||
if attr is not None:
|
||||
pattr += attr.encode()
|
||||
bvalue += struct.pack('!H', len(pattr))
|
||||
bvalue += pattr
|
||||
|
||||
for anlri in self._nlri_list:
|
||||
bvalue += anlri.encode()
|
||||
|
||||
return bvalue
|
||||
|
||||
|
||||
def decode(ptype, payload, msg_len):
|
||||
"""Decodes given payload into bgp message instance of given type.
|
||||
"""
|
||||
bgp_msg_class = _BGP_MESSAGE_REGISTRY.get(ptype)
|
||||
if not bgp_msg_class:
|
||||
raise BadMsg(ptype)
|
||||
|
||||
return bgp_msg_class.from_bytes(payload, msg_len)
|
||||
841
ryu/services/protocols/bgp/protocols/bgp/nlri.py
Normal file
841
ryu/services/protocols/bgp/protocols/bgp/nlri.py
Normal file
@ -0,0 +1,841 @@
|
||||
# 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.
|
||||
"""
|
||||
Module related to BGP Network layer reachability information (NLRI).
|
||||
"""
|
||||
|
||||
from abc import ABCMeta
|
||||
import logging
|
||||
import socket
|
||||
import struct
|
||||
from types import IntType
|
||||
|
||||
from ryu.services.protocols.bgp.protocols.bgp.exceptions import OptAttrError
|
||||
from ryu.services.protocols.bgp.utils.other import bytes2hex
|
||||
from ryu.services.protocols.bgp.utils.other import hex2byte
|
||||
from ryu.services.protocols.bgp.utils.validation import is_valid_ext_comm_attr
|
||||
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_prefix
|
||||
from ryu.services.protocols.bgp.utils.validation import is_valid_mpls_label
|
||||
from ryu.services.protocols.bgp.utils.validation import is_valid_old_asn
|
||||
from ryu.services.protocols.bgp.utils.validation import is_valid_route_disc
|
||||
|
||||
|
||||
LOG = logging.getLogger('protocols.bgp.nlri')
|
||||
|
||||
# Registry for bgp message class by their type code.
|
||||
# <key>: <value> - <afi, safi>: <nlri class>
|
||||
_NLRI_REGISTRY = {}
|
||||
|
||||
|
||||
def _register_nlri(cls):
|
||||
"""Used as class decorator for registering NLRI classes by their afi/safi.
|
||||
"""
|
||||
assert _NLRI_REGISTRY.get((cls.AFI, cls.SAFI)) is None
|
||||
_NLRI_REGISTRY[(cls.AFI, cls.SAFI)] = cls
|
||||
return cls
|
||||
|
||||
|
||||
#
|
||||
# AddressFamily
|
||||
#
|
||||
class AddressFamily(object):
|
||||
"""Subclasses of this class hold methods for a specific AF and
|
||||
help the calling code to stay AF-independent.
|
||||
|
||||
Each subclass need have just a singleton instance (see below).
|
||||
"""
|
||||
|
||||
def __init__(self, afi):
|
||||
self.afi = afi
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.afi)
|
||||
|
||||
def __cmp__(self, other):
|
||||
afi1 = None
|
||||
afi2 = None
|
||||
if isinstance(other, IntType):
|
||||
afi2 = other
|
||||
else:
|
||||
afi2 = other.afi
|
||||
if isinstance(self, IntType):
|
||||
afi1 = self
|
||||
else:
|
||||
afi1 = self.afi
|
||||
return cmp(afi1, afi2)
|
||||
|
||||
|
||||
class AfiIpv4(AddressFamily):
|
||||
def __init__(self):
|
||||
super(AfiIpv4, self).__init__(1)
|
||||
|
||||
def __repr__(self):
|
||||
return "IPv4"
|
||||
|
||||
|
||||
class AfiIpv6(AddressFamily):
|
||||
def __init__(self):
|
||||
super(AfiIpv6, self).__init__(2)
|
||||
|
||||
def __repr__(self):
|
||||
return "IPv6"
|
||||
|
||||
|
||||
#
|
||||
# SubAddressFamily
|
||||
#
|
||||
# An sub-address family as defined by BGP.
|
||||
#
|
||||
class SubAddressFamily(object):
|
||||
|
||||
def __init__(self, safi):
|
||||
self.safi = safi
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.safi)
|
||||
|
||||
def __cmp__(self, other):
|
||||
safi1 = None
|
||||
safi2 = None
|
||||
if isinstance(self, IntType):
|
||||
safi1 = self
|
||||
else:
|
||||
safi1 = self.safi
|
||||
if isinstance(other, IntType):
|
||||
safi2 = other
|
||||
else:
|
||||
safi2 = other.safi
|
||||
return cmp(safi1, safi2)
|
||||
|
||||
|
||||
class SafiNlriUnicast(SubAddressFamily):
|
||||
def __init__(self):
|
||||
super(SafiNlriUnicast, self).__init__(1)
|
||||
|
||||
def __repr__(self):
|
||||
return "SafiNlriUnicast"
|
||||
|
||||
|
||||
class SafiVpn(SubAddressFamily):
|
||||
def __init__(self):
|
||||
super(SafiVpn, self).__init__(128)
|
||||
|
||||
def __repr__(self):
|
||||
return "SafiVpn"
|
||||
|
||||
|
||||
class SafiRtc(SubAddressFamily):
|
||||
def __init__(self):
|
||||
super(SafiRtc, self).__init__(132)
|
||||
|
||||
def __repr__(self):
|
||||
return "SafiRtc"
|
||||
|
||||
NLRI_UC = SafiNlriUnicast()
|
||||
SAF_VPN = SafiVpn()
|
||||
SAF_RTC = SafiRtc()
|
||||
|
||||
# Singleton objects for each AF.
|
||||
AF_IPv4 = AfiIpv4()
|
||||
AF_IPv6 = AfiIpv6()
|
||||
|
||||
# Constants to represent address family and sub-address family.
|
||||
ADD_FMLY = 'afi'
|
||||
SUB_ADD_FMLY = 'safi'
|
||||
|
||||
|
||||
#
|
||||
# RouteFamily
|
||||
#
|
||||
class RouteFamily(object):
|
||||
"""The family that a given route (or Network Layer Reachability
|
||||
Information) belongs to.
|
||||
|
||||
Basically represents a combination of AFI/SAFI.
|
||||
"""
|
||||
__slots__ = ('_add_fmly', '_sub_add_fmly')
|
||||
|
||||
def __init__(self, add_fmly, sub_add_fmly):
|
||||
# Validate i/p.
|
||||
if not add_fmly or not sub_add_fmly:
|
||||
raise ValueError('Invalid arguments.')
|
||||
|
||||
self._add_fmly = add_fmly
|
||||
self._sub_add_fmly = sub_add_fmly
|
||||
|
||||
@property
|
||||
def afi(self):
|
||||
return self._add_fmly.afi
|
||||
|
||||
@property
|
||||
def safi(self):
|
||||
return self._sub_add_fmly.safi
|
||||
|
||||
def __repr__(self):
|
||||
return ('RouteFamily(afi=%s, safi=%s)' % (self.afi, self.safi))
|
||||
|
||||
def __cmp__(self, other):
|
||||
other_rf = (other.afi, other.safi)
|
||||
self_rf = (self.afi, self.safi)
|
||||
return cmp(self_rf, other_rf)
|
||||
|
||||
@staticmethod
|
||||
def is_valid(other):
|
||||
if other and (hasattr(other, 'afi') and hasattr(other, 'safi')):
|
||||
return True
|
||||
return False
|
||||
|
||||
# Various route family singletons.
|
||||
RF_IPv4_UC = RouteFamily(AF_IPv4, NLRI_UC)
|
||||
RF_IPv6_UC = RouteFamily(AF_IPv6, NLRI_UC)
|
||||
RF_IPv4_VPN = RouteFamily(AF_IPv4, SAF_VPN)
|
||||
RF_IPv6_VPN = RouteFamily(AF_IPv6, SAF_VPN)
|
||||
RF_RTC_UC = RouteFamily(AF_IPv4, SAF_RTC)
|
||||
|
||||
_rf_by_afi_safi = {
|
||||
(1, 1): RF_IPv4_UC,
|
||||
(2, 1): RF_IPv6_UC,
|
||||
(1, 128): RF_IPv4_VPN,
|
||||
(2, 128): RF_IPv6_VPN,
|
||||
(1, 132): RF_RTC_UC
|
||||
}
|
||||
|
||||
|
||||
def get_rf(afi, safi):
|
||||
"""Returns *RouteFamily* singleton instance for given *afi* and *safi*."""
|
||||
if not isinstance(afi, IntType):
|
||||
afi = int(afi)
|
||||
if not isinstance(safi, IntType):
|
||||
safi = int(safi)
|
||||
return _rf_by_afi_safi.get((afi, safi))
|
||||
|
||||
|
||||
# TODO(PH): Consider trade-offs of making this extend Internable.
|
||||
class Nlri(object):
|
||||
"""Represents super class of all Network Layer Reachability Information.
|
||||
"""
|
||||
__meta__ = ABCMeta
|
||||
__slots__ = ()
|
||||
|
||||
# Sub-classes should set afi/safi constants appropriately.
|
||||
AFI = 0
|
||||
SAFI = 0
|
||||
|
||||
@classmethod
|
||||
def encode(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def route_family(self):
|
||||
return get_rf(self.__class__.AFI, self.__class__.SAFI)
|
||||
|
||||
|
||||
@_register_nlri
|
||||
class Vpnv4(Nlri):
|
||||
"""Vpnv4 NLRI.
|
||||
"""
|
||||
__slots__ = ('_labels', '_route_disc', '_prefix')
|
||||
|
||||
AFI = 1
|
||||
SAFI = 128
|
||||
|
||||
def __init__(self, labels, route_disc, prefix):
|
||||
Nlri.__init__(self)
|
||||
if not labels:
|
||||
labels = []
|
||||
|
||||
# Validate given params
|
||||
for label in labels:
|
||||
if not is_valid_mpls_label(label):
|
||||
raise ValueError('Invalid label %s' % label)
|
||||
if (not is_valid_ipv4_prefix(prefix) or
|
||||
not is_valid_route_disc(route_disc)):
|
||||
raise ValueError('Invalid parameter value(s).')
|
||||
|
||||
self._labels = labels
|
||||
self._route_disc = route_disc
|
||||
self._prefix = prefix
|
||||
|
||||
@property
|
||||
def label_list(self):
|
||||
return self._labels[:]
|
||||
|
||||
@property
|
||||
def route_disc(self):
|
||||
return self._route_disc
|
||||
|
||||
@property
|
||||
def prefix(self):
|
||||
return self._prefix
|
||||
|
||||
@property
|
||||
def formatted_nlri_str(self):
|
||||
return "%s:%s" % (self._route_disc, self.prefix)
|
||||
|
||||
def __repr__(self):
|
||||
if self._labels:
|
||||
l = ','.join([str(l) for l in self._labels])
|
||||
else:
|
||||
l = 'none'
|
||||
|
||||
return ('Vpnv4(label=%s, route_disc=%s, prefix=%s)' %
|
||||
(l, self.route_disc, self.prefix))
|
||||
|
||||
def __str__(self):
|
||||
return 'Vpnv4 %s:%s, %s' % (self.route_disc, self.prefix, self._labels)
|
||||
|
||||
def __cmp__(self, other):
|
||||
return cmp(
|
||||
(self._labels, self.route_disc, self.prefix),
|
||||
(other.label_list, other.route_disc, other.prefix),
|
||||
)
|
||||
|
||||
def encode(self):
|
||||
plen = 0
|
||||
v = ''
|
||||
labels = self._labels[:]
|
||||
|
||||
if not labels:
|
||||
return '\0'
|
||||
|
||||
labels = [l << 4 for l in labels]
|
||||
labels[-1] |= 1
|
||||
|
||||
for l in labels:
|
||||
lo = l & 0xff
|
||||
hi = (l & 0xffff00) >> 8
|
||||
v += struct.pack('>HB', hi, lo)
|
||||
plen += 24
|
||||
|
||||
l, r = self.route_disc.split(':')
|
||||
if '.' in l:
|
||||
ip = socket.inet_aton(l)
|
||||
route_disc = struct.pack('!H4sH', 1, ip, int(r))
|
||||
else:
|
||||
route_disc = struct.pack('!HHI', 0, int(l), int(r))
|
||||
|
||||
v += route_disc
|
||||
plen += 64
|
||||
|
||||
ip, masklen = self.prefix.split('/')
|
||||
ip = socket.inet_aton(ip)
|
||||
masklen = int(masklen)
|
||||
|
||||
plen += masklen
|
||||
if masklen > 24:
|
||||
v += ip
|
||||
elif masklen > 16:
|
||||
v += ip[:3]
|
||||
elif masklen > 8:
|
||||
v += ip[:2]
|
||||
elif masklen > 0:
|
||||
v += ip[:1]
|
||||
else:
|
||||
pass
|
||||
|
||||
return struct.pack('B', plen) + v
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, plen, val):
|
||||
|
||||
if plen == 0:
|
||||
# TODO(PH): Confirm this is valid case and implementation.
|
||||
return cls([], '0:0', '0.0.0.0/0')
|
||||
|
||||
idx = 0
|
||||
|
||||
# plen is the length, in bits, of all the MPLS labels,
|
||||
# plus the 8-byte RD, plus the IP prefix
|
||||
labels = []
|
||||
while True:
|
||||
ls, = struct.unpack_from('3s', val, idx)
|
||||
idx += 3
|
||||
plen -= 24
|
||||
|
||||
if ls == '\x80\x00\x00':
|
||||
# special null label for vpnv4 withdraws
|
||||
labels = None
|
||||
break
|
||||
|
||||
label, = struct.unpack_from('!I', '\x00' + ls)
|
||||
bottom = label & 1
|
||||
|
||||
labels.append(label >> 4)
|
||||
if bottom:
|
||||
break
|
||||
# TODO(PH): We are breaking after first label as we support only
|
||||
# one label for now. Revisit if we need to support stack of labels.
|
||||
break
|
||||
|
||||
rdtype, route_disc = struct.unpack_from('!H6s', val, idx)
|
||||
if rdtype == 1:
|
||||
rdip, num = struct.unpack('!4sH', route_disc)
|
||||
rdip = socket.inet_ntoa(rdip)
|
||||
route_disc = '%s:%s' % (rdip, num)
|
||||
else:
|
||||
num1, num2 = struct.unpack('!HI', route_disc)
|
||||
route_disc = '%s:%s' % (num1, num2)
|
||||
|
||||
idx += 8
|
||||
plen -= 64
|
||||
|
||||
ipl = pb(plen)
|
||||
ip = val[idx:idx + ipl]
|
||||
idx += ipl
|
||||
|
||||
prefix = unpack_ipv4(ip, plen)
|
||||
|
||||
return cls(labels, route_disc, prefix)
|
||||
|
||||
|
||||
@_register_nlri
|
||||
class Vpnv6(Nlri):
|
||||
"""Vpnv4 NLRI.
|
||||
"""
|
||||
__slots__ = ('_labels', '_route_disc', '_prefix')
|
||||
|
||||
AFI = 2
|
||||
SAFI = 128
|
||||
|
||||
def __init__(self, labels, route_disc, prefix):
|
||||
Nlri.__init__(self)
|
||||
if not labels:
|
||||
labels = []
|
||||
|
||||
# Validate given params
|
||||
for label in labels:
|
||||
if not is_valid_mpls_label(label):
|
||||
raise ValueError('Invalid label %s' % label)
|
||||
if (not is_valid_route_disc(route_disc) or
|
||||
not is_valid_ipv6_prefix(prefix)):
|
||||
raise ValueError('Invalid parameter value(s).')
|
||||
|
||||
self._labels = labels
|
||||
self._route_disc = route_disc
|
||||
self._prefix = prefix
|
||||
|
||||
@property
|
||||
def label_list(self):
|
||||
return self._labels[:]
|
||||
|
||||
@property
|
||||
def route_disc(self):
|
||||
return self._route_disc
|
||||
|
||||
@property
|
||||
def prefix(self):
|
||||
return self._prefix
|
||||
|
||||
@property
|
||||
def formatted_nlri_str(self):
|
||||
return "%s:%s" % (self._route_disc, self.prefix)
|
||||
|
||||
def __repr__(self):
|
||||
if self._labels:
|
||||
l = ','.join([str(l) for l in self._labels])
|
||||
else:
|
||||
l = 'none'
|
||||
|
||||
return ('Vpnv6(label=%s, route_disc=%s, prefix=%s)' %
|
||||
(l, self.route_disc, self.prefix))
|
||||
|
||||
def __str__(self):
|
||||
return 'Vpnv6 %s:%s, %s' % (self.route_disc, self.prefix, self._labels)
|
||||
|
||||
def __cmp__(self, other):
|
||||
return cmp(
|
||||
(self._labels, self.route_disc, Ipv6(self.prefix).encode()),
|
||||
(other.label_list, other.route_disc, Ipv6(other.prefix).encode()),
|
||||
)
|
||||
|
||||
def encode(self):
|
||||
plen = 0
|
||||
v = ''
|
||||
labels = self._labels[:]
|
||||
|
||||
if not labels:
|
||||
return '\0'
|
||||
|
||||
labels = [l << 4 for l in labels]
|
||||
labels[-1] |= 1
|
||||
|
||||
for l in labels:
|
||||
lo = l & 0xff
|
||||
hi = (l & 0xffff00) >> 8
|
||||
v += struct.pack('>HB', hi, lo)
|
||||
plen += 24
|
||||
|
||||
l, r = self.route_disc.split(':')
|
||||
if '.' in l:
|
||||
ip = socket.inet_aton(l)
|
||||
route_disc = struct.pack('!H4sH', 1, ip, int(r))
|
||||
else:
|
||||
route_disc = struct.pack('!HHI', 0, int(l), int(r))
|
||||
v += route_disc
|
||||
plen += 64
|
||||
|
||||
ip, masklen = self.prefix.split('/')
|
||||
ip = socket.inet_pton(socket.AF_INET6, ip)
|
||||
masklen = int(masklen)
|
||||
|
||||
plen += masklen
|
||||
v += ip[:pb6(masklen)]
|
||||
|
||||
return struct.pack('B', plen) + v
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, plen, val):
|
||||
if plen == 0:
|
||||
# TODO(PH): Confirm this is valid case and implementation.
|
||||
return cls([], '0:0', '::/0')
|
||||
|
||||
idx = 0
|
||||
|
||||
# plen is the length, in bits, of all the MPLS labels,
|
||||
# plus the 8-byte RD, plus the IP prefix
|
||||
labels = []
|
||||
while True:
|
||||
ls, = struct.unpack_from('3s', val, idx)
|
||||
idx += 3
|
||||
plen -= 24
|
||||
|
||||
if ls == '\x80\x00\x00':
|
||||
# special null label for vpnv4 withdraws
|
||||
labels = None
|
||||
break
|
||||
|
||||
label, = struct.unpack_from('!I', '\x00' + ls)
|
||||
bottom = label & 1
|
||||
|
||||
labels.append(label >> 4)
|
||||
if bottom:
|
||||
break
|
||||
# TODO(PH): We are breaking after first label as we support only
|
||||
# one label for now. Revisit if we need to support stack of labels.
|
||||
break
|
||||
|
||||
rdtype, route_disc = struct.unpack_from('!H6s', val, idx)
|
||||
if rdtype == 1:
|
||||
rdip, num = struct.unpack('!4sH', route_disc)
|
||||
rdip = socket.inet_ntoa(rdip)
|
||||
route_disc = '%s:%s' % (rdip, num)
|
||||
else:
|
||||
num1, num2 = struct.unpack('!HI', route_disc)
|
||||
route_disc = '%s:%s' % (num1, num2)
|
||||
|
||||
idx += 8
|
||||
plen -= 64
|
||||
|
||||
ipl = pb6(plen)
|
||||
ip = val[idx:idx + ipl]
|
||||
idx += ipl
|
||||
|
||||
prefix = unpack_ipv6(ip, plen)
|
||||
|
||||
return cls(labels, route_disc, prefix)
|
||||
|
||||
|
||||
@_register_nlri
|
||||
class Ipv4(Nlri):
|
||||
__slots__ = ('_prefix')
|
||||
|
||||
AFI = 1
|
||||
SAFI = 1
|
||||
|
||||
def __init__(self, prefix):
|
||||
if not is_valid_ipv4_prefix(prefix):
|
||||
raise ValueError('Invalid prefix %s.' % prefix)
|
||||
Nlri.__init__(self)
|
||||
self._prefix = prefix
|
||||
|
||||
@property
|
||||
def prefix(self):
|
||||
return self._prefix
|
||||
|
||||
@property
|
||||
def formatted_nlri_str(self):
|
||||
return self._prefix
|
||||
|
||||
def __cmp__(self, other):
|
||||
aip, alen = self.prefix.split('/')
|
||||
alen = int(alen)
|
||||
aip = socket.inet_aton(aip)
|
||||
|
||||
bip, blen = other.prefix.split('/')
|
||||
blen = int(blen)
|
||||
bip = socket.inet_aton(bip)
|
||||
|
||||
return cmp((aip, alen), (bip, blen))
|
||||
|
||||
def encode(self):
|
||||
plen = 0
|
||||
v = ''
|
||||
|
||||
ip, masklen = self.prefix.split('/')
|
||||
ip = socket.inet_aton(ip)
|
||||
masklen = int(masklen)
|
||||
|
||||
plen += masklen
|
||||
if masklen > 24:
|
||||
v += ip
|
||||
elif masklen > 16:
|
||||
v += ip[:3]
|
||||
elif masklen > 8:
|
||||
v += ip[:2]
|
||||
elif masklen > 0:
|
||||
v += ip[:1]
|
||||
else:
|
||||
pass
|
||||
|
||||
return struct.pack('B', plen) + v
|
||||
|
||||
def __repr__(self):
|
||||
return 'Ipv4(%s)' % (self.prefix)
|
||||
|
||||
def __str__(self):
|
||||
return 'Ipv4 ' + self.prefix
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, plen, val):
|
||||
return cls(unpack_ipv4(val, plen))
|
||||
|
||||
|
||||
@_register_nlri
|
||||
class Ipv6(Nlri):
|
||||
__slots__ = ('_prefix')
|
||||
|
||||
AFI = 2
|
||||
SAFI = 1
|
||||
|
||||
def __init__(self, prefix):
|
||||
if not is_valid_ipv6_prefix(prefix):
|
||||
raise ValueError('Invalid prefix %s.' % prefix)
|
||||
Nlri.__init__(self)
|
||||
self._prefix = prefix
|
||||
|
||||
@property
|
||||
def prefix(self):
|
||||
return self._prefix
|
||||
|
||||
@property
|
||||
def formatted_nlri_str(self):
|
||||
return self._prefix
|
||||
|
||||
def __cmp__(self, other):
|
||||
abin = self.encode()
|
||||
bbin = other.encode()
|
||||
return cmp(abin, bbin)
|
||||
|
||||
def encode(self):
|
||||
plen = 0
|
||||
v = ''
|
||||
|
||||
ip, masklen = self.prefix.split('/')
|
||||
ip = socket.inet_pton(socket.AF_INET6, ip)
|
||||
masklen = int(masklen)
|
||||
|
||||
plen += masklen
|
||||
ip_slice = pb6(masklen)
|
||||
v += ip[:ip_slice]
|
||||
|
||||
return struct.pack('B', plen) + v
|
||||
|
||||
def __repr__(self):
|
||||
return 'Ipv6(%s)' % (self.prefix)
|
||||
|
||||
def __str__(self):
|
||||
return 'Ipv6 ' + self.prefix
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, plen, val):
|
||||
return cls(unpack_ipv6(val, plen))
|
||||
|
||||
|
||||
@_register_nlri
|
||||
class RtNlri(Nlri):
|
||||
"""Route Target Membership NLRI.
|
||||
|
||||
Route Target membership NLRI is advertised in BGP UPDATE messages using
|
||||
the MP_REACH_NLRI and MP_UNREACH_NLRI attributes.
|
||||
"""
|
||||
__slots__ = ('_origin_as', '_route_target')
|
||||
|
||||
AFI = 1
|
||||
SAFI = 132
|
||||
DEFAULT_AS = '0:0'
|
||||
DEFAULT_RT = '0:0'
|
||||
|
||||
def __init__(self, origin_as, route_target):
|
||||
Nlri.__init__(self)
|
||||
# If given is not default_as and default_rt
|
||||
if not (origin_as is RtNlri.DEFAULT_AS and
|
||||
route_target is RtNlri.DEFAULT_RT):
|
||||
# We validate them
|
||||
if (not is_valid_old_asn(origin_as) or
|
||||
not is_valid_ext_comm_attr(route_target)):
|
||||
raise ValueError('Invalid params.')
|
||||
self._origin_as = origin_as
|
||||
self._route_target = route_target
|
||||
|
||||
@property
|
||||
def origin_as(self):
|
||||
return self._origin_as
|
||||
|
||||
@property
|
||||
def route_target(self):
|
||||
return self._route_target
|
||||
|
||||
@property
|
||||
def formatted_nlri_str(self):
|
||||
return "%s:%s" % (self.origin_as, self.route_target)
|
||||
|
||||
def is_default_rtnlri(self):
|
||||
if (self._origin_as is RtNlri.DEFAULT_AS and
|
||||
self._route_target is RtNlri.DEFAULT_RT):
|
||||
return True
|
||||
return False
|
||||
|
||||
def __str__(self):
|
||||
return 'RtNlri ' + str(self._origin_as) + ':' + self._route_target
|
||||
|
||||
def __repr__(self):
|
||||
return 'RtNlri(%s, %s)' % (self._origin_as, self._route_target)
|
||||
|
||||
def __cmp__(self, other):
|
||||
return cmp(
|
||||
(self._origin_as, self._route_target),
|
||||
(other.origin_as, other.route_target),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, plen, val):
|
||||
idx = 0
|
||||
if plen == 0 and not val:
|
||||
return cls(RtNlri.DEFAULT_AS, RtNlri.DEFAULT_RT)
|
||||
|
||||
# Extract origin AS.
|
||||
origin_as, = struct.unpack_from('!I', val, idx)
|
||||
idx += 4
|
||||
|
||||
# Extract route target.
|
||||
route_target = ''
|
||||
etype, esubtype, payload = struct.unpack_from('BB6s', val, idx)
|
||||
# RFC says: The value of the high-order octet of the Type field for the
|
||||
# Route Target Community can be 0x00, 0x01, or 0x02. The value of the
|
||||
# low-order octet of the Type field for this community is 0x02.
|
||||
# TODO(PH): Remove this exception when it breaks something Here we make
|
||||
# exception as Routem packs lower-order octet as 0x00
|
||||
if etype in (0, 2) and esubtype in (0, 2):
|
||||
# If we have route target community in AS number format.
|
||||
asnum, i = struct.unpack('!HI', payload)
|
||||
route_target = ('%s:%s' % (asnum, i))
|
||||
elif etype == 1 and esubtype == 2:
|
||||
# If we have route target community in IP address format.
|
||||
ip_addr, i = struct.unpack('!4sH', payload)
|
||||
ip_addr = socket.inet_ntoa(ip_addr)
|
||||
route_target = ('%s:%s' % (ip_addr, i))
|
||||
elif etype == 0 and esubtype == 1:
|
||||
# TODO(PH): Parsing of RtNlri 1:1:100:1
|
||||
asnum, i = struct.unpack('!HI', payload)
|
||||
route_target = ('%s:%s' % (asnum, i))
|
||||
|
||||
return cls(origin_as, route_target)
|
||||
|
||||
def encode(self):
|
||||
rt_nlri = ''
|
||||
if not self.is_default_rtnlri():
|
||||
rt_nlri += struct.pack('!I', self.origin_as)
|
||||
# Encode route target
|
||||
first, second = self.route_target.split(':')
|
||||
if '.' in first:
|
||||
ip_addr = socket.inet_aton(first)
|
||||
rt_nlri += struct.pack('!BB4sH', 1, 2, ip_addr, int(second))
|
||||
else:
|
||||
rt_nlri += struct.pack('!BBHI', 0, 2, int(first), int(second))
|
||||
|
||||
# RT Nlri is 12 octets
|
||||
return struct.pack('B', (8 * 12)) + rt_nlri
|
||||
|
||||
|
||||
def pb(masklen):
|
||||
if masklen > 24:
|
||||
return 4
|
||||
elif masklen > 16:
|
||||
return 3
|
||||
elif masklen > 8:
|
||||
return 2
|
||||
elif masklen > 0:
|
||||
return 1
|
||||
return 0
|
||||
|
||||
_v6_bits = range(120, -8, -8)
|
||||
_v6_bytes = [i / 8 for i in range(128, 0, -8)]
|
||||
|
||||
|
||||
def pb6(masklen):
|
||||
for idx, bits in enumerate(_v6_bits):
|
||||
if masklen > bits:
|
||||
return _v6_bytes[idx]
|
||||
return 0
|
||||
|
||||
|
||||
def unpack_ipv4(pi, masklen):
|
||||
pi += '\x00' * 4
|
||||
return '%s/%s' % (socket.inet_ntoa(pi[:4]), masklen)
|
||||
|
||||
|
||||
def unpack_ipv6(pi, masklen):
|
||||
pi += '\x00' * 16
|
||||
ip = socket.inet_ntop(socket.AF_INET6, pi[:16])
|
||||
return '%s/%s' % (ip, masklen)
|
||||
|
||||
|
||||
def ipv4_mapped_ipv6(ipv4):
|
||||
if not is_valid_ipv4(ipv4):
|
||||
raise ValueError('Invalid ipv4 address given %s.' % ipv4)
|
||||
ipv4n = socket.inet_pton(socket.AF_INET, ipv4)
|
||||
ipv6_hex = '00' * 10 + 'ff' * 2 + bytes2hex(ipv4n)
|
||||
ipv6n = hex2byte(ipv6_hex)
|
||||
ipv6 = socket.inet_ntop(socket.AF_INET6, ipv6n)
|
||||
return ipv6
|
||||
|
||||
|
||||
# TODO(PH): Consider refactoring common functionality new methods
|
||||
# Look at previous commit
|
||||
def parse(received, afi=1, safi=1):
|
||||
recv_nlri_list = []
|
||||
|
||||
klass = _NLRI_REGISTRY.get((afi, safi))
|
||||
if not klass:
|
||||
raise ValueError('Asked to parse unsupported NLRI afi/safi: '
|
||||
'(%s, %s)' % (afi, safi))
|
||||
|
||||
try:
|
||||
idx = 0
|
||||
while idx < len(received):
|
||||
plen, = struct.unpack_from('B', received, idx)
|
||||
idx += 1
|
||||
nbytes, rest = divmod(plen, 8)
|
||||
if rest:
|
||||
nbytes += 1
|
||||
val = received[idx:idx + nbytes]
|
||||
idx += nbytes
|
||||
recv_nlri_list.append(klass.from_bytes(plen, val))
|
||||
except Exception:
|
||||
raise OptAttrError()
|
||||
|
||||
return recv_nlri_list
|
||||
1076
ryu/services/protocols/bgp/protocols/bgp/pathattr.py
Normal file
1076
ryu/services/protocols/bgp/protocols/bgp/pathattr.py
Normal file
File diff suppressed because it is too large
Load Diff
700
ryu/services/protocols/bgp/rtconf/base.py
Normal file
700
ryu/services/protocols/bgp/rtconf/base.py
Normal file
@ -0,0 +1,700 @@
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Running or runtime configuration base classes.
|
||||
"""
|
||||
from abc import ABCMeta
|
||||
from abc import abstractmethod
|
||||
import logging
|
||||
from types import BooleanType
|
||||
from types import IntType
|
||||
from types import LongType
|
||||
|
||||
from ryu.services.protocols.bgp.base import add_bgp_error_metadata
|
||||
from ryu.services.protocols.bgp.base import BGPSException
|
||||
from ryu.services.protocols.bgp.base import get_validator
|
||||
from ryu.services.protocols.bgp.base import RUNTIME_CONF_ERROR_CODE
|
||||
from ryu.services.protocols.bgp.base import validate
|
||||
|
||||
from ryu.services.protocols.bgp.protocols.bgp.pathattr import ExtCommunity
|
||||
from ryu.services.protocols.bgp.utils import validation
|
||||
from ryu.services.protocols.bgp.utils.validation import is_valid_old_asn
|
||||
|
||||
LOG = logging.getLogger('bgpspeaker.rtconf.base')
|
||||
|
||||
#
|
||||
# Nested settings.
|
||||
#
|
||||
CAP_REFRESH = 'cap_refresh'
|
||||
CAP_ENHANCED_REFRESH = 'cap_enhanced_refresh'
|
||||
CAP_MBGP_VPNV4 = 'cap_mbgp_vpnv4'
|
||||
CAP_MBGP_VPNV6 = 'cap_mbgp_vpnv6'
|
||||
CAP_RTC = 'cap_rtc'
|
||||
RTC_AS = 'rtc_as'
|
||||
HOLD_TIME = 'hold_time'
|
||||
|
||||
# To control how many prefixes can be received from a neighbor.
|
||||
# 0 value indicates no limit and other related options will be ignored.
|
||||
# Current behavior is to log that limit has reached.
|
||||
MAX_PREFIXES = 'max_prefixes'
|
||||
|
||||
# Has same meaning as: http://www.juniper.net/techpubs/software/junos/junos94
|
||||
# /swconfig-routing/disabling-suppression-of-route-
|
||||
# advertisements.html#id-13255463
|
||||
ADVERTISE_PEER_AS = 'advertise_peer_as'
|
||||
|
||||
# MED - MULTI_EXIT_DISC
|
||||
MULTI_EXIT_DISC = 'multi_exit_disc'
|
||||
|
||||
# Extended community attribute route origin.
|
||||
SITE_OF_ORIGINS = 'site_of_origins'
|
||||
|
||||
# Constants related to errors.
|
||||
CONF_NAME = 'conf_name'
|
||||
CONF_VALUE = 'conf_value'
|
||||
|
||||
# Max. value limits
|
||||
MAX_NUM_IMPORT_RT = 1000
|
||||
MAX_NUM_EXPORT_RT = 250
|
||||
MAX_NUM_SOO = 10
|
||||
|
||||
|
||||
#==============================================================================
|
||||
# Runtime configuration errors or exceptions.
|
||||
#==============================================================================
|
||||
|
||||
@add_bgp_error_metadata(code=RUNTIME_CONF_ERROR_CODE, sub_code=1,
|
||||
def_desc='Error with runtime-configuration.')
|
||||
class RuntimeConfigError(BGPSException):
|
||||
"""Base class for all runtime configuration errors.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@add_bgp_error_metadata(code=RUNTIME_CONF_ERROR_CODE, sub_code=2,
|
||||
def_desc='Missing required configuration.')
|
||||
class MissingRequiredConf(RuntimeConfigError):
|
||||
"""Exception raised when trying to configure with missing required
|
||||
settings.
|
||||
"""
|
||||
def __init__(self, **kwargs):
|
||||
conf_name = kwargs.get('conf_name')
|
||||
if conf_name:
|
||||
super(MissingRequiredConf, self).__init__(
|
||||
desc='Missing required configuration: %s' % conf_name)
|
||||
else:
|
||||
super(MissingRequiredConf, self).__init__(desc=kwargs.get('desc'))
|
||||
|
||||
|
||||
@add_bgp_error_metadata(code=RUNTIME_CONF_ERROR_CODE, sub_code=3,
|
||||
def_desc='Incorrect Type for configuration.')
|
||||
class ConfigTypeError(RuntimeConfigError):
|
||||
"""Exception raised when configuration value type miss-match happens.
|
||||
"""
|
||||
def __init__(self, **kwargs):
|
||||
conf_name = kwargs.get(CONF_NAME)
|
||||
conf_value = kwargs.get(CONF_VALUE)
|
||||
if conf_name and conf_value:
|
||||
super(ConfigTypeError, self).__init__(
|
||||
desc='Incorrect Type %s for configuration: %s' %
|
||||
(conf_value, conf_name))
|
||||
elif conf_name:
|
||||
super(ConfigTypeError, self).__init__(
|
||||
desc='Incorrect Type for configuration: %s' % conf_name)
|
||||
else:
|
||||
super(ConfigTypeError, self).__init__(desc=kwargs.get('desc'))
|
||||
|
||||
|
||||
@add_bgp_error_metadata(code=RUNTIME_CONF_ERROR_CODE, sub_code=4,
|
||||
def_desc='Incorrect Value for configuration.')
|
||||
class ConfigValueError(RuntimeConfigError):
|
||||
"""Exception raised when configuration value is of correct type but
|
||||
incorrect value.
|
||||
"""
|
||||
def __init__(self, **kwargs):
|
||||
conf_name = kwargs.get(CONF_NAME)
|
||||
conf_value = kwargs.get(CONF_VALUE)
|
||||
if conf_name and conf_value:
|
||||
super(ConfigValueError, self).__init__(
|
||||
desc='Incorrect Value %s for configuration: %s' %
|
||||
(conf_value, conf_name))
|
||||
elif conf_name:
|
||||
super(ConfigValueError, self).__init__(
|
||||
desc='Incorrect Value for configuration: %s' % conf_name)
|
||||
else:
|
||||
super(ConfigValueError, self).__init__(desc=kwargs.get('desc'))
|
||||
|
||||
|
||||
#==============================================================================
|
||||
# Configuration base classes.
|
||||
#==============================================================================
|
||||
|
||||
class BaseConf(object):
|
||||
"""Base class for a set of configuration values.
|
||||
|
||||
Configurations can be required or optional. Also acts as a container of
|
||||
configuration change listeners.
|
||||
"""
|
||||
__metaclass__ = ABCMeta
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self._req_settings = self.get_req_settings()
|
||||
self._opt_settings = self.get_opt_settings()
|
||||
self._valid_evts = self.get_valid_evts()
|
||||
self._listeners = {}
|
||||
self._settings = {}
|
||||
|
||||
# validate required and unknown settings
|
||||
self._validate_req_unknown_settings(**kwargs)
|
||||
|
||||
# Initialize configuration settings.
|
||||
self._init_req_settings(**kwargs)
|
||||
self._init_opt_settings(**kwargs)
|
||||
|
||||
@property
|
||||
def settings(self):
|
||||
"""Returns a copy of current settings."""
|
||||
return self._settings.copy()
|
||||
|
||||
@classmethod
|
||||
def get_valid_evts(self):
|
||||
return set()
|
||||
|
||||
@classmethod
|
||||
def get_req_settings(self):
|
||||
return set()
|
||||
|
||||
@classmethod
|
||||
def get_opt_settings(self):
|
||||
return set()
|
||||
|
||||
@abstractmethod
|
||||
def _init_opt_settings(self, **kwargs):
|
||||
"""Sub-classes should override this method to initialize optional
|
||||
settings.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def update(self, **kwargs):
|
||||
# Validate given values
|
||||
self._validate_req_unknown_settings(**kwargs)
|
||||
|
||||
def _validate_req_unknown_settings(self, **kwargs):
|
||||
"""Checks if required settings are present.
|
||||
|
||||
Also checks if unknown requirements are present.
|
||||
"""
|
||||
# Validate given configuration.
|
||||
self._all_attrs = (self._req_settings | self._opt_settings)
|
||||
if not kwargs and len(self._req_settings) > 0:
|
||||
raise MissingRequiredConf(desc='Missing all required attributes.')
|
||||
|
||||
given_attrs = frozenset(kwargs.keys())
|
||||
unknown_attrs = given_attrs - self._all_attrs
|
||||
if unknown_attrs:
|
||||
raise RuntimeConfigError(desc=(
|
||||
'Unknown attributes: %s' %
|
||||
', '.join([str(i) for i in unknown_attrs]))
|
||||
)
|
||||
missing_req_settings = self._req_settings - given_attrs
|
||||
if missing_req_settings:
|
||||
raise MissingRequiredConf(conf_name=list(missing_req_settings))
|
||||
|
||||
def _init_req_settings(self, **kwargs):
|
||||
for req_attr in self._req_settings:
|
||||
req_attr_value = kwargs.get(req_attr)
|
||||
if req_attr_value is None:
|
||||
raise MissingRequiredConf(conf_name=req_attr_value)
|
||||
# Validate attribute value
|
||||
req_attr_value = get_validator(req_attr)(req_attr_value)
|
||||
self._settings[req_attr] = req_attr_value
|
||||
|
||||
def add_listener(self, evt, callback):
|
||||
# if (evt not in self.get_valid_evts()):
|
||||
# raise RuntimeConfigError(desc=('Unknown event %s' % evt))
|
||||
|
||||
listeners = self._listeners.get(evt, None)
|
||||
if not listeners:
|
||||
listeners = set()
|
||||
self._listeners[evt] = listeners
|
||||
listeners.update([callback])
|
||||
|
||||
def remove_listener(self, evt, callback):
|
||||
if evt in self.get_valid_evts():
|
||||
listeners = self._listeners.get(evt, None)
|
||||
if listeners and (callback in listeners):
|
||||
listeners.remove(callback)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _notify_listeners(self, evt, value):
|
||||
listeners = self._listeners.get(evt, [])
|
||||
for callback in listeners:
|
||||
callback(ConfEvent(self, evt, value))
|
||||
|
||||
def __repr__(self):
|
||||
return '%s(%r)' % (self.__class__, self._settings)
|
||||
|
||||
|
||||
class ConfWithId(BaseConf):
|
||||
"""Configuration settings related to identity."""
|
||||
# Config./resource identifier.
|
||||
ID = 'id'
|
||||
# Config./resource name.
|
||||
NAME = 'name'
|
||||
# Config./resource description.
|
||||
DESCRIPTION = 'description'
|
||||
|
||||
UPDATE_NAME_EVT = 'update_name_evt'
|
||||
UPDATE_DESCRIPTION_EVT = 'update_description_evt'
|
||||
|
||||
VALID_EVT = frozenset([UPDATE_NAME_EVT, UPDATE_DESCRIPTION_EVT])
|
||||
REQUIRED_SETTINGS = frozenset([ID])
|
||||
OPTIONAL_SETTINGS = frozenset([NAME, DESCRIPTION])
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(ConfWithId, self).__init__(**kwargs)
|
||||
|
||||
@classmethod
|
||||
def get_opt_settings(cls):
|
||||
self_confs = super(ConfWithId, cls).get_opt_settings()
|
||||
self_confs.update(ConfWithId.OPTIONAL_SETTINGS)
|
||||
return self_confs
|
||||
|
||||
@classmethod
|
||||
def get_req_settings(cls):
|
||||
self_confs = super(ConfWithId, cls).get_req_settings()
|
||||
self_confs.update(ConfWithId.REQUIRED_SETTINGS)
|
||||
return self_confs
|
||||
|
||||
@classmethod
|
||||
def get_valid_evts(cls):
|
||||
self_valid_evts = super(ConfWithId, cls).get_valid_evts()
|
||||
self_valid_evts.update(ConfWithId.VALID_EVT)
|
||||
return self_valid_evts
|
||||
|
||||
def _init_opt_settings(self, **kwargs):
|
||||
super(ConfWithId, self)._init_opt_settings(**kwargs)
|
||||
self._settings[ConfWithId.NAME] = \
|
||||
compute_optional_conf(ConfWithId.NAME, str(self), **kwargs)
|
||||
self._settings[ConfWithId.DESCRIPTION] = \
|
||||
compute_optional_conf(ConfWithId.DESCRIPTION, str(self), **kwargs)
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
return self._settings[ConfWithId.ID]
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self._settings[ConfWithId.NAME]
|
||||
|
||||
@name.setter
|
||||
def name(self, new_name):
|
||||
old_name = self.name
|
||||
if not new_name:
|
||||
new_name = repr(self)
|
||||
else:
|
||||
get_validator(ConfWithId.NAME)(new_name)
|
||||
|
||||
if old_name != new_name:
|
||||
self._settings[ConfWithId.NAME] = new_name
|
||||
self._notify_listeners(ConfWithId.UPDATE_NAME_EVT,
|
||||
(old_name, self.name))
|
||||
|
||||
@property
|
||||
def description(self):
|
||||
return self._settings[ConfWithId.DESCRIPTION]
|
||||
|
||||
@description.setter
|
||||
def description(self, new_description):
|
||||
old_desc = self.description
|
||||
if not new_description:
|
||||
new_description = str(self)
|
||||
else:
|
||||
get_validator(ConfWithId.DESCRIPTION)(new_description)
|
||||
|
||||
if old_desc != new_description:
|
||||
self._settings[ConfWithId.DESCRIPTION] = new_description
|
||||
self._notify_listeners(ConfWithId.UPDATE_DESCRIPTION_EVT,
|
||||
(old_desc, self.description))
|
||||
|
||||
def update(self, **kwargs):
|
||||
# Update inherited configurations
|
||||
super(ConfWithId, self).update(**kwargs)
|
||||
self.name = compute_optional_conf(ConfWithId.NAME,
|
||||
str(self),
|
||||
**kwargs)
|
||||
self.description = compute_optional_conf(ConfWithId.DESCRIPTION,
|
||||
str(self),
|
||||
**kwargs)
|
||||
|
||||
|
||||
class ConfWithStats(BaseConf):
|
||||
"""Configuration settings related to statistics collection."""
|
||||
|
||||
# Enable or disable statistics logging.
|
||||
STATS_LOG_ENABLED = 'statistics_log_enabled'
|
||||
DEFAULT_STATS_LOG_ENABLED = False
|
||||
|
||||
# Statistics logging time.
|
||||
STATS_TIME = 'statistics_interval'
|
||||
DEFAULT_STATS_TIME = 60
|
||||
|
||||
UPDATE_STATS_LOG_ENABLED_EVT = 'update_stats_log_enabled_evt'
|
||||
UPDATE_STATS_TIME_EVT = 'update_stats_time_evt'
|
||||
|
||||
VALID_EVT = frozenset([UPDATE_STATS_LOG_ENABLED_EVT,
|
||||
UPDATE_STATS_TIME_EVT])
|
||||
OPTIONAL_SETTINGS = frozenset([STATS_LOG_ENABLED, STATS_TIME])
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(ConfWithStats, self).__init__(**kwargs)
|
||||
|
||||
def _init_opt_settings(self, **kwargs):
|
||||
super(ConfWithStats, self)._init_opt_settings(**kwargs)
|
||||
self._settings[ConfWithStats.STATS_LOG_ENABLED] = \
|
||||
compute_optional_conf(ConfWithStats.STATS_LOG_ENABLED,
|
||||
ConfWithStats.DEFAULT_STATS_LOG_ENABLED,
|
||||
**kwargs)
|
||||
self._settings[ConfWithStats.STATS_TIME] = \
|
||||
compute_optional_conf(ConfWithStats.STATS_TIME,
|
||||
ConfWithStats.DEFAULT_STATS_TIME,
|
||||
**kwargs)
|
||||
|
||||
@property
|
||||
def stats_log_enabled(self):
|
||||
return self._settings[ConfWithStats.STATS_LOG_ENABLED]
|
||||
|
||||
@stats_log_enabled.setter
|
||||
def stats_log_enabled(self, enabled):
|
||||
get_validator(ConfWithStats.STATS_LOG_ENABLED)(enabled)
|
||||
if enabled != self.stats_log_enabled:
|
||||
self._settings[ConfWithStats.STATS_LOG_ENABLED] = enabled
|
||||
self._notify_listeners(ConfWithStats.UPDATE_STATS_LOG_ENABLED_EVT,
|
||||
enabled)
|
||||
|
||||
@property
|
||||
def stats_time(self):
|
||||
return self._settings[ConfWithStats.STATS_TIME]
|
||||
|
||||
@stats_time.setter
|
||||
def stats_time(self, stats_time):
|
||||
get_validator(ConfWithStats.STATS_TIME)(stats_time)
|
||||
if stats_time != self.stats_time:
|
||||
self._settings[ConfWithStats.STATS_TIME] = stats_time
|
||||
self._notify_listeners(ConfWithStats.UPDATE_STATS_TIME_EVT,
|
||||
stats_time)
|
||||
|
||||
@classmethod
|
||||
def get_opt_settings(cls):
|
||||
confs = super(ConfWithStats, cls).get_opt_settings()
|
||||
confs.update(ConfWithStats.OPTIONAL_SETTINGS)
|
||||
return confs
|
||||
|
||||
@classmethod
|
||||
def get_valid_evts(cls):
|
||||
valid_evts = super(ConfWithStats, cls).get_valid_evts()
|
||||
valid_evts.update(ConfWithStats.VALID_EVT)
|
||||
return valid_evts
|
||||
|
||||
def update(self, **kwargs):
|
||||
# Update inherited configurations
|
||||
super(ConfWithStats, self).update(**kwargs)
|
||||
self.stats_log_enabled = \
|
||||
compute_optional_conf(ConfWithStats.STATS_LOG_ENABLED,
|
||||
ConfWithStats.DEFAULT_STATS_LOG_ENABLED,
|
||||
**kwargs)
|
||||
self.stats_time = \
|
||||
compute_optional_conf(ConfWithStats.STATS_TIME,
|
||||
ConfWithStats.DEFAULT_STATS_TIME,
|
||||
**kwargs)
|
||||
|
||||
|
||||
class BaseConfListener(object):
|
||||
"""Base class of all configuration listeners."""
|
||||
__metaclass__ = ABCMeta
|
||||
|
||||
def __init__(self, base_conf):
|
||||
pass
|
||||
# TODO(PH): re-vist later and check if we need this check
|
||||
# if not isinstance(base_conf, BaseConf):
|
||||
# raise TypeError('Currently we only support listening to '
|
||||
# 'instances of BaseConf')
|
||||
|
||||
|
||||
class ConfWithIdListener(BaseConfListener):
|
||||
|
||||
def __init__(self, conf_with_id):
|
||||
assert conf_with_id
|
||||
super(ConfWithIdListener, self).__init__(conf_with_id)
|
||||
conf_with_id.add_listener(ConfWithId.UPDATE_NAME_EVT,
|
||||
self.on_chg_name_conf_with_id)
|
||||
conf_with_id.add_listener(ConfWithId.UPDATE_DESCRIPTION_EVT,
|
||||
self.on_chg_desc_conf_with_id)
|
||||
|
||||
def on_chg_name_conf_with_id(self, conf_evt):
|
||||
# Note did not makes this method abstract as this is not important
|
||||
# event.
|
||||
raise NotImplementedError()
|
||||
|
||||
def on_chg_desc_conf_with_id(self, conf_evt):
|
||||
# Note did not makes this method abstract as this is not important
|
||||
# event.
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class ConfWithStatsListener(BaseConfListener):
|
||||
|
||||
def __init__(self, conf_with_stats):
|
||||
assert conf_with_stats
|
||||
super(ConfWithStatsListener, self).__init__(conf_with_stats)
|
||||
conf_with_stats.add_listener(
|
||||
ConfWithStats.UPDATE_STATS_LOG_ENABLED_EVT,
|
||||
self.on_chg_stats_enabled_conf_with_stats)
|
||||
|
||||
conf_with_stats.add_listener(ConfWithStats.UPDATE_STATS_TIME_EVT,
|
||||
self.on_chg_stats_time_conf_with_stats)
|
||||
|
||||
@abstractmethod
|
||||
def on_chg_stats_time_conf_with_stats(self, conf_evt):
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
def on_chg_stats_enabled_conf_with_stats(self, conf_evt):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class ConfEvent(object):
|
||||
"""Encapsulates configuration settings change/update event."""
|
||||
|
||||
def __init__(self, evt_src, evt_name, evt_value):
|
||||
"""Creates an instance using given parameters.
|
||||
|
||||
Parameters:
|
||||
-`evt_src`: (BaseConf) source of the event
|
||||
-`evt_name`: (str) name of event, has to be one of the valid
|
||||
event of `evt_src`
|
||||
- `evt_value`: (tuple) event context that helps event handler
|
||||
"""
|
||||
if evt_name not in evt_src.get_valid_evts():
|
||||
raise ValueError('Event %s is not a valid event for type %s.' %
|
||||
(evt_name, type(evt_src)))
|
||||
self._src = evt_src
|
||||
self._name = evt_name
|
||||
self._value = evt_value
|
||||
|
||||
@property
|
||||
def src(self):
|
||||
return self._src
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
return self._value
|
||||
|
||||
def __repr__(self):
|
||||
return '<ConfEvent(%s, %s, %s)>' % (self.src, self.name, self.value)
|
||||
|
||||
def __str__(self):
|
||||
return ('ConfEvent(src=%s, name=%s, value=%s)' %
|
||||
(self.src, self.name, self.value))
|
||||
|
||||
def __cmp__(self, other):
|
||||
return cmp((other.src, other.name, other.value),
|
||||
(self.src, self.name, self.value))
|
||||
|
||||
|
||||
#==============================================================================
|
||||
# Runtime configuration setting validators and their registry.
|
||||
#==============================================================================
|
||||
|
||||
@validate(name=ConfWithId.ID)
|
||||
def validate_conf_id(identifier):
|
||||
if not isinstance(identifier, str):
|
||||
raise ConfigTypeError(conf_name=ConfWithId.ID, conf_value=identifier)
|
||||
if len(identifier) > 128:
|
||||
raise ConfigValueError(conf_name=ConfWithId.ID, conf_value=identifier)
|
||||
return identifier
|
||||
|
||||
|
||||
@validate(name=ConfWithId.NAME)
|
||||
def validate_conf_name(name):
|
||||
if not isinstance(name, str):
|
||||
raise ConfigTypeError(conf_name=ConfWithId.NAME, conf_value=name)
|
||||
if len(name) > 128:
|
||||
raise ConfigValueError(conf_name=ConfWithId.NAME, conf_value=name)
|
||||
return name
|
||||
|
||||
|
||||
@validate(name=ConfWithId.DESCRIPTION)
|
||||
def validate_conf_desc(description):
|
||||
if not isinstance(description, str):
|
||||
raise ConfigTypeError(conf_name=ConfWithId.DESCRIPTION,
|
||||
conf_value=description)
|
||||
return description
|
||||
|
||||
|
||||
@validate(name=ConfWithStats.STATS_LOG_ENABLED)
|
||||
def validate_stats_log_enabled(stats_log_enabled):
|
||||
if stats_log_enabled not in (True, False):
|
||||
raise ConfigTypeError(desc='Statistics log enabled settings can only'
|
||||
' be boolean type.')
|
||||
return stats_log_enabled
|
||||
|
||||
|
||||
@validate(name=ConfWithStats.STATS_TIME)
|
||||
def validate_stats_time(stats_time):
|
||||
if not isinstance(stats_time, (int, long)):
|
||||
raise ConfigTypeError(desc='Statistics log timer value has to be of '
|
||||
'type int/long but got: %r' % stats_time)
|
||||
if stats_time < 10:
|
||||
raise ConfigValueError(desc='Statistics log timer cannot be set to '
|
||||
'less then 10 sec, given timer value %s.' %
|
||||
stats_time)
|
||||
return stats_time
|
||||
|
||||
|
||||
@validate(name=CAP_REFRESH)
|
||||
def validate_cap_refresh(crefresh):
|
||||
if crefresh not in (True, False):
|
||||
raise ConfigTypeError(desc='Invalid Refresh capability settings: %s '
|
||||
' boolean value expected' % crefresh)
|
||||
return crefresh
|
||||
|
||||
|
||||
@validate(name=CAP_ENHANCED_REFRESH)
|
||||
def validate_cap_enhanced_refresh(cer):
|
||||
if cer not in (True, False):
|
||||
raise ConfigTypeError(desc='Invalid Enhanced Refresh capability '
|
||||
'settings: %s boolean value expected' % cer)
|
||||
return cer
|
||||
|
||||
|
||||
@validate(name=CAP_MBGP_VPNV4)
|
||||
def validate_cap_mbgp_vpnv4(cmv4):
|
||||
if cmv4 not in (True, False):
|
||||
raise ConfigTypeError(desc='Invalid Enhanced Refresh capability '
|
||||
'settings: %s boolean value expected' % cmv4)
|
||||
|
||||
return cmv4
|
||||
|
||||
|
||||
@validate(name=CAP_MBGP_VPNV6)
|
||||
def validate_cap_mbgp_vpnv6(cmv6):
|
||||
if cmv6 not in (True, False):
|
||||
raise ConfigTypeError(desc='Invalid Enhanced Refresh capability '
|
||||
'settings: %s boolean value expected' % cmv6)
|
||||
|
||||
return cmv6
|
||||
|
||||
|
||||
@validate(name=CAP_RTC)
|
||||
def validate_cap_rtc(cap_rtc):
|
||||
if cap_rtc not in (True, False):
|
||||
raise ConfigTypeError(desc='Invalid type for specifying RTC '
|
||||
'capability. Expected boolean got: %s' %
|
||||
type(cap_rtc))
|
||||
return cap_rtc
|
||||
|
||||
|
||||
@validate(name=RTC_AS)
|
||||
def validate_cap_rtc_as(rtc_as):
|
||||
if not is_valid_old_asn(rtc_as):
|
||||
raise ConfigValueError(desc='Invalid RTC AS configuration value: %s'
|
||||
% rtc_as)
|
||||
return rtc_as
|
||||
|
||||
|
||||
@validate(name=HOLD_TIME)
|
||||
def validate_hold_time(hold_time):
|
||||
if ((hold_time is None) or (not isinstance(hold_time, IntType)) or
|
||||
hold_time < 10):
|
||||
raise ConfigValueError(desc='Invalid hold_time configuration value %s'
|
||||
% hold_time)
|
||||
|
||||
return hold_time
|
||||
|
||||
|
||||
@validate(name=MULTI_EXIT_DISC)
|
||||
def validate_med(med):
|
||||
if med is not None and not validation.is_valid_med(med):
|
||||
raise ConfigValueError(desc='Invalid multi-exit-discriminatory (med)'
|
||||
' value: %s.' % med)
|
||||
return med
|
||||
|
||||
|
||||
@validate(name=SITE_OF_ORIGINS)
|
||||
def validate_soo_list(soo_list):
|
||||
if not isinstance(soo_list, list):
|
||||
raise ConfigTypeError(conf_name=SITE_OF_ORIGINS, conf_value=soo_list)
|
||||
if not (len(soo_list) <= MAX_NUM_SOO):
|
||||
raise ConfigValueError(desc='Max. SOO is limited to %s' %
|
||||
MAX_NUM_SOO)
|
||||
try:
|
||||
ExtCommunity.validate_supported_attributes(soo_list)
|
||||
except ValueError:
|
||||
raise ConfigValueError(conf_name=SITE_OF_ORIGINS,
|
||||
conf_value=soo_list)
|
||||
# Check if we have duplicates
|
||||
unique_rts = set(soo_list)
|
||||
if len(unique_rts) != len(soo_list):
|
||||
raise ConfigValueError(desc='Duplicate value provided in %s' %
|
||||
(soo_list))
|
||||
return soo_list
|
||||
|
||||
|
||||
@validate(name=MAX_PREFIXES)
|
||||
def validate_max_prefixes(max_prefixes):
|
||||
if not isinstance(max_prefixes, (IntType, LongType)):
|
||||
raise ConfigTypeError(desc='Max. prefixes value should be of type '
|
||||
'int or long but found %s' % type(max_prefixes))
|
||||
if max_prefixes < 0:
|
||||
raise ConfigValueError(desc='Invalid max. prefixes value: %s' %
|
||||
max_prefixes)
|
||||
return max_prefixes
|
||||
|
||||
|
||||
@validate(name=ADVERTISE_PEER_AS)
|
||||
def validate_advertise_peer_as(advertise_peer_as):
|
||||
if not isinstance(advertise_peer_as, BooleanType):
|
||||
raise ConfigTypeError(desc='Invalid type for advertise-peer-as, '
|
||||
'expected bool got %s' %
|
||||
type(advertise_peer_as))
|
||||
return advertise_peer_as
|
||||
|
||||
|
||||
#==============================================================================
|
||||
# Other utils.
|
||||
#==============================================================================
|
||||
|
||||
def compute_optional_conf(conf_name, default_value, **all_config):
|
||||
"""Returns *conf_name* settings if provided in *all_config*, else returns
|
||||
*default_value*.
|
||||
|
||||
Validates *conf_name* value if provided.
|
||||
"""
|
||||
conf_value = all_config.get(conf_name)
|
||||
if conf_value is not None:
|
||||
# Validate configuration value.
|
||||
get_validator(conf_name)(conf_value)
|
||||
else:
|
||||
conf_value = default_value
|
||||
return conf_value
|
||||
334
ryu/services/protocols/bgp/rtconf/common.py
Normal file
334
ryu/services/protocols/bgp/rtconf/common.py
Normal file
@ -0,0 +1,334 @@
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Runtime configuration that applies to all bgp sessions, i.e. global settings.
|
||||
"""
|
||||
import logging
|
||||
|
||||
from ryu.services.protocols.bgp.utils.validation import is_valid_ipv4
|
||||
from ryu.services.protocols.bgp.utils.validation import is_valid_old_asn
|
||||
|
||||
from ryu.services.protocols.bgp import rtconf
|
||||
from ryu.services.protocols.bgp.rtconf.base import BaseConf
|
||||
from ryu.services.protocols.bgp.rtconf.base import BaseConfListener
|
||||
from ryu.services.protocols.bgp.rtconf.base import compute_optional_conf
|
||||
from ryu.services.protocols.bgp.rtconf.base import ConfigTypeError
|
||||
from ryu.services.protocols.bgp.rtconf.base import ConfigValueError
|
||||
from ryu.services.protocols.bgp.rtconf.base import MissingRequiredConf
|
||||
from ryu.services.protocols.bgp.rtconf.base import validate
|
||||
|
||||
LOG = logging.getLogger('bgpspeaker.rtconf.common')
|
||||
|
||||
|
||||
# Global configuration settings.
|
||||
LOCAL_AS = 'local_as'
|
||||
ROUTER_ID = 'router_id'
|
||||
LABEL_RANGE = 'label_range'
|
||||
LABEL_RANGE_MAX = 'max'
|
||||
LABEL_RANGE_MIN = 'min'
|
||||
|
||||
# Configuration that can be set at global level as well as per context
|
||||
# (session/vrf) level
|
||||
# Nested configuration override global or higher level configuration as they
|
||||
# are more granular.
|
||||
# TODO(apgw-dev) Nested configuration overriding higher level configuration is
|
||||
# currently low priority
|
||||
|
||||
# Similar to Cisco command 'bgp refresh stalepath-time'. To cause the router to
|
||||
# remove stale routes from the BGP table even if the router does not receive a
|
||||
# Route-Refresh EOR message The bgp refresh stalepath-time command is not
|
||||
# needed under normal circumstances.
|
||||
# TODO(PH): Support this feature (currently low priority)
|
||||
REFRESH_STALEPATH_TIME = 'refresh_stalepath_time'
|
||||
|
||||
# Similar to Cisco command 'bgp refresh max-eor-time'. The bgp refresh max-eor-
|
||||
# time command is not needed under normal circumstances. You might configure
|
||||
# the bgp refresh max-eor-time command in the event of continuous route
|
||||
# flapping, when the router is unable to generate a Route- Refresh EOR message,
|
||||
# in which case a Route-Refresh EOR is generated after the timer expires.
|
||||
# TODO(PH): Support this feature (currently low priority)
|
||||
REFRESH_MAX_EOR_TIME = 'refresh_max_eor_time'
|
||||
|
||||
BGP_CONN_RETRY_TIME = 'bgp_conn_retry_time'
|
||||
BGP_SERVER_PORT = 'bgp_server_port'
|
||||
TCP_CONN_TIMEOUT = 'tcp_conn_timeout'
|
||||
MAX_PATH_EXT_RTFILTER_ALL = 'maximum_paths_external_rtfilter_all'
|
||||
|
||||
|
||||
# Valid default values of some settings.
|
||||
DEFAULT_LABEL_RANGE = (100, 100000)
|
||||
DEFAULT_REFRESH_STALEPATH_TIME = 0
|
||||
DEFAULT_REFRESH_MAX_EOR_TIME = 0
|
||||
DEFAULT_BGP_SERVER_PORT = 179
|
||||
DEFAULT_TCP_CONN_TIMEOUT = 30
|
||||
DEFAULT_BGP_CONN_RETRY_TIME = 30
|
||||
DEFAULT_MED = 0
|
||||
DEFAULT_MAX_PATH_EXT_RTFILTER_ALL = True
|
||||
|
||||
|
||||
@validate(name=LOCAL_AS)
|
||||
def validate_local_as(asn):
|
||||
if asn is None:
|
||||
raise MissingRequiredConf(conf_name=LOCAL_AS)
|
||||
|
||||
if not is_valid_old_asn(asn):
|
||||
raise ConfigValueError(desc='Invalid local_as configuration value: %s'
|
||||
% asn)
|
||||
return asn
|
||||
|
||||
|
||||
@validate(name=ROUTER_ID)
|
||||
def validate_router_id(router_id):
|
||||
if not router_id:
|
||||
raise MissingRequiredConf(conf_name=ROUTER_ID)
|
||||
|
||||
if not isinstance(router_id, str):
|
||||
raise ConfigTypeError(conf_name=ROUTER_ID)
|
||||
if not is_valid_ipv4(router_id):
|
||||
raise ConfigValueError(desc='Invalid router id %s' % router_id)
|
||||
|
||||
return router_id
|
||||
|
||||
|
||||
@validate(name=REFRESH_STALEPATH_TIME)
|
||||
def validate_refresh_stalepath_time(rst):
|
||||
if not isinstance(rst, (int, long)):
|
||||
raise ConfigTypeError(desc=('Configuration value for %s has to be '
|
||||
'int/long' % REFRESH_STALEPATH_TIME))
|
||||
if rst < 0:
|
||||
raise ConfigValueError(desc='Invalid refresh stalepath time %s' % rst)
|
||||
|
||||
return rst
|
||||
|
||||
|
||||
@validate(name=REFRESH_MAX_EOR_TIME)
|
||||
def validate_refresh_max_eor_time(rmet):
|
||||
if not isinstance(rmet, (int, long)):
|
||||
raise ConfigTypeError(desc=('Configuration value for %s has to be of '
|
||||
'type int/long ' % REFRESH_MAX_EOR_TIME))
|
||||
if rmet < 0:
|
||||
raise ConfigValueError(desc='Invalid refresh stalepath time %s' % rmet)
|
||||
|
||||
return rmet
|
||||
|
||||
|
||||
@validate(name=LABEL_RANGE)
|
||||
def validate_label_range(label_range):
|
||||
min_label, max_label = label_range
|
||||
if (not min_label or not max_label
|
||||
or not isinstance(min_label, (int, long))
|
||||
or not isinstance(max_label, (int, long)) or min_label < 17
|
||||
or min_label >= max_label):
|
||||
raise ConfigValueError(desc=('Invalid label_range configuration value:'
|
||||
' (%s).' % label_range))
|
||||
|
||||
return label_range
|
||||
|
||||
|
||||
@validate(name=BGP_SERVER_PORT)
|
||||
def validate_bgp_server_port(server_port):
|
||||
if not isinstance(server_port, (int, long)):
|
||||
raise ConfigTypeError(desc=('Invalid bgp sever port configuration '
|
||||
'value %s' % server_port))
|
||||
if server_port <= 0 or server_port > 65535:
|
||||
raise ConfigValueError(desc='Invalid server port %s' % server_port)
|
||||
|
||||
return server_port
|
||||
|
||||
|
||||
@validate(name=TCP_CONN_TIMEOUT)
|
||||
def validate_tcp_conn_timeout(tcp_conn_timeout):
|
||||
# TODO(apgw-dev) made-up some valid values for this settings, check if we
|
||||
# have a standard value in any routers
|
||||
if not isinstance(tcp_conn_timeout, (int, long)):
|
||||
raise ConfigTypeError(desc=('Invalid tcp connection timeout '
|
||||
'configuration value %s' %
|
||||
tcp_conn_timeout))
|
||||
|
||||
if tcp_conn_timeout < 10:
|
||||
raise ConfigValueError(desc=('Invalid tcp connection timeout'
|
||||
' configuration value %s' %
|
||||
tcp_conn_timeout))
|
||||
|
||||
return tcp_conn_timeout
|
||||
|
||||
|
||||
@validate(name=BGP_CONN_RETRY_TIME)
|
||||
def validate_bgp_conn_retry_time(bgp_conn_retry_time):
|
||||
if not isinstance(bgp_conn_retry_time, (int, long)):
|
||||
raise ConfigTypeError(desc=('Invalid bgp conn. retry time '
|
||||
'configuration value %s' %
|
||||
bgp_conn_retry_time))
|
||||
|
||||
if bgp_conn_retry_time < 10:
|
||||
raise ConfigValueError(desc=('Invalid bgp connection retry time'
|
||||
' configuration value %s' %
|
||||
bgp_conn_retry_time))
|
||||
return bgp_conn_retry_time
|
||||
|
||||
|
||||
@validate(name=MAX_PATH_EXT_RTFILTER_ALL)
|
||||
def validate_max_path_ext_rtfilter_all(max_path_ext_rtfilter_all):
|
||||
if max_path_ext_rtfilter_all not in (True, False):
|
||||
raise ConfigTypeError(desc=('Invalid max_path_ext_rtfilter_all'
|
||||
' configuration value %s' %
|
||||
max_path_ext_rtfilter_all))
|
||||
return max_path_ext_rtfilter_all
|
||||
|
||||
|
||||
class CommonConf(BaseConf):
|
||||
"""Encapsulates configurations applicable to all peer sessions.
|
||||
|
||||
Currently if any of these configurations change, it is assumed that current
|
||||
active peer session will be bought down and restarted.
|
||||
"""
|
||||
CONF_CHANGED_EVT = 1
|
||||
|
||||
VALID_EVT = frozenset([CONF_CHANGED_EVT])
|
||||
|
||||
REQUIRED_SETTINGS = frozenset([ROUTER_ID, LOCAL_AS])
|
||||
|
||||
OPTIONAL_SETTINGS = frozenset([REFRESH_STALEPATH_TIME,
|
||||
REFRESH_MAX_EOR_TIME,
|
||||
LABEL_RANGE, BGP_SERVER_PORT,
|
||||
TCP_CONN_TIMEOUT,
|
||||
BGP_CONN_RETRY_TIME,
|
||||
MAX_PATH_EXT_RTFILTER_ALL])
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(CommonConf, self).__init__(**kwargs)
|
||||
|
||||
def _init_opt_settings(self, **kwargs):
|
||||
super(CommonConf, self)._init_opt_settings(**kwargs)
|
||||
self._settings[LABEL_RANGE] = compute_optional_conf(
|
||||
LABEL_RANGE, DEFAULT_LABEL_RANGE, **kwargs)
|
||||
self._settings[REFRESH_STALEPATH_TIME] = compute_optional_conf(
|
||||
REFRESH_STALEPATH_TIME, DEFAULT_REFRESH_STALEPATH_TIME, **kwargs)
|
||||
self._settings[REFRESH_MAX_EOR_TIME] = compute_optional_conf(
|
||||
REFRESH_MAX_EOR_TIME, DEFAULT_REFRESH_MAX_EOR_TIME, **kwargs)
|
||||
self._settings[BGP_SERVER_PORT] = compute_optional_conf(
|
||||
BGP_SERVER_PORT, DEFAULT_BGP_SERVER_PORT, **kwargs)
|
||||
self._settings[TCP_CONN_TIMEOUT] = compute_optional_conf(
|
||||
TCP_CONN_TIMEOUT, DEFAULT_TCP_CONN_TIMEOUT, **kwargs)
|
||||
self._settings[BGP_CONN_RETRY_TIME] = compute_optional_conf(
|
||||
BGP_CONN_RETRY_TIME, DEFAULT_BGP_CONN_RETRY_TIME, **kwargs)
|
||||
self._settings[MAX_PATH_EXT_RTFILTER_ALL] = compute_optional_conf(
|
||||
MAX_PATH_EXT_RTFILTER_ALL, DEFAULT_MAX_PATH_EXT_RTFILTER_ALL,
|
||||
**kwargs)
|
||||
|
||||
#==========================================================================
|
||||
# Required attributes
|
||||
#==========================================================================
|
||||
|
||||
@property
|
||||
def local_as(self):
|
||||
return self._settings[LOCAL_AS]
|
||||
|
||||
@property
|
||||
def router_id(self):
|
||||
return self._settings[ROUTER_ID]
|
||||
|
||||
#==========================================================================
|
||||
# Optional attributes with valid defaults.
|
||||
#==========================================================================
|
||||
|
||||
@property
|
||||
def bgp_conn_retry_time(self):
|
||||
return self._settings[BGP_CONN_RETRY_TIME]
|
||||
|
||||
@property
|
||||
def tcp_conn_timeout(self):
|
||||
return self._settings[TCP_CONN_TIMEOUT]
|
||||
|
||||
@property
|
||||
def refresh_stalepath_time(self):
|
||||
return self._settings[REFRESH_STALEPATH_TIME]
|
||||
|
||||
@property
|
||||
def refresh_max_eor_time(self):
|
||||
return self._settings[REFRESH_MAX_EOR_TIME]
|
||||
|
||||
@property
|
||||
def label_range(self):
|
||||
return self._settings[LABEL_RANGE]
|
||||
|
||||
@property
|
||||
def bgp_server_port(self):
|
||||
return self._settings[BGP_SERVER_PORT]
|
||||
|
||||
@property
|
||||
def max_path_ext_rtfilter_all(self):
|
||||
return self._settings[MAX_PATH_EXT_RTFILTER_ALL]
|
||||
|
||||
@classmethod
|
||||
def get_opt_settings(self):
|
||||
self_confs = super(CommonConf, self).get_opt_settings()
|
||||
self_confs.update(CommonConf.OPTIONAL_SETTINGS)
|
||||
return self_confs
|
||||
|
||||
@classmethod
|
||||
def get_req_settings(self):
|
||||
self_confs = super(CommonConf, self).get_req_settings()
|
||||
self_confs.update(CommonConf.REQUIRED_SETTINGS)
|
||||
return self_confs
|
||||
|
||||
@classmethod
|
||||
def get_valid_evts(self):
|
||||
self_valid_evts = super(CommonConf, self).get_valid_evts()
|
||||
self_valid_evts.update(CommonConf.VALID_EVT)
|
||||
return self_valid_evts
|
||||
|
||||
def update(self, **kwargs):
|
||||
"""Updates global configuration settings with given values.
|
||||
|
||||
First checks if given configuration values differ from current values.
|
||||
If any of the configuration values changed, generates a change event.
|
||||
Currently we generate change event for any configuration change.
|
||||
Note: This method is idempotent.
|
||||
"""
|
||||
# Update inherited configurations
|
||||
super(CommonConf, self).update(**kwargs)
|
||||
conf_changed = False
|
||||
|
||||
# Validate given configurations and check if value changed
|
||||
for conf_name, conf_value in kwargs.items():
|
||||
rtconf.base.get_validator(conf_name)(conf_value)
|
||||
item1 = self._settings.get(conf_name, None)
|
||||
item2 = kwargs.get(conf_name, None)
|
||||
|
||||
if item1 != item2:
|
||||
conf_changed = True
|
||||
|
||||
# If any configuration changed, we update configuration value and
|
||||
# notify listeners
|
||||
if conf_changed:
|
||||
for conf_name, conf_value in kwargs.items():
|
||||
# Since all new values are already validated, we can use them
|
||||
self._settings[conf_name] = conf_value
|
||||
|
||||
self._notify_listeners(CommonConf.CONF_CHANGED_EVT, self)
|
||||
|
||||
|
||||
class CommonConfListener(BaseConfListener):
|
||||
"""Base listener for various changes to common configurations."""
|
||||
|
||||
def __init__(self, global_conf):
|
||||
super(CommonConfListener, self).__init__(global_conf)
|
||||
global_conf.add_listener(CommonConf.CONF_CHANGED_EVT,
|
||||
self.on_update_common_conf)
|
||||
|
||||
def on_update_common_conf(self, evt):
|
||||
raise NotImplementedError('This method should be overridden.')
|
||||
469
ryu/services/protocols/bgp/rtconf/neighbors.py
Normal file
469
ryu/services/protocols/bgp/rtconf/neighbors.py
Normal file
@ -0,0 +1,469 @@
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Running or runtime configuration related to bgp peers/neighbors.
|
||||
"""
|
||||
from abc import abstractmethod
|
||||
import logging
|
||||
|
||||
from ryu.services.protocols.bgp.base import OrderedDict
|
||||
from ryu.services.protocols.bgp.rtconf.base import ADVERTISE_PEER_AS
|
||||
from ryu.services.protocols.bgp.rtconf.base import BaseConf
|
||||
from ryu.services.protocols.bgp.rtconf.base import BaseConfListener
|
||||
from ryu.services.protocols.bgp.rtconf.base import CAP_ENHANCED_REFRESH
|
||||
from ryu.services.protocols.bgp.rtconf.base import CAP_MBGP_VPNV4
|
||||
from ryu.services.protocols.bgp.rtconf.base import CAP_MBGP_VPNV6
|
||||
from ryu.services.protocols.bgp.rtconf.base import CAP_REFRESH
|
||||
from ryu.services.protocols.bgp.rtconf.base import CAP_RTC
|
||||
from ryu.services.protocols.bgp.rtconf.base import compute_optional_conf
|
||||
from ryu.services.protocols.bgp.rtconf.base import ConfigTypeError
|
||||
from ryu.services.protocols.bgp.rtconf.base import ConfigValueError
|
||||
from ryu.services.protocols.bgp.rtconf.base import ConfWithId
|
||||
from ryu.services.protocols.bgp.rtconf.base import ConfWithIdListener
|
||||
from ryu.services.protocols.bgp.rtconf.base import ConfWithStats
|
||||
from ryu.services.protocols.bgp.rtconf.base import ConfWithStatsListener
|
||||
from ryu.services.protocols.bgp.rtconf.base import HOLD_TIME
|
||||
from ryu.services.protocols.bgp.rtconf.base import MAX_PREFIXES
|
||||
from ryu.services.protocols.bgp.rtconf.base import MULTI_EXIT_DISC
|
||||
from ryu.services.protocols.bgp.rtconf.base import RTC_AS
|
||||
from ryu.services.protocols.bgp.rtconf.base import RuntimeConfigError
|
||||
from ryu.services.protocols.bgp.rtconf.base import SITE_OF_ORIGINS
|
||||
from ryu.services.protocols.bgp.rtconf.base import validate
|
||||
from ryu.services.protocols.bgp.rtconf.base import validate_med
|
||||
from ryu.services.protocols.bgp.rtconf.base import validate_soo_list
|
||||
|
||||
from ryu.services.protocols.bgp.protocols.bgp.capabilities import \
|
||||
EnhancedRouteRefreshCap
|
||||
from ryu.services.protocols.bgp.protocols.bgp.capabilities import \
|
||||
MultiprotocolExtentionCap
|
||||
from ryu.services.protocols.bgp.protocols.bgp.capabilities import \
|
||||
RouteRefreshCap
|
||||
from ryu.services.protocols.bgp.protocols.bgp.nlri import RF_IPv4_VPN
|
||||
from ryu.services.protocols.bgp.protocols.bgp.nlri import RF_IPv6_VPN
|
||||
from ryu.services.protocols.bgp.protocols.bgp.nlri import RF_RTC_UC
|
||||
from ryu.services.protocols.bgp.utils.validation import is_valid_ipv4
|
||||
from ryu.services.protocols.bgp.utils.validation import is_valid_old_asn
|
||||
|
||||
LOG = logging.getLogger('bgpspeaker.rtconf.neighbor')
|
||||
|
||||
# Various neighbor settings.
|
||||
REMOTE_AS = 'remote_as'
|
||||
IP_ADDRESS = 'ip_address'
|
||||
ENABLED = 'enabled'
|
||||
CHANGES = 'changes'
|
||||
LOCAL_ADDRESS = 'local_address'
|
||||
LOCAL_PORT = 'local_port'
|
||||
|
||||
# Default value constants.
|
||||
DEFAULT_CAP_GR_NULL = True
|
||||
DEFAULT_CAP_REFRESH = True
|
||||
DEFAULT_CAP_ENHANCED_REFRESH = True
|
||||
DEFAULT_CAP_MBGP_VPNV4 = True
|
||||
DEFAULT_CAP_MBGP_VPNV6 = False
|
||||
DEFAULT_HOLD_TIME = 40
|
||||
DEFAULT_ENABLED = True
|
||||
DEFAULT_CAP_RTC = True
|
||||
|
||||
# Default value for *MAX_PREFIXES* setting is set to 0.
|
||||
DEFAULT_MAX_PREFIXES = 0
|
||||
DEFAULT_ADVERTISE_PEER_AS = False
|
||||
|
||||
|
||||
@validate(name=ENABLED)
|
||||
def validate_enabled(enabled):
|
||||
if not isinstance(enabled, bool):
|
||||
raise ConfigValueError(desc='Enable property is not an instance of '
|
||||
'boolean')
|
||||
return enabled
|
||||
|
||||
|
||||
@validate(name=CHANGES)
|
||||
def validate_changes(changes):
|
||||
for k, v in changes.iteritems():
|
||||
if k not in (MULTI_EXIT_DISC, ENABLED):
|
||||
raise ConfigValueError(desc="Unknown field to change: %s" % k)
|
||||
|
||||
if k == MULTI_EXIT_DISC:
|
||||
validate_med(v)
|
||||
elif k == ENABLED:
|
||||
validate_enabled(v)
|
||||
return changes
|
||||
|
||||
|
||||
@validate(name=IP_ADDRESS)
|
||||
def validate_ip_address(ip_address):
|
||||
if not is_valid_ipv4(ip_address):
|
||||
raise ConfigValueError(desc='Invalid neighbor ip_address: %s' %
|
||||
ip_address)
|
||||
return ip_address
|
||||
|
||||
|
||||
@validate(name=LOCAL_ADDRESS)
|
||||
def validate_local_address(ip_address):
|
||||
if not is_valid_ipv4(ip_address):
|
||||
raise ConfigValueError(desc='Invalid local ip_address: %s' %
|
||||
ip_address)
|
||||
return ip_address
|
||||
|
||||
|
||||
@validate(name=LOCAL_PORT)
|
||||
def validate_local_port(port):
|
||||
if not isinstance(port, (int, long)):
|
||||
raise ConfigTypeError(desc='Invalid local port: %s' % port)
|
||||
if port < 1025 or port > 65535:
|
||||
raise ConfigValueError(desc='Invalid local port value: %s, has to be'
|
||||
' between 1025 and 65535' % port)
|
||||
return port
|
||||
|
||||
|
||||
@validate(name=REMOTE_AS)
|
||||
def validate_remote_as(asn):
|
||||
if not is_valid_old_asn(asn):
|
||||
raise ConfigValueError(desc='Invalid remote as value %s' % asn)
|
||||
return asn
|
||||
|
||||
|
||||
class NeighborConf(ConfWithId, ConfWithStats):
|
||||
"""Class that encapsulates one neighbors' configuration."""
|
||||
|
||||
UPDATE_ENABLED_EVT = 'update_enabled_evt'
|
||||
UPDATE_MED_EVT = 'update_med_evt'
|
||||
|
||||
VALID_EVT = frozenset([UPDATE_ENABLED_EVT, UPDATE_MED_EVT])
|
||||
REQUIRED_SETTINGS = frozenset([REMOTE_AS, IP_ADDRESS, LOCAL_ADDRESS,
|
||||
LOCAL_PORT])
|
||||
OPTIONAL_SETTINGS = frozenset([CAP_REFRESH,
|
||||
CAP_ENHANCED_REFRESH, CAP_MBGP_VPNV4,
|
||||
CAP_MBGP_VPNV6, CAP_RTC, RTC_AS, HOLD_TIME,
|
||||
ENABLED, MULTI_EXIT_DISC, MAX_PREFIXES,
|
||||
ADVERTISE_PEER_AS, SITE_OF_ORIGINS])
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(NeighborConf, self).__init__(**kwargs)
|
||||
|
||||
def _init_opt_settings(self, **kwargs):
|
||||
self._settings[CAP_REFRESH] = compute_optional_conf(
|
||||
CAP_REFRESH, DEFAULT_CAP_REFRESH, **kwargs)
|
||||
self._settings[CAP_ENHANCED_REFRESH] = compute_optional_conf(
|
||||
CAP_ENHANCED_REFRESH, DEFAULT_CAP_ENHANCED_REFRESH, **kwargs)
|
||||
self._settings[CAP_MBGP_VPNV4] = compute_optional_conf(
|
||||
CAP_MBGP_VPNV4, DEFAULT_CAP_MBGP_VPNV4, **kwargs)
|
||||
self._settings[CAP_MBGP_VPNV6] = compute_optional_conf(
|
||||
CAP_MBGP_VPNV6, DEFAULT_CAP_MBGP_VPNV6, **kwargs)
|
||||
self._settings[HOLD_TIME] = compute_optional_conf(
|
||||
HOLD_TIME, DEFAULT_HOLD_TIME, **kwargs)
|
||||
self._settings[ENABLED] = compute_optional_conf(
|
||||
ENABLED, DEFAULT_ENABLED, **kwargs)
|
||||
self._settings[MAX_PREFIXES] = compute_optional_conf(
|
||||
MAX_PREFIXES, DEFAULT_MAX_PREFIXES, **kwargs)
|
||||
self._settings[ADVERTISE_PEER_AS] = compute_optional_conf(
|
||||
ADVERTISE_PEER_AS, DEFAULT_ADVERTISE_PEER_AS, **kwargs)
|
||||
|
||||
# We do not have valid default MED value.
|
||||
# If no MED attribute is provided then we do not have to use MED.
|
||||
# If MED attribute is provided we have to validate it and use it.
|
||||
med = kwargs.pop(MULTI_EXIT_DISC, None)
|
||||
if med and validate_med(med):
|
||||
self._settings[MULTI_EXIT_DISC] = med
|
||||
|
||||
# We do not have valid default SOO value.
|
||||
# If no SOO attribute is provided then we do not have to use SOO.
|
||||
# If SOO attribute is provided we have to validate it and use it.
|
||||
soos = kwargs.pop(SITE_OF_ORIGINS, None)
|
||||
if soos and validate_soo_list(soos):
|
||||
self._settings[SITE_OF_ORIGINS] = soos
|
||||
|
||||
# RTC configurations.
|
||||
self._settings[CAP_RTC] = \
|
||||
compute_optional_conf(CAP_RTC, DEFAULT_CAP_RTC, **kwargs)
|
||||
# Default RTC_AS is local (router) AS.
|
||||
from ryu.services.protocols.bgp.speaker.core_manager import \
|
||||
CORE_MANAGER
|
||||
default_rt_as = CORE_MANAGER.common_conf.local_as
|
||||
self._settings[RTC_AS] = \
|
||||
compute_optional_conf(RTC_AS, default_rt_as, **kwargs)
|
||||
|
||||
# Since ConfWithId' default values use str(self) and repr(self), we
|
||||
# call super method after we have initialized other settings.
|
||||
super(NeighborConf, self)._init_opt_settings(**kwargs)
|
||||
|
||||
@classmethod
|
||||
def get_opt_settings(cls):
|
||||
self_confs = super(NeighborConf, cls).get_opt_settings()
|
||||
self_confs.update(NeighborConf.OPTIONAL_SETTINGS)
|
||||
return self_confs
|
||||
|
||||
@classmethod
|
||||
def get_req_settings(cls):
|
||||
self_confs = super(NeighborConf, cls).get_req_settings()
|
||||
self_confs.update(NeighborConf.REQUIRED_SETTINGS)
|
||||
return self_confs
|
||||
|
||||
@classmethod
|
||||
def get_valid_evts(cls):
|
||||
self_valid_evts = super(NeighborConf, cls).get_valid_evts()
|
||||
self_valid_evts.update(NeighborConf.VALID_EVT)
|
||||
return self_valid_evts
|
||||
|
||||
#==========================================================================
|
||||
# Required attributes
|
||||
#==========================================================================
|
||||
|
||||
@property
|
||||
def remote_as(self):
|
||||
return self._settings[REMOTE_AS]
|
||||
|
||||
@property
|
||||
def ip_address(self):
|
||||
return self._settings[IP_ADDRESS]
|
||||
|
||||
@property
|
||||
def host_bind_ip(self):
|
||||
return self._settings[LOCAL_ADDRESS]
|
||||
|
||||
@property
|
||||
def host_bind_port(self):
|
||||
return self._settings[LOCAL_PORT]
|
||||
|
||||
#==========================================================================
|
||||
# Optional attributes with valid defaults.
|
||||
#==========================================================================
|
||||
|
||||
@property
|
||||
def hold_time(self):
|
||||
return self._settings[HOLD_TIME]
|
||||
|
||||
@property
|
||||
def cap_refresh(self):
|
||||
return self._settings[CAP_REFRESH]
|
||||
|
||||
@property
|
||||
def cap_enhanced_refresh(self):
|
||||
return self._settings[CAP_ENHANCED_REFRESH]
|
||||
|
||||
@property
|
||||
def cap_mbgp_vpnv4(self):
|
||||
return self._settings[CAP_MBGP_VPNV4]
|
||||
|
||||
@property
|
||||
def cap_mbgp_vpnv6(self):
|
||||
return self._settings[CAP_MBGP_VPNV6]
|
||||
|
||||
@property
|
||||
def cap_rtc(self):
|
||||
return self._settings[CAP_RTC]
|
||||
|
||||
@property
|
||||
def enabled(self):
|
||||
return self._settings[ENABLED]
|
||||
|
||||
@enabled.setter
|
||||
def enabled(self, enable):
|
||||
# Update enabled flag and notify listeners.
|
||||
if self._settings[ENABLED] != enable:
|
||||
self._settings[ENABLED] = enable
|
||||
self._notify_listeners(NeighborConf.UPDATE_ENABLED_EVT,
|
||||
enable)
|
||||
|
||||
#==========================================================================
|
||||
# Optional attributes with no valid defaults.
|
||||
#==========================================================================
|
||||
|
||||
@property
|
||||
def multi_exit_disc(self):
|
||||
# This property does not have any valid default. Hence if not set we
|
||||
# return None.
|
||||
return self._settings.get(MULTI_EXIT_DISC)
|
||||
|
||||
@multi_exit_disc.setter
|
||||
def multi_exit_disc(self, value):
|
||||
if self._settings.get(MULTI_EXIT_DISC) != value:
|
||||
self._settings[MULTI_EXIT_DISC] = value
|
||||
self._notify_listeners(NeighborConf.UPDATE_MED_EVT, value)
|
||||
|
||||
@property
|
||||
def soo_list(self):
|
||||
soos = self._settings.get(SITE_OF_ORIGINS)
|
||||
if soos:
|
||||
soos = list(soos)
|
||||
else:
|
||||
soos = []
|
||||
return soos
|
||||
|
||||
@property
|
||||
def rtc_as(self):
|
||||
return self._settings[RTC_AS]
|
||||
|
||||
def exceeds_max_prefix_allowed(self, prefix_count):
|
||||
allowed_max = self._settings[MAX_PREFIXES]
|
||||
does_exceed = False
|
||||
# Check if allowed max. is unlimited.
|
||||
if allowed_max != 0:
|
||||
# If max. prefix is limited, check if given exceeds this limit.
|
||||
if prefix_count > allowed_max:
|
||||
does_exceed = True
|
||||
|
||||
return does_exceed
|
||||
|
||||
def get_configured_capabilites(self):
|
||||
"""Returns configured capabilities."""
|
||||
|
||||
capabilities = OrderedDict()
|
||||
mbgp_caps = []
|
||||
if self.cap_mbgp_vpnv4:
|
||||
mbgp_caps.append(MultiprotocolExtentionCap(RF_IPv4_VPN))
|
||||
|
||||
if self.cap_mbgp_vpnv6:
|
||||
mbgp_caps.append(MultiprotocolExtentionCap(RF_IPv6_VPN))
|
||||
|
||||
if self.cap_rtc:
|
||||
mbgp_caps.append(MultiprotocolExtentionCap(RF_RTC_UC))
|
||||
|
||||
if mbgp_caps:
|
||||
capabilities[MultiprotocolExtentionCap.CODE] = mbgp_caps
|
||||
|
||||
if self.cap_refresh:
|
||||
capabilities[RouteRefreshCap.CODE] = [
|
||||
RouteRefreshCap.get_singleton()]
|
||||
|
||||
if self.cap_enhanced_refresh:
|
||||
capabilities[EnhancedRouteRefreshCap.CODE] = [
|
||||
EnhancedRouteRefreshCap.get_singleton()]
|
||||
|
||||
return capabilities
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s(%r, %r, %r)>' % (self.__class__.__name__,
|
||||
self.remote_as,
|
||||
self.ip_address,
|
||||
self.enabled)
|
||||
|
||||
def __str__(self):
|
||||
return 'Neighbor: %s' % (self.ip_address)
|
||||
|
||||
|
||||
class NeighborsConf(BaseConf):
|
||||
"""Container of all neighbor configurations."""
|
||||
|
||||
ADD_NEIGH_CONF_EVT = 'add_neigh_conf_evt'
|
||||
REMOVE_NEIGH_CONF_EVT = 'remove_neigh_conf_evt'
|
||||
|
||||
VALID_EVT = frozenset([ADD_NEIGH_CONF_EVT, REMOVE_NEIGH_CONF_EVT])
|
||||
|
||||
def __init__(self):
|
||||
super(NeighborsConf, self).__init__()
|
||||
self._neighbors = {}
|
||||
|
||||
def _init_opt_settings(self, **kwargs):
|
||||
pass
|
||||
|
||||
def update(self, **kwargs):
|
||||
raise NotImplementedError('Use either add/remove_neighbor_conf'
|
||||
' methods instead.')
|
||||
|
||||
@property
|
||||
def rtc_as_set(self):
|
||||
"""Returns current RTC AS configured for current neighbors.
|
||||
"""
|
||||
rtc_as_set = set()
|
||||
for neigh in self._neighbors.itervalues():
|
||||
rtc_as_set.add(neigh.rtc_as)
|
||||
return rtc_as_set
|
||||
|
||||
@classmethod
|
||||
def get_valid_evts(cls):
|
||||
self_valid_evts = super(NeighborsConf, cls).get_valid_evts()
|
||||
self_valid_evts.update(NeighborsConf.VALID_EVT)
|
||||
return self_valid_evts
|
||||
|
||||
def add_neighbor_conf(self, neigh_conf):
|
||||
# Check if we already know this neighbor
|
||||
if neigh_conf.ip_address in self._neighbors.keys():
|
||||
message = 'Neighbor with given ip address already exists'
|
||||
raise RuntimeConfigError(desc=message)
|
||||
|
||||
# Check if this neighbor's host address overlaps with other neighbors
|
||||
for nconf in self._neighbors.itervalues():
|
||||
if ((neigh_conf.host_bind_ip, neigh_conf.host_bind_port) ==
|
||||
(nconf.host_bind_ip, nconf.host_bind_port)):
|
||||
raise RuntimeConfigError(desc='Given host_bind_ip and '
|
||||
'host_bind_port already taken')
|
||||
|
||||
# Add this neighbor to known configured neighbors and generate update
|
||||
# event
|
||||
self._neighbors[neigh_conf.ip_address] = neigh_conf
|
||||
self._notify_listeners(NeighborsConf.ADD_NEIGH_CONF_EVT, neigh_conf)
|
||||
|
||||
def remove_neighbor_conf(self, neigh_ip_address):
|
||||
neigh_conf = self._neighbors.pop(neigh_ip_address, None)
|
||||
if not neigh_conf:
|
||||
raise RuntimeConfigError(desc='Tried to remove a neighbor that '
|
||||
'does not exists')
|
||||
else:
|
||||
self._notify_listeners(NeighborsConf.REMOVE_NEIGH_CONF_EVT,
|
||||
neigh_conf)
|
||||
return neigh_conf
|
||||
|
||||
def get_neighbor_conf(self, neigh_ip_address):
|
||||
return self._neighbors.get(neigh_ip_address, None)
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s(%r)>' % (self.__class__.__name__, self._neighbors)
|
||||
|
||||
def __str__(self):
|
||||
return '\'Neighbors\': %s' % self._neighbors
|
||||
|
||||
@property
|
||||
def settings(self):
|
||||
return [neighbor.settings for _, neighbor in
|
||||
self._neighbors.iteritems()]
|
||||
|
||||
|
||||
class NeighborConfListener(ConfWithIdListener, ConfWithStatsListener):
|
||||
"""Base listener for change events to a specific neighbors' configurations.
|
||||
"""
|
||||
def __init__(self, neigh_conf):
|
||||
super(NeighborConfListener, self).__init__(neigh_conf)
|
||||
neigh_conf.add_listener(NeighborConf.UPDATE_ENABLED_EVT,
|
||||
self.on_update_enabled)
|
||||
neigh_conf.add_listener(NeighborConf.UPDATE_MED_EVT,
|
||||
self.on_update_med)
|
||||
|
||||
@abstractmethod
|
||||
def on_update_enabled(self, evt):
|
||||
raise NotImplementedError('This method should be overridden.')
|
||||
|
||||
def on_update_med(self, evt):
|
||||
raise NotImplementedError('This method should be overridden.')
|
||||
|
||||
|
||||
class NeighborsConfListener(BaseConfListener):
|
||||
"""Base listener for change events to neighbor configuration container."""
|
||||
|
||||
def __init__(self, neighbors_conf):
|
||||
super(NeighborsConfListener, self).__init__(neighbors_conf)
|
||||
neighbors_conf.add_listener(NeighborsConf.ADD_NEIGH_CONF_EVT,
|
||||
self.on_add_neighbor_conf)
|
||||
neighbors_conf.add_listener(NeighborsConf.REMOVE_NEIGH_CONF_EVT,
|
||||
self.on_remove_neighbor_conf)
|
||||
|
||||
@abstractmethod
|
||||
def on_add_neighbor_conf(self, evt):
|
||||
raise NotImplementedError('This method should be overridden.')
|
||||
|
||||
@abstractmethod
|
||||
def on_remove_neighbor_conf(self, evt):
|
||||
raise NotImplementedError('This method should be overridden.')
|
||||
551
ryu/services/protocols/bgp/rtconf/vrfs.py
Normal file
551
ryu/services/protocols/bgp/rtconf/vrfs.py
Normal file
@ -0,0 +1,551 @@
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Running or runtime configuration related to Virtual Routing and Forwarding
|
||||
tables (VRFs).
|
||||
"""
|
||||
import abc
|
||||
import logging
|
||||
|
||||
from ryu.services.protocols.bgp.protocols.bgp.pathattr import ExtCommunity
|
||||
from ryu.services.protocols.bgp.utils import validation
|
||||
|
||||
from ryu.services.protocols.bgp.base import get_validator
|
||||
from ryu.services.protocols.bgp.rtconf.base import BaseConf
|
||||
from ryu.services.protocols.bgp.rtconf.base import BaseConfListener
|
||||
from ryu.services.protocols.bgp.rtconf.base import ConfigTypeError
|
||||
from ryu.services.protocols.bgp.rtconf.base import ConfigValueError
|
||||
from ryu.services.protocols.bgp.rtconf.base import ConfWithId
|
||||
from ryu.services.protocols.bgp.rtconf.base import ConfWithIdListener
|
||||
from ryu.services.protocols.bgp.rtconf.base import ConfWithStats
|
||||
from ryu.services.protocols.bgp.rtconf.base import ConfWithStatsListener
|
||||
from ryu.services.protocols.bgp.rtconf.base import MAX_NUM_EXPORT_RT
|
||||
from ryu.services.protocols.bgp.rtconf.base import MAX_NUM_IMPORT_RT
|
||||
from ryu.services.protocols.bgp.rtconf.base import MULTI_EXIT_DISC
|
||||
from ryu.services.protocols.bgp.rtconf.base import RuntimeConfigError
|
||||
from ryu.services.protocols.bgp.rtconf.base import SITE_OF_ORIGINS
|
||||
from ryu.services.protocols.bgp.rtconf.base import validate
|
||||
from ryu.services.protocols.bgp.rtconf.base import validate_med
|
||||
from ryu.services.protocols.bgp.rtconf.base import validate_soo_list
|
||||
from ryu.services.protocols.bgp.protocols.bgp.nlri import RF_IPv4_UC
|
||||
from ryu.services.protocols.bgp.protocols.bgp.nlri import RF_IPv6_UC
|
||||
|
||||
|
||||
LOG = logging.getLogger('bgpspeaker.rtconf.vrfs')
|
||||
|
||||
# Configuration setting names.
|
||||
ROUTE_DISTINGUISHER = 'route_dist'
|
||||
IMPORT_RTS = 'import_rts'
|
||||
EXPORT_RTS = 'export_rts'
|
||||
VRF_NAME = 'vrf_name'
|
||||
VRF_DESC = 'vrf_desc'
|
||||
VRF_RF = 'route_family'
|
||||
IMPORT_MAPS = 'import_maps'
|
||||
|
||||
# Two supported VRF route-families
|
||||
VRF_RF_IPV6 = 'ipv6'
|
||||
VRF_RF_IPV4 = 'ipv4'
|
||||
SUPPORTED_VRF_RF = (VRF_RF_IPV4, VRF_RF_IPV6)
|
||||
|
||||
|
||||
# Default configuration values.
|
||||
DEFAULT_VRF_NAME = 'no-vrf-name'
|
||||
DEFAULT_VRF_DESC = 'no-vrf-desc'
|
||||
|
||||
|
||||
@validate(name=IMPORT_RTS)
|
||||
def validate_import_rts(import_rts):
|
||||
if not isinstance(import_rts, list):
|
||||
raise ConfigTypeError(conf_name=IMPORT_RTS, conf_value=import_rts)
|
||||
if not (len(import_rts) <= MAX_NUM_IMPORT_RT):
|
||||
raise ConfigValueError(desc='Max. import RT is limited to %s' %
|
||||
MAX_NUM_IMPORT_RT)
|
||||
try:
|
||||
ExtCommunity.validate_supported_attributes(import_rts)
|
||||
except ValueError:
|
||||
raise ConfigValueError(conf_name=IMPORT_RTS,
|
||||
conf_value=import_rts)
|
||||
# Check if we have duplicates
|
||||
unique_rts = set(import_rts)
|
||||
if len(unique_rts) != len(import_rts):
|
||||
raise ConfigValueError(desc='Duplicate value provided %s' %
|
||||
(import_rts))
|
||||
|
||||
return import_rts
|
||||
|
||||
|
||||
@validate(name=EXPORT_RTS)
|
||||
def validate_export_rts(export_rts):
|
||||
if not isinstance(export_rts, list):
|
||||
raise ConfigTypeError(conf_name=EXPORT_RTS, conf_value=export_rts)
|
||||
if not (len(export_rts) <= MAX_NUM_EXPORT_RT):
|
||||
raise ConfigValueError(desc='Max. import RT is limited to %s' %
|
||||
MAX_NUM_EXPORT_RT)
|
||||
try:
|
||||
ExtCommunity.validate_supported_attributes(export_rts)
|
||||
except Exception:
|
||||
raise ConfigValueError(conf_name=EXPORT_RTS, conf_value=export_rts)
|
||||
# Check if we have duplicates
|
||||
unique_rts = set(export_rts)
|
||||
if len(unique_rts) != len(export_rts):
|
||||
raise ConfigValueError(desc='Duplicate value provided in %s' %
|
||||
(export_rts))
|
||||
return export_rts
|
||||
|
||||
|
||||
@validate(name=ROUTE_DISTINGUISHER)
|
||||
def valdiate_rd(route_disc):
|
||||
if not isinstance(route_disc, str):
|
||||
raise ConfigTypeError(conf_name=ROUTE_DISTINGUISHER,
|
||||
conf_value=route_disc)
|
||||
|
||||
if not validation.is_valid_route_disc(route_disc):
|
||||
raise ConfigValueError(conf_name=ROUTE_DISTINGUISHER,
|
||||
conf_value=route_disc)
|
||||
return route_disc
|
||||
|
||||
|
||||
@validate(name=VRF_RF)
|
||||
def validate_vrf_rf(vrf_rf):
|
||||
if vrf_rf not in SUPPORTED_VRF_RF:
|
||||
raise ConfigValueError(desc='Give VRF route family %s is not '
|
||||
'supported.' % vrf_rf)
|
||||
return vrf_rf
|
||||
|
||||
|
||||
class VrfConf(ConfWithId, ConfWithStats):
|
||||
"""Class that encapsulates configurations for one VRF."""
|
||||
|
||||
VRF_CHG_EVT = 'vrf_chg_evt'
|
||||
|
||||
VALID_EVT = frozenset([VRF_CHG_EVT])
|
||||
|
||||
REQUIRED_SETTINGS = frozenset([ROUTE_DISTINGUISHER,
|
||||
IMPORT_RTS,
|
||||
EXPORT_RTS])
|
||||
|
||||
OPTIONAL_SETTINGS = frozenset(
|
||||
[MULTI_EXIT_DISC, SITE_OF_ORIGINS, VRF_RF, IMPORT_MAPS]
|
||||
)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Create an instance of VRF runtime configuration."""
|
||||
super(VrfConf, self).__init__(**kwargs)
|
||||
|
||||
def _init_opt_settings(self, **kwargs):
|
||||
super(VrfConf, self)._init_opt_settings(**kwargs)
|
||||
# We do not have valid default MED value.
|
||||
# If no MED attribute is provided then we do not have to use MED.
|
||||
# If MED attribute is provided we have to validate it and use it.
|
||||
med = kwargs.pop(MULTI_EXIT_DISC, None)
|
||||
if med and validate_med(med):
|
||||
self._settings[MULTI_EXIT_DISC] = med
|
||||
|
||||
# We do not have valid default SOO value.
|
||||
# If no SOO attribute is provided then we do not have to use SOO.
|
||||
# If SOO attribute is provided we have to validate it and use it.
|
||||
soos = kwargs.pop(SITE_OF_ORIGINS, None)
|
||||
if soos and validate_soo_list(soos):
|
||||
self._settings[SITE_OF_ORIGINS] = soos
|
||||
|
||||
# Current we we only support VRF for IPv4 and IPv6 with default IPv4
|
||||
vrf_rf = kwargs.pop(VRF_RF, VRF_RF_IPV4)
|
||||
if vrf_rf and validate_vrf_rf(vrf_rf):
|
||||
self._settings[VRF_RF] = vrf_rf
|
||||
|
||||
import_maps = kwargs.pop(IMPORT_MAPS, [])
|
||||
self._settings[IMPORT_MAPS] = import_maps
|
||||
|
||||
#==========================================================================
|
||||
# Required attributes
|
||||
#==========================================================================
|
||||
|
||||
@property
|
||||
def route_dist(self):
|
||||
return self._settings[ROUTE_DISTINGUISHER]
|
||||
|
||||
#==========================================================================
|
||||
# Optional attributes with valid defaults.
|
||||
#==========================================================================
|
||||
|
||||
@property
|
||||
def import_rts(self):
|
||||
return list(self._settings[IMPORT_RTS])
|
||||
|
||||
@property
|
||||
def export_rts(self):
|
||||
return list(self._settings[EXPORT_RTS])
|
||||
|
||||
@property
|
||||
def soo_list(self):
|
||||
soos = self._settings.get(SITE_OF_ORIGINS)
|
||||
if soos:
|
||||
soos = list(soos)
|
||||
else:
|
||||
soos = []
|
||||
return soos
|
||||
|
||||
@property
|
||||
def multi_exit_disc(self):
|
||||
"""Returns configured value of MED, else None.
|
||||
|
||||
This configuration does not have default value.
|
||||
"""
|
||||
return self._settings.get(MULTI_EXIT_DISC)
|
||||
|
||||
@property
|
||||
def route_family(self):
|
||||
"""Returns configured route family for this VRF
|
||||
|
||||
This configuration does not change.
|
||||
"""
|
||||
return self._settings.get(VRF_RF)
|
||||
|
||||
@property
|
||||
def rd_rf_id(self):
|
||||
return VrfConf.create_rd_rf_id(self.route_dist, self.route_family)
|
||||
|
||||
@property
|
||||
def import_maps(self):
|
||||
return self._settings.get(IMPORT_MAPS)
|
||||
|
||||
@staticmethod
|
||||
def create_rd_rf_id(route_dist, route_family):
|
||||
return route_dist, route_family
|
||||
|
||||
@staticmethod
|
||||
def vrf_rf_2_rf(vrf_rf):
|
||||
if vrf_rf == VRF_RF_IPV4:
|
||||
return RF_IPv4_UC
|
||||
elif vrf_rf == VRF_RF_IPV6:
|
||||
return RF_IPv6_UC
|
||||
else:
|
||||
raise ValueError('Unsupported VRF route family given %s' % vrf_rf)
|
||||
|
||||
@staticmethod
|
||||
def rf_2_vrf_rf(route_family):
|
||||
if route_family == RF_IPv4_UC:
|
||||
return VRF_RF_IPV4
|
||||
elif route_family == RF_IPv6_UC:
|
||||
return VRF_RF_IPV6
|
||||
else:
|
||||
raise ValueError('No supported mapping for route family '
|
||||
'to vrf_route_family exists for %s' %
|
||||
route_family)
|
||||
|
||||
@property
|
||||
def settings(self):
|
||||
"""Returns a copy of current settings.
|
||||
|
||||
As some of the attributes are themselves containers, we clone the
|
||||
settings to provide clones for those containers as well.
|
||||
"""
|
||||
# Shallow copy first
|
||||
cloned_setting = self._settings.copy()
|
||||
# Don't want clone to link to same RT containers
|
||||
cloned_setting[IMPORT_RTS] = self.import_rts
|
||||
cloned_setting[EXPORT_RTS] = self.export_rts
|
||||
cloned_setting[SITE_OF_ORIGINS] = self.soo_list
|
||||
return cloned_setting
|
||||
|
||||
@classmethod
|
||||
def get_opt_settings(cls):
|
||||
self_confs = super(VrfConf, cls).get_opt_settings()
|
||||
self_confs.update(VrfConf.OPTIONAL_SETTINGS)
|
||||
return self_confs
|
||||
|
||||
@classmethod
|
||||
def get_req_settings(cls):
|
||||
self_confs = super(VrfConf, cls).get_req_settings()
|
||||
self_confs.update(VrfConf.REQUIRED_SETTINGS)
|
||||
return self_confs
|
||||
|
||||
@classmethod
|
||||
def get_valid_evts(cls):
|
||||
self_valid_evts = super(VrfConf, cls).get_valid_evts()
|
||||
self_valid_evts.update(VrfConf.VALID_EVT)
|
||||
return self_valid_evts
|
||||
|
||||
def update(self, **kwargs):
|
||||
"""Updates this `VrfConf` settings.
|
||||
|
||||
Notifies listeners if any settings changed. Returns `True` if update
|
||||
was successful. This vrfs' route family, id and route dist settings
|
||||
cannot be updated/changed.
|
||||
"""
|
||||
# Update inherited configurations
|
||||
super(VrfConf, self).update(**kwargs)
|
||||
vrf_id = kwargs.get(ConfWithId.ID)
|
||||
vrf_rd = kwargs.get(ROUTE_DISTINGUISHER)
|
||||
vrf_rf = kwargs.get(VRF_RF)
|
||||
if (vrf_id != self.id or
|
||||
vrf_rd != self.route_dist or
|
||||
vrf_rf != self.route_family):
|
||||
raise ConfigValueError(desc='id/route-distinguisher/route-family'
|
||||
' do not match configured value.')
|
||||
|
||||
# Validate and update individual settings
|
||||
new_imp_rts, old_imp_rts = \
|
||||
self._update_import_rts(**kwargs)
|
||||
export_rts_changed = self._update_export_rts(**kwargs)
|
||||
soos_list_changed = self._update_soo_list(**kwargs)
|
||||
med_changed = self._update_med(**kwargs)
|
||||
re_export_needed = (export_rts_changed or
|
||||
soos_list_changed or
|
||||
med_changed)
|
||||
import_maps = kwargs.get(IMPORT_MAPS, [])
|
||||
re_import_needed = self._update_importmaps(import_maps)
|
||||
|
||||
# If we did have any change in value of any settings, we notify
|
||||
# listeners
|
||||
if (new_imp_rts is not None or
|
||||
old_imp_rts is not None or
|
||||
re_export_needed or re_import_needed):
|
||||
evt_value = (
|
||||
new_imp_rts,
|
||||
old_imp_rts,
|
||||
import_maps,
|
||||
re_export_needed,
|
||||
re_import_needed
|
||||
)
|
||||
self._notify_listeners(VrfConf.VRF_CHG_EVT, evt_value)
|
||||
return True
|
||||
|
||||
def _update_import_rts(self, **kwargs):
|
||||
import_rts = kwargs.get(IMPORT_RTS)
|
||||
get_validator(IMPORT_RTS)(import_rts)
|
||||
curr_import_rts = set(self._settings[IMPORT_RTS])
|
||||
|
||||
import_rts = set(import_rts)
|
||||
if not import_rts.symmetric_difference(curr_import_rts):
|
||||
return (None, None)
|
||||
|
||||
# Get the difference between current and new RTs
|
||||
new_import_rts = import_rts - curr_import_rts
|
||||
old_import_rts = curr_import_rts - import_rts
|
||||
|
||||
# Update current RTs and notify listeners.
|
||||
self._settings[IMPORT_RTS] = import_rts
|
||||
return (new_import_rts, old_import_rts)
|
||||
|
||||
def _update_export_rts(self, **kwargs):
|
||||
export_rts = kwargs.get(EXPORT_RTS)
|
||||
get_validator(EXPORT_RTS)(export_rts)
|
||||
curr_export_rts = set(self._settings[EXPORT_RTS])
|
||||
|
||||
if curr_export_rts.symmetric_difference(export_rts):
|
||||
# Update current RTs and notify listeners.
|
||||
self._settings[EXPORT_RTS] = list(export_rts)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _update_soo_list(self, **kwargs):
|
||||
soo_list = kwargs.get(SITE_OF_ORIGINS, [])
|
||||
get_validator(SITE_OF_ORIGINS)(soo_list)
|
||||
curr_soos = set(self.soo_list)
|
||||
|
||||
# If given list is different from existing settings, we update it
|
||||
if curr_soos.symmetric_difference(soo_list):
|
||||
self._settings[SITE_OF_ORIGINS] = soo_list[:]
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _update_med(self, **kwargs):
|
||||
multi_exit_disc = kwargs.get(MULTI_EXIT_DISC, None)
|
||||
if multi_exit_disc:
|
||||
get_validator(MULTI_EXIT_DISC)(multi_exit_disc)
|
||||
|
||||
if multi_exit_disc != self.multi_exit_disc:
|
||||
self._settings[MULTI_EXIT_DISC] = multi_exit_disc
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _update_importmaps(self, import_maps):
|
||||
if set(self._settings[IMPORT_MAPS]).symmetric_difference(import_maps):
|
||||
self._settings[IMPORT_MAPS] = import_maps
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def __repr__(self):
|
||||
return ('<%s(route_dist: %r, import_rts: %r, export_rts: %r, '
|
||||
'soo_list: %r)>' % (self.__class__.__name__,
|
||||
self.route_dist, self.import_rts,
|
||||
self.export_rts, self.soo_list))
|
||||
|
||||
def __str__(self):
|
||||
return ('VrfConf-%s' % (self.route_dist))
|
||||
|
||||
|
||||
class VrfsConf(BaseConf):
|
||||
"""Container for all VRF configurations."""
|
||||
|
||||
ADD_VRF_CONF_EVT, REMOVE_VRF_CONF_EVT = xrange(2)
|
||||
|
||||
VALID_EVT = frozenset([ADD_VRF_CONF_EVT, REMOVE_VRF_CONF_EVT])
|
||||
|
||||
def __init__(self):
|
||||
super(VrfsConf, self).__init__()
|
||||
self._vrfs_by_rd_rf = {}
|
||||
self._vrfs_by_id = {}
|
||||
|
||||
def _init_opt_settings(self, **kwargs):
|
||||
pass
|
||||
|
||||
@property
|
||||
def vrf_confs(self):
|
||||
"""Returns a list of configured `VrfConf`s
|
||||
"""
|
||||
return self._vrfs_by_rd_rf.values()
|
||||
|
||||
@property
|
||||
def vrf_interested_rts(self):
|
||||
interested_rts = set()
|
||||
for vrf_conf in self._vrfs_by_id.values():
|
||||
interested_rts.update(vrf_conf.import_rts)
|
||||
return interested_rts
|
||||
|
||||
def update(self, **kwargs):
|
||||
raise NotImplementedError('Use either add/remove_vrf_conf'
|
||||
' methods instead.')
|
||||
|
||||
def add_vrf_conf(self, vrf_conf):
|
||||
if vrf_conf.rd_rf_id in self._vrfs_by_rd_rf.keys():
|
||||
raise RuntimeConfigError(
|
||||
desc='VrfConf with rd_rf %s already exists'
|
||||
% str(vrf_conf.rd_rf_id)
|
||||
)
|
||||
if vrf_conf.id in self._vrfs_by_id:
|
||||
raise RuntimeConfigError(
|
||||
desc='VrfConf with id %s already exists' % str(vrf_conf.id)
|
||||
)
|
||||
|
||||
self._vrfs_by_rd_rf[vrf_conf.rd_rf_id] = vrf_conf
|
||||
self._vrfs_by_id[vrf_conf.id] = vrf_conf
|
||||
self._notify_listeners(VrfsConf.ADD_VRF_CONF_EVT, vrf_conf)
|
||||
|
||||
def remove_vrf_conf(self, route_dist=None, vrf_id=None,
|
||||
vrf_rf=None):
|
||||
"""Removes any matching `VrfConf` for given `route_dist` or `vrf_id`
|
||||
|
||||
Paramters:
|
||||
- `route_dist`: (str) route distinguisher of a configured VRF
|
||||
- `vrf_id`: (str) vrf ID
|
||||
- `vrf_rf`: (str) route family of the VRF configuration
|
||||
If only `route_dist` is given, removes `VrfConf`s for all supported
|
||||
address families for this `route_dist`. If `vrf_rf` is given, than only
|
||||
removes `VrfConf` for that specific route family. If only `vrf_id` is
|
||||
given, matching `VrfConf` will be removed.
|
||||
"""
|
||||
if route_dist is None and vrf_id is None:
|
||||
raise RuntimeConfigError(desc='To delete supply route_dist or id.')
|
||||
|
||||
# By default we remove all VRFs for given Id or RD
|
||||
vrf_rfs = SUPPORTED_VRF_RF
|
||||
# If asked to delete specific route family vrf conf.
|
||||
if vrf_rf:
|
||||
vrf_rfs = (vrf_rf)
|
||||
|
||||
# For all vrf route family asked to be deleted, we collect all deleted
|
||||
# VrfConfs
|
||||
removed_vrf_confs = []
|
||||
for route_family in vrf_rfs:
|
||||
if route_dist is not None:
|
||||
rd_rf_id = VrfConf.create_rd_rf_id(route_dist, route_family)
|
||||
vrf_conf = self._vrfs_by_rd_rf.pop(rd_rf_id, None)
|
||||
if vrf_conf:
|
||||
self._vrfs_by_id.pop(vrf_conf.id, None)
|
||||
removed_vrf_confs.append(vrf_conf)
|
||||
else:
|
||||
vrf_conf = self._vrfs_by_id.pop(vrf_id, None)
|
||||
if vrf_conf:
|
||||
self._vrfs_by_rd_rf.pop(vrf_conf.rd_rd_id, None)
|
||||
removed_vrf_confs.append(vrf_conf)
|
||||
|
||||
# We do not raise any exception if we cannot find asked VRF.
|
||||
for vrf_conf in removed_vrf_confs:
|
||||
self._notify_listeners(VrfsConf.REMOVE_VRF_CONF_EVT, vrf_conf)
|
||||
return removed_vrf_confs
|
||||
|
||||
def get_vrf_conf(self, route_dist, vrf_rf, vrf_id=None):
|
||||
if route_dist is None and vrf_id is None:
|
||||
raise RuntimeConfigError(desc='To get VRF supply route_dist '
|
||||
'or vrf_id.')
|
||||
vrf = None
|
||||
if route_dist is not None and vrf_id is not None:
|
||||
vrf1 = self._vrfs_by_id.get(vrf_id)
|
||||
rd_rf_id = VrfConf.create_rd_rf_id(route_dist, vrf_rf)
|
||||
vrf2 = self._vrfs_by_rd_rf.get(rd_rf_id)
|
||||
if vrf1 is not vrf2:
|
||||
raise RuntimeConfigError(desc='Given VRF ID (%s) and RD (%s)'
|
||||
' are not of same VRF.' %
|
||||
(vrf_id, route_dist))
|
||||
vrf = vrf1
|
||||
elif route_dist is not None:
|
||||
rd_rf_id = VrfConf.create_rd_rf_id(route_dist, vrf_rf)
|
||||
vrf = self._vrfs_by_rd_rf.get(rd_rf_id)
|
||||
else:
|
||||
vrf = self._vrfs_by_id.get(vrf_id)
|
||||
return vrf
|
||||
|
||||
@property
|
||||
def vrfs_by_rd_rf_id(self):
|
||||
return dict(self._vrfs_by_rd_rf)
|
||||
|
||||
@classmethod
|
||||
def get_valid_evts(self):
|
||||
self_valid_evts = super(VrfsConf, self).get_valid_evts()
|
||||
self_valid_evts.update(VrfsConf.VALID_EVT)
|
||||
return self_valid_evts
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s(%r)>' % (self.__class__.__name__, self._vrfs_by_id)
|
||||
|
||||
@property
|
||||
def settings(self):
|
||||
return [vrf.settings for vrf in self._vrfs_by_id.values()]
|
||||
|
||||
|
||||
class VrfConfListener(ConfWithIdListener, ConfWithStatsListener):
|
||||
"""Base listener for various VRF configuration change event."""
|
||||
|
||||
def __init__(self, vrf_conf):
|
||||
super(VrfConfListener, self).__init__(vrf_conf)
|
||||
vrf_conf.add_listener(VrfConf.VRF_CHG_EVT, self.on_chg_vrf_conf)
|
||||
|
||||
def on_chg_vrf_conf(self, evt):
|
||||
raise NotImplementedError('This method should be overridden')
|
||||
|
||||
|
||||
class VrfsConfListener(BaseConfListener):
|
||||
"""Base listener for VRF container change events."""
|
||||
|
||||
def __init__(self, vrfs_conf):
|
||||
super(VrfsConfListener, self).__init__(vrfs_conf)
|
||||
vrfs_conf.add_listener(VrfsConf.ADD_VRF_CONF_EVT, self.on_add_vrf_conf)
|
||||
vrfs_conf.add_listener(VrfsConf.REMOVE_VRF_CONF_EVT,
|
||||
self.on_remove_vrf_conf)
|
||||
|
||||
@abc.abstractmethod
|
||||
def on_add_vrf_conf(self, evt):
|
||||
raise NotImplementedError('This method should be overridden')
|
||||
|
||||
@abc.abstractmethod
|
||||
def on_remove_vrf_conf(self, evt):
|
||||
raise NotImplementedError('This method should be overridden')
|
||||
5
ryu/services/protocols/bgp/signals/__init__.py
Normal file
5
ryu/services/protocols/bgp/signals/__init__.py
Normal file
@ -0,0 +1,5 @@
|
||||
__author__ = 'yak'
|
||||
|
||||
from ryu.services.protocols.bgp.signals.base import SignalBus
|
||||
|
||||
__all__ = [SignalBus]
|
||||
33
ryu/services/protocols/bgp/signals/base.py
Normal file
33
ryu/services/protocols/bgp/signals/base.py
Normal file
@ -0,0 +1,33 @@
|
||||
import logging
|
||||
LOG = logging.getLogger('bgpspeaker.signals.base')
|
||||
|
||||
|
||||
class SignalBus(object):
|
||||
def __init__(self):
|
||||
self._listeners = {}
|
||||
|
||||
def emit_signal(self, identifier, data):
|
||||
identifier = _to_tuple(identifier)
|
||||
LOG.debug('SIGNAL: %s emited with data: %s ' % (identifier, data))
|
||||
for func, filter_func in self._listeners.get(identifier, []):
|
||||
if not filter_func or filter_func(data):
|
||||
func(identifier, data)
|
||||
|
||||
def register_listener(self, identifier, func, filter_func=None):
|
||||
identifier = _to_tuple(identifier)
|
||||
substrings = (identifier[:i] for i in xrange(1, len(identifier) + 1))
|
||||
for partial_id in substrings:
|
||||
self._listeners.setdefault(
|
||||
partial_id,
|
||||
[]
|
||||
).append((func, filter_func))
|
||||
|
||||
def unregister_all(self):
|
||||
self._listeners = {}
|
||||
|
||||
|
||||
def _to_tuple(tuple_or_not):
|
||||
if not isinstance(tuple_or_not, tuple):
|
||||
return (tuple_or_not, )
|
||||
else:
|
||||
return tuple_or_not
|
||||
55
ryu/services/protocols/bgp/signals/emit.py
Normal file
55
ryu/services/protocols/bgp/signals/emit.py
Normal file
@ -0,0 +1,55 @@
|
||||
from ryu.services.protocols.bgp.signals import SignalBus
|
||||
|
||||
|
||||
class BgpSignalBus(SignalBus):
|
||||
BGP_ERROR = ('error', 'bgp')
|
||||
BGP_DEST_CHANGED = ('core', 'dest', 'changed')
|
||||
BGP_VRF_REMOVED = ('core', 'vrf', 'removed')
|
||||
BGP_VRF_ADDED = ('core', 'vrf', 'added')
|
||||
BGP_NOTIFICATION_RECEIVED = ('bgp', 'notification_received')
|
||||
BGP_NOTIFICATION_SENT = ('bgp', 'notification_sent')
|
||||
BGP_VRF_STATS_CONFIG_CHANGED = (
|
||||
'core', 'vrf', 'config', 'stats', 'changed'
|
||||
)
|
||||
|
||||
def bgp_error(self, peer, code, subcode, reason):
|
||||
return self.emit_signal(
|
||||
self.BGP_ERROR + (peer, ),
|
||||
{'code': code, 'subcode': subcode, 'reason': reason, 'peer': peer}
|
||||
)
|
||||
|
||||
def bgp_notification_received(self, peer, notification):
|
||||
return self.emit_signal(
|
||||
self.BGP_NOTIFICATION_RECEIVED + (peer,),
|
||||
notification
|
||||
)
|
||||
|
||||
def bgp_notification_sent(self, peer, notification):
|
||||
return self.emit_signal(
|
||||
self.BGP_NOTIFICATION_SENT + (peer,),
|
||||
notification
|
||||
)
|
||||
|
||||
def dest_changed(self, dest):
|
||||
return self.emit_signal(
|
||||
self.BGP_DEST_CHANGED,
|
||||
dest
|
||||
)
|
||||
|
||||
def vrf_removed(self, route_dist):
|
||||
return self.emit_signal(
|
||||
self.BGP_VRF_REMOVED,
|
||||
route_dist
|
||||
)
|
||||
|
||||
def vrf_added(self, vrf_conf):
|
||||
return self.emit_signal(
|
||||
self.BGP_VRF_ADDED,
|
||||
vrf_conf
|
||||
)
|
||||
|
||||
def stats_config_changed(self, vrf_conf):
|
||||
return self.emit_signal(
|
||||
self.BGP_VRF_STATS_CONFIG_CHANGED,
|
||||
vrf_conf
|
||||
)
|
||||
596
ryu/services/protocols/bgp/speaker.py
Normal file
596
ryu/services/protocols/bgp/speaker.py
Normal file
@ -0,0 +1,596 @@
|
||||
# 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.
|
||||
|
||||
"""
|
||||
BGP protocol implementation.
|
||||
"""
|
||||
import logging
|
||||
import socket
|
||||
import struct
|
||||
import traceback
|
||||
|
||||
from ryu.services.protocols.bgp.base import Activity
|
||||
from ryu.services.protocols.bgp.base import add_bgp_error_metadata
|
||||
from ryu.services.protocols.bgp.base import BGPSException
|
||||
from ryu.services.protocols.bgp.base import CORE_ERROR_CODE
|
||||
from ryu.services.protocols.bgp.constants import BGP_FSM_CONNECT
|
||||
from ryu.services.protocols.bgp.constants import BGP_FSM_OPEN_CONFIRM
|
||||
from ryu.services.protocols.bgp.constants import BGP_FSM_OPEN_SENT
|
||||
from ryu.services.protocols.bgp.constants import BGP_VERSION_NUM
|
||||
from ryu.services.protocols.bgp.protocol import Protocol
|
||||
from ryu.services.protocols.bgp.protocols.bgp.capabilities import \
|
||||
EnhancedRouteRefreshCap
|
||||
from ryu.services.protocols.bgp.protocols.bgp.capabilities import \
|
||||
MultiprotocolExtentionCap
|
||||
from ryu.services.protocols.bgp.protocols.bgp.capabilities import \
|
||||
RouteRefreshCap
|
||||
import ryu.services.protocols.bgp.protocols.bgp.exceptions as exceptions
|
||||
from ryu.services.protocols.bgp.protocols.bgp.exceptions import BgpExc
|
||||
from ryu.services.protocols.bgp.protocols.bgp import messages
|
||||
from ryu.services.protocols.bgp.protocols.bgp.messages import Keepalive
|
||||
from ryu.services.protocols.bgp.protocols.bgp.messages import Notification
|
||||
from ryu.services.protocols.bgp.protocols.bgp.messages import Open
|
||||
from ryu.services.protocols.bgp.protocols.bgp.messages import RouteRefresh
|
||||
from ryu.services.protocols.bgp.protocols.bgp.messages import Update
|
||||
from ryu.services.protocols.bgp.protocols.bgp import nlri
|
||||
from ryu.services.protocols.bgp.protocols.bgp.nlri import RF_RTC_UC
|
||||
from ryu.services.protocols.bgp.utils.validation import is_valid_old_asn
|
||||
|
||||
|
||||
LOG = logging.getLogger('bgpspeaker.speaker')
|
||||
|
||||
# BGP min. and max. message lengths as per RFC.
|
||||
BGP_MIN_MSG_LEN = 19
|
||||
BGP_MAX_MSG_LEN = 4096
|
||||
|
||||
# Keep-alive singleton.
|
||||
_KEEP_ALIVE = Keepalive()
|
||||
|
||||
|
||||
@add_bgp_error_metadata(code=CORE_ERROR_CODE, sub_code=2,
|
||||
def_desc='Unknown error occurred related to Speaker.')
|
||||
class BgpProtocolException(BGPSException):
|
||||
"""Base exception related to peer connection management.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def nofitication_factory(code, subcode):
|
||||
"""Returns a `Notification` message corresponding to given codes.
|
||||
|
||||
Parameters:
|
||||
- `code`: (int) BGP error code
|
||||
- `subcode`: (int) BGP error sub-code
|
||||
"""
|
||||
reason = Notification.REASONS.get((code, subcode))
|
||||
if not reason:
|
||||
raise ValueError('Invalid code/sub-code.')
|
||||
|
||||
return Notification(code, subcode)
|
||||
|
||||
|
||||
class BgpProtocol(Protocol, Activity):
|
||||
"""Protocol that handles BGP messages.
|
||||
"""
|
||||
MESSAGE_MARKER = ('\xff\xff\xff\xff\xff\xff\xff\xff'
|
||||
'\xff\xff\xff\xff\xff\xff\xff\xff')
|
||||
|
||||
def __init__(self, socket, signal_bus, is_reactive_conn=False):
|
||||
# Validate input.
|
||||
if socket is None:
|
||||
raise ValueError('Invalid arguments passed.')
|
||||
activity_name = ('BgpProtocol %s, %s, %s' % (
|
||||
is_reactive_conn, socket.getpeername(), socket.getsockname())
|
||||
)
|
||||
Activity.__init__(self, name=activity_name)
|
||||
# Intialize instance variables.
|
||||
self._peer = None
|
||||
self._recv_buff = ''
|
||||
self._socket = socket
|
||||
self._signal_bus = signal_bus
|
||||
self._holdtime = None
|
||||
self._keepalive = None
|
||||
self._expiry = None
|
||||
# Add socket to Activity's socket container for managing it.
|
||||
if is_reactive_conn:
|
||||
self._asso_socket_map['passive_conn'] = self._socket
|
||||
else:
|
||||
self._asso_socket_map['active_conn'] = self._socket
|
||||
self._open_msg = None
|
||||
self.state = BGP_FSM_CONNECT
|
||||
self._is_reactive = is_reactive_conn
|
||||
self.sent_open_msg = None
|
||||
self.recv_open_msg = None
|
||||
self._is_bound = False
|
||||
|
||||
def get_peername(self):
|
||||
return self._socket.getpeername()
|
||||
|
||||
def get_sockname(self):
|
||||
return self._socket.getsockname()
|
||||
|
||||
@property
|
||||
def is_reactive(self):
|
||||
return self._is_reactive
|
||||
|
||||
@property
|
||||
def holdtime(self):
|
||||
return self._holdtime
|
||||
|
||||
@property
|
||||
def keepalive(self):
|
||||
return self._keepalive
|
||||
|
||||
def is_colliding(self, other_protocol):
|
||||
if not isinstance(other_protocol, BgpProtocol):
|
||||
raise ValueError('Currently only support comparing with '
|
||||
'`BgpProtocol`')
|
||||
|
||||
# Compare protocol connection end point's addresses
|
||||
if (self.get_peername()[0] == other_protocol.get_peername()[0] and
|
||||
self.get_sockname()[0] == other_protocol.get_sockname()[0]):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def is_local_router_id_greater(self):
|
||||
"""Compares *True* if local router id is greater when compared to peer
|
||||
bgp id.
|
||||
|
||||
Should only be called after protocol has reached OpenConfirm state.
|
||||
"""
|
||||
from ryu.services.protocols.bgp.speaker.utils.bgp import from_inet_ptoi
|
||||
|
||||
if not self.state == BGP_FSM_OPEN_CONFIRM:
|
||||
raise BgpProtocolException(desc='Can access remote router id only'
|
||||
' after open message is received')
|
||||
remote_id = self.recv_open_msg.bgpid
|
||||
local_id = self.sent_open_msg.bgpid
|
||||
return from_inet_ptoi(local_id) > from_inet_ptoi(remote_id)
|
||||
|
||||
def is_enhanced_rr_cap_valid(self):
|
||||
"""Checks is enhanced route refresh capability is enabled/valid.
|
||||
|
||||
Checks sent and received `Open` messages to see if this session with
|
||||
peer is capable of enhanced route refresh capability.
|
||||
"""
|
||||
if not self.recv_open_msg:
|
||||
raise ValueError('Did not yet receive peers open message.')
|
||||
|
||||
err_cap_enabled = False
|
||||
local_cap = self.sent_open_msg.caps
|
||||
peer_cap = self.recv_open_msg.caps
|
||||
# Both local and peer should advertise ERR capability for it to be
|
||||
# enabled.
|
||||
if (local_cap.get(EnhancedRouteRefreshCap.CODE) and
|
||||
peer_cap.get(EnhancedRouteRefreshCap.CODE)):
|
||||
err_cap_enabled = True
|
||||
|
||||
return err_cap_enabled
|
||||
|
||||
def _check_route_fmly_adv(self, open_msg, route_family):
|
||||
match_found = False
|
||||
|
||||
local_caps = open_msg.caps
|
||||
mbgp_cap = local_caps.get(MultiprotocolExtentionCap.CODE)
|
||||
# Check MP_BGP capability was advertised.
|
||||
if mbgp_cap:
|
||||
# Iterate over all advertised mp_bgp caps to find a match.
|
||||
for peer_cap in mbgp_cap:
|
||||
if (route_family.afi == peer_cap.route_family.afi and
|
||||
route_family.safi == peer_cap.route_family.safi):
|
||||
match_found = True
|
||||
|
||||
return match_found
|
||||
|
||||
def is_route_family_adv(self, route_family):
|
||||
"""Checks if `route_family` was advertised to peer as per MP_BGP cap.
|
||||
|
||||
Returns:
|
||||
- True: if given address family was advertised.
|
||||
- False: if given address family was not advertised.
|
||||
"""
|
||||
return self._check_route_fmly_adv(self.sent_open_msg, route_family)
|
||||
|
||||
def is_route_family_adv_recv(self, route_family):
|
||||
"""Checks if `route_family` was advertised by peer as per MP_BGP cap.
|
||||
|
||||
Returns:
|
||||
- True: if given address family was advertised.
|
||||
- False: if given address family was not advertised.
|
||||
"""
|
||||
return self._check_route_fmly_adv(self.recv_open_msg, route_family)
|
||||
|
||||
@property
|
||||
def negotiated_afs(self):
|
||||
local_caps = self.sent_open_msg.caps
|
||||
remote_caps = self.recv_open_msg.caps
|
||||
|
||||
local_mbgp_cap = local_caps.get(MultiprotocolExtentionCap.CODE)
|
||||
remote_mbgp_cap = remote_caps.get(MultiprotocolExtentionCap.CODE)
|
||||
# Check MP_BGP capabilities were advertised.
|
||||
if local_mbgp_cap and remote_mbgp_cap:
|
||||
local_families = {
|
||||
(peer_cap.route_family.afi, peer_cap.route_family.safi)
|
||||
for peer_cap in local_mbgp_cap
|
||||
}
|
||||
remote_families = {
|
||||
(peer_cap.route_family.afi, peer_cap.route_family.safi)
|
||||
for peer_cap in remote_mbgp_cap
|
||||
}
|
||||
afi_safi = local_families.intersection(remote_families)
|
||||
else:
|
||||
afi_safi = set()
|
||||
|
||||
afs = []
|
||||
for afi, safi in afi_safi:
|
||||
afs.append(nlri.get_rf(afi, safi))
|
||||
return afs
|
||||
|
||||
def is_mbgp_cap_valid(self, route_family):
|
||||
"""Returns true if both sides of this protocol have advertise
|
||||
capability for this address family.
|
||||
"""
|
||||
return (self.is_route_family_adv(route_family) and
|
||||
self.is_route_family_adv_recv(route_family))
|
||||
|
||||
def _run(self, peer):
|
||||
"""Sends open message to peer and handles received messages.
|
||||
|
||||
Parameters:
|
||||
- `peer`: the peer to which this protocol instance is connected to.
|
||||
"""
|
||||
# We know the peer we are connected to, we send open message.
|
||||
self._peer = peer
|
||||
self.connection_made()
|
||||
|
||||
# We wait for peer to send messages.
|
||||
self._recv_loop()
|
||||
|
||||
def data_received(self, next_bytes):
|
||||
try:
|
||||
self._data_received(next_bytes)
|
||||
except BgpExc as exc:
|
||||
LOG.error(
|
||||
"BGPExc Exception while receiving data: "
|
||||
"%s \n Traceback %s \n"
|
||||
% (str(exc), traceback.format_exc())
|
||||
)
|
||||
if exc.SEND_ERROR:
|
||||
self.send_notification(exc.CODE, exc.SUB_CODE)
|
||||
else:
|
||||
self._socket.close()
|
||||
raise exc
|
||||
|
||||
@staticmethod
|
||||
def parse_msg_header(buff):
|
||||
"""Parses given `buff` into bgp message header format.
|
||||
|
||||
Returns a tuple of marker, length, type of bgp message.
|
||||
"""
|
||||
return struct.unpack('!16sHB', buff)
|
||||
|
||||
def _data_received(self, next_bytes):
|
||||
"""Maintains buffer of bytes received from peer and extracts bgp
|
||||
message from this buffer if enough data is received.
|
||||
|
||||
Validates bgp message marker, length, type and data and constructs
|
||||
appropriate bgp message instance and calls handler.
|
||||
|
||||
:Parameters:
|
||||
- `next_bytes`: next set of bytes received from peer.
|
||||
"""
|
||||
# Append buffer with received bytes.
|
||||
self._recv_buff += next_bytes
|
||||
|
||||
while True:
|
||||
# If current buffer size is less then minimum bgp message size, we
|
||||
# return as we do not have a complete bgp message to work with.
|
||||
if len(self._recv_buff) < BGP_MIN_MSG_LEN:
|
||||
return
|
||||
|
||||
# Parse message header into elements.
|
||||
auth, length, ptype = BgpProtocol.parse_msg_header(
|
||||
self._recv_buff[:BGP_MIN_MSG_LEN])
|
||||
|
||||
# Check if we have valid bgp message marker.
|
||||
# We should get default marker since we are not supporting any
|
||||
# authentication.
|
||||
if (auth != BgpProtocol.MESSAGE_MARKER):
|
||||
LOG.error('Invalid message marker received: %s' % auth)
|
||||
raise exceptions.NotSync()
|
||||
|
||||
# Check if we have valid bgp message length.
|
||||
check = lambda: length < BGP_MIN_MSG_LEN\
|
||||
or length > BGP_MAX_MSG_LEN
|
||||
|
||||
# RFC says: The minimum length of the OPEN message is 29
|
||||
# octets (including the message header).
|
||||
check2 = lambda: ptype == Open.TYPE_CODE\
|
||||
and length < Open.MIN_LENGTH
|
||||
|
||||
# RFC says: A KEEPALIVE message consists of only the
|
||||
# message header and has a length of 19 octets.
|
||||
check3 = lambda: ptype == Keepalive.TYPE_CODE\
|
||||
and length != BGP_MIN_MSG_LEN
|
||||
|
||||
# RFC says: The minimum length of the UPDATE message is 23
|
||||
# octets.
|
||||
check4 = lambda: ptype == Update.TYPE_CODE\
|
||||
and length < Update.MIN_LENGTH
|
||||
|
||||
if check() or check2() or check3() or check4():
|
||||
raise exceptions.BadLen(ptype, length)
|
||||
|
||||
# If we have partial message we wait for rest of the message.
|
||||
if len(self._recv_buff) < length:
|
||||
return
|
||||
|
||||
# If we have full message, we get its payload/data.
|
||||
payload = self._recv_buff[BGP_MIN_MSG_LEN:length]
|
||||
|
||||
# Update buffer to not contain any part of the current message.
|
||||
self._recv_buff = self._recv_buff[length:]
|
||||
|
||||
# Try to decode payload into specified message type.
|
||||
# If we have any error parsing the message, we send appropriate
|
||||
# bgp notification message.
|
||||
msg = messages.decode(ptype, payload, length)
|
||||
|
||||
# If we have a valid bgp message we call message handler.
|
||||
self._handle_msg(msg)
|
||||
|
||||
def send_notification(self, code, subcode):
|
||||
"""Utility to send notification message.
|
||||
|
||||
Closes the socket after sending the message.
|
||||
:Parameters:
|
||||
- `socket`: (socket) - socket over which to send notification
|
||||
message.
|
||||
- `code`: (int) - BGP Notification code
|
||||
- `subcode`: (int) - BGP Notification sub-code
|
||||
|
||||
RFC ref: http://tools.ietf.org/html/rfc4486
|
||||
http://www.iana.org/assignments/bgp-parameters/bgp-parameters.xhtml
|
||||
"""
|
||||
reason = Notification.REASONS.get((code, subcode))
|
||||
if not reason:
|
||||
# Not checking for type of parameters to allow some flexibility
|
||||
# via. duck-typing.
|
||||
raise ValueError('Unsupported code/sub-code given.')
|
||||
|
||||
notification = Notification(code, subcode, reason)
|
||||
self._socket.sendall(notification.encode())
|
||||
self._signal_bus.bgp_error(self._peer, code, subcode, reason)
|
||||
LOG.error(
|
||||
'Sent notification to %r>> %s' %
|
||||
(self._socket.getpeername(), notification)
|
||||
)
|
||||
self._socket.close()
|
||||
|
||||
def send(self, msg):
|
||||
if not self.started:
|
||||
raise BgpProtocolException('Tried to send message to peer when '
|
||||
'this protocol instance is not started'
|
||||
' or is no longer is started state.')
|
||||
self._socket.sendall(msg.encode())
|
||||
if msg.MSG_NAME == Notification.MSG_NAME:
|
||||
LOG.error('Sent notification to %s>> %s' %
|
||||
(self.get_peername(), msg))
|
||||
|
||||
self._signal_bus.bgp_notification_sent(self._peer, msg)
|
||||
|
||||
else:
|
||||
LOG.debug('Sent msg. %s to %s>> %s' %
|
||||
(msg.MSG_NAME, self.get_peername(), msg))
|
||||
|
||||
def stop(self):
|
||||
Activity.stop(self)
|
||||
|
||||
def _validate_open_msg(self, open_msg):
|
||||
"""Validates BGP OPEN message according from application context.
|
||||
|
||||
Parsing modules takes care of validating OPEN message that need no
|
||||
context. But here we validate it according to current application
|
||||
settings. RTC or RR/ERR are MUST capability if peer does not support
|
||||
either one of them we have to end session.
|
||||
"""
|
||||
assert open_msg.TYPE_CODE == Open.TYPE_CODE
|
||||
# Validate remote ASN.
|
||||
remote_asnum = open_msg.asnum
|
||||
# Since 4byte AS is not yet supported, we validate AS as old style AS.
|
||||
if (not is_valid_old_asn(remote_asnum) or
|
||||
remote_asnum != self._peer.remote_as):
|
||||
raise exceptions.BadPeerAs()
|
||||
|
||||
# Validate bgp version number.
|
||||
if open_msg.version != BGP_VERSION_NUM:
|
||||
raise exceptions.UnsupportedVersion(BGP_VERSION_NUM)
|
||||
|
||||
adv_caps = open_msg.caps
|
||||
rr_cap_adv = adv_caps.get(RouteRefreshCap.CODE)
|
||||
err_cap_adv = adv_caps.get(EnhancedRouteRefreshCap.CODE)
|
||||
# If either RTC or RR/ERR are MUST capability if peer does not support
|
||||
# either one of them we have to end session as we have to request peer
|
||||
# to send prefixes for new VPNs that may be created automatically.
|
||||
# TODO(PH): Check with experts if error is suitable in this case
|
||||
if not (rr_cap_adv or err_cap_adv or
|
||||
self._check_route_fmly_adv(open_msg, RF_RTC_UC)):
|
||||
raise exceptions.UnsupportedOptParam()
|
||||
|
||||
def _handle_msg(self, msg):
|
||||
"""When a BGP message is received, send it to peer.
|
||||
|
||||
Open messages are validated here. Peer handler is called to handle each
|
||||
message except for *Open* and *Notification* message. On receiving
|
||||
*Notification* message we close connection with peer.
|
||||
"""
|
||||
LOG.debug('Received %s msg. from %s<< \n%s' %
|
||||
(msg.MSG_NAME, str(self.get_peername()), msg))
|
||||
|
||||
# If we receive open message we try to bind to protocol
|
||||
if (msg.MSG_NAME == Open.MSG_NAME):
|
||||
if self.state == BGP_FSM_OPEN_SENT:
|
||||
# Validate open message.
|
||||
self._validate_open_msg(msg)
|
||||
self.recv_open_msg = msg
|
||||
self.state = BGP_FSM_OPEN_CONFIRM
|
||||
self._peer.state.bgp_state = self.state
|
||||
|
||||
# Try to bind this protocol to peer.
|
||||
self._is_bound = self._peer.bind_protocol(self)
|
||||
|
||||
# If this protocol failed to bind to peer.
|
||||
if not self._is_bound:
|
||||
# Failure to bind to peer indicates connection collision
|
||||
# resolution choose different instance of protocol and this
|
||||
# instance has to close. Before closing it sends
|
||||
# appropriate notification msg. to peer.
|
||||
raise exceptions.CollisionResolution()
|
||||
|
||||
# If peer sends Hold Time as zero, then according to RFC we do
|
||||
# not set Hold Time and Keep Alive timer.
|
||||
if msg.holdtime == 0:
|
||||
LOG.info('The Hold Time sent by the peer is zero, hence '
|
||||
'not setting any Hold Time and Keep Alive'
|
||||
' timers.')
|
||||
else:
|
||||
# Start Keep Alive timer considering Hold Time preference
|
||||
# of the peer.
|
||||
self._start_timers(msg.holdtime)
|
||||
self._send_keepalive()
|
||||
|
||||
# Peer does not see open message.
|
||||
return
|
||||
else:
|
||||
# If we receive a Open message out of order
|
||||
LOG.error('Open message received when current state is not '
|
||||
'OpenSent')
|
||||
# Received out-of-order open message
|
||||
# We raise Finite state machine error
|
||||
raise exceptions.FiniteStateMachineError()
|
||||
elif msg.MSG_NAME == Notification.MSG_NAME:
|
||||
if self._peer:
|
||||
self._signal_bus.bgp_notification_received(self._peer, msg)
|
||||
# If we receive notification message
|
||||
LOG.error('Received notification message, hence closing '
|
||||
'connection %s' % msg)
|
||||
self._socket.close()
|
||||
return
|
||||
|
||||
# If we receive keepalive or update message, we reset expire timer.
|
||||
if (msg.MSG_NAME == Keepalive.MSG_NAME or
|
||||
msg.MSG_NAME == Update.MSG_NAME):
|
||||
if self._expiry:
|
||||
self._expiry.reset()
|
||||
|
||||
# Call peer message handler for appropriate messages.
|
||||
if (msg.MSG_NAME in
|
||||
(Keepalive.MSG_NAME, Update.MSG_NAME, RouteRefresh.MSG_NAME)):
|
||||
self._peer.handle_msg(msg)
|
||||
# We give chance to other threads to run.
|
||||
self.pause(0)
|
||||
|
||||
def _start_timers(self, peer_holdtime):
|
||||
"""Starts keepalive and expire timers.
|
||||
|
||||
Hold time is set to min. of peer and configured/default hold time.
|
||||
Starts keep alive timer and expire timer based on this value.
|
||||
"""
|
||||
neg_timer = min(self._holdtime, peer_holdtime)
|
||||
if neg_timer < self._holdtime:
|
||||
LOG.info('Negotiated hold time (%s) is lower then '
|
||||
'configured/default (%s).' % (neg_timer, self._holdtime))
|
||||
# We use negotiated timer value.
|
||||
self._holdtime = neg_timer
|
||||
self._keepalive = self._create_timer('Keepalive Timer',
|
||||
self._send_keepalive)
|
||||
interval = self._holdtime / 3
|
||||
self._keepalive.start(interval, now=False)
|
||||
# Setup the expire timer.
|
||||
self._expiry = self._create_timer('Holdtime Timer', self._expired)
|
||||
self._expiry.start(self._holdtime, now=False)
|
||||
LOG.debug('Started keep-alive and expire timer for negotiated hold'
|
||||
'time %s' % self._holdtime)
|
||||
|
||||
def _expired(self):
|
||||
"""Hold timer expired event handler.
|
||||
"""
|
||||
LOG.info('Negotiated hold time %s expired.' % self._holdtime)
|
||||
code = exceptions.HoldTimerExpired.CODE
|
||||
subcode = exceptions.HoldTimerExpired.SUB_CODE
|
||||
self.send_notification(code, subcode)
|
||||
self.connection_lost('Negotiated hold time %s expired.' %
|
||||
self._holdtime)
|
||||
self.stop()
|
||||
|
||||
def _send_keepalive(self):
|
||||
self.send(_KEEP_ALIVE)
|
||||
|
||||
def _recv_loop(self):
|
||||
"""Sits in tight loop collecting data received from peer and
|
||||
processing it.
|
||||
"""
|
||||
required_len = BGP_MIN_MSG_LEN
|
||||
conn_lost_reason = "Connection lost as protocol is no longer active"
|
||||
try:
|
||||
while True:
|
||||
next_bytes = self._socket.recv(required_len)
|
||||
if len(next_bytes) == 0:
|
||||
conn_lost_reason = 'Peer closed connection'
|
||||
break
|
||||
self.data_received(next_bytes)
|
||||
except socket.error as err:
|
||||
conn_lost_reason = 'Connection to peer lost: %s.' % err
|
||||
except BgpExc as ex:
|
||||
conn_lost_reason = 'Connection to peer lost, reason: %s.' % ex
|
||||
except Exception as e:
|
||||
LOG.debug(traceback.format_exc())
|
||||
conn_lost_reason = str(e)
|
||||
finally:
|
||||
self.connection_lost(conn_lost_reason)
|
||||
|
||||
def connection_made(self):
|
||||
"""Connection to peer handler.
|
||||
|
||||
We send bgp open message to peer and intialize related attributes.
|
||||
"""
|
||||
assert self.state == BGP_FSM_CONNECT
|
||||
# We have a connection with peer we send open message.
|
||||
open_msg = self._peer.create_open_msg()
|
||||
self._holdtime = open_msg.holdtime
|
||||
self.state = BGP_FSM_OPEN_SENT
|
||||
if not self.is_reactive:
|
||||
self._peer.state.bgp_state = self.state
|
||||
self.sent_open_msg = open_msg
|
||||
self.send(open_msg)
|
||||
self._peer.connection_made()
|
||||
LOG.debug('Sent open message %s' % open_msg)
|
||||
|
||||
def connection_lost(self, reason):
|
||||
"""Stops all timers and notifies peer that connection is lost.
|
||||
"""
|
||||
|
||||
if self._peer:
|
||||
state = self._peer.state.bgp_state
|
||||
if self._is_bound or state == BGP_FSM_OPEN_SENT:
|
||||
self._peer.connection_lost(reason)
|
||||
|
||||
self._peer = None
|
||||
|
||||
if reason:
|
||||
LOG.info(reason)
|
||||
else:
|
||||
LOG.info('Connection to peer closed for unknown reasons.')
|
||||
121
ryu/services/protocols/bgp/utils/bgp.py
Normal file
121
ryu/services/protocols/bgp/utils/bgp.py
Normal file
@ -0,0 +1,121 @@
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Utilities related to bgp data types and models.
|
||||
"""
|
||||
import logging
|
||||
import socket
|
||||
|
||||
from ryu.services.protocols.bgp.protocols.bgp.messages import Update
|
||||
from ryu.services.protocols.bgp.protocols.bgp.nlri import RF_IPv4_VPN
|
||||
from ryu.services.protocols.bgp.protocols.bgp.nlri import RF_IPv6_VPN
|
||||
from ryu.services.protocols.bgp.protocols.bgp.nlri import RF_RTC_UC
|
||||
from ryu.services.protocols.bgp.protocols.bgp.nlri import RtNlri
|
||||
from ryu.services.protocols.bgp.protocols.bgp import pathattr
|
||||
from ryu.services.protocols.bgp.protocols.bgp.pathattr import Med
|
||||
from ryu.services.protocols.bgp.info_base.rtc import RtcPath
|
||||
from ryu.services.protocols.bgp.info_base.vpnv4 import Vpnv4Path
|
||||
from ryu.services.protocols.bgp.info_base.vpnv6 import Vpnv6Path
|
||||
|
||||
|
||||
LOG = logging.getLogger('utils.bgp')
|
||||
|
||||
# RouteFmaily to path sub-class mapping.
|
||||
_ROUTE_FAMILY_TO_PATH_MAP = {RF_IPv4_VPN: Vpnv4Path,
|
||||
RF_IPv6_VPN: Vpnv6Path,
|
||||
RF_RTC_UC: RtcPath}
|
||||
|
||||
|
||||
def create_path(src_peer, nlri, **kwargs):
|
||||
route_family = nlri.route_family
|
||||
assert route_family in _ROUTE_FAMILY_TO_PATH_MAP.keys()
|
||||
path_cls = _ROUTE_FAMILY_TO_PATH_MAP.get(route_family)
|
||||
return path_cls(src_peer, nlri, src_peer.version_num, **kwargs)
|
||||
|
||||
|
||||
def clone_path_and_update_med_for_target_neighbor(path, med):
|
||||
assert path and med
|
||||
route_family = path.route_family
|
||||
if route_family not in _ROUTE_FAMILY_TO_PATH_MAP.keys():
|
||||
raise ValueError('Clone is not supported for address-family %s' %
|
||||
route_family)
|
||||
path_cls = _ROUTE_FAMILY_TO_PATH_MAP.get(route_family)
|
||||
pattrs = path.pathattr_map
|
||||
pattrs[Med.ATTR_NAME] = Med(med)
|
||||
return path_cls(
|
||||
path.source, path.nlri, path.source_version_num,
|
||||
pattrs=pattrs, nexthop=path.nexthop,
|
||||
is_withdraw=path.is_withdraw,
|
||||
med_set_by_target_neighbor=True
|
||||
)
|
||||
|
||||
|
||||
def clone_rtcpath_update_rt_as(path, new_rt_as):
|
||||
"""Clones given RT NLRI `path`, and updates it with new RT_NLRI AS.
|
||||
|
||||
Parameters:
|
||||
- `path`: (Path) RT_NLRI path
|
||||
- `new_rt_as`: AS value of cloned paths' RT_NLRI
|
||||
"""
|
||||
assert path and new_rt_as
|
||||
if not path or path.route_family != RF_RTC_UC:
|
||||
raise ValueError('Expected RT_NLRI path')
|
||||
old_nlri = path.nlri
|
||||
new_rt_nlri = RtNlri(new_rt_as, old_nlri.route_target)
|
||||
return RtcPath(path.source, new_rt_nlri, path.source_version_num,
|
||||
pattrs=path.pathattr_map, nexthop=path.nexthop,
|
||||
is_withdraw=path.is_withdraw)
|
||||
|
||||
|
||||
def from_inet_ptoi(bgp_id):
|
||||
"""Convert an IPv4 address string format to a four byte long.
|
||||
"""
|
||||
four_byte_id = None
|
||||
try:
|
||||
packed_byte = socket.inet_pton(socket.AF_INET, bgp_id)
|
||||
four_byte_id = long(packed_byte.encode('hex'), 16)
|
||||
except ValueError:
|
||||
LOG.debug('Invalid bgp id given for conversion to integer value %s' %
|
||||
bgp_id)
|
||||
|
||||
return four_byte_id
|
||||
|
||||
|
||||
def get_unknow_opttrans_attr(path):
|
||||
"""Utility method that gives a `dict` of unknown optional transitive
|
||||
path attributes of `path`.
|
||||
|
||||
Returns dict: <key> - attribute type code, <value> - unknown path-attr.
|
||||
"""
|
||||
path_attrs = path.pathattr_map
|
||||
unknown_opt_tran_attrs = {}
|
||||
for _, attr in path_attrs.iteritems():
|
||||
if (isinstance(attr, pathattr.UnRcgPathAttr) and
|
||||
attr.is_optional_transitive()):
|
||||
unknown_opt_tran_attrs[attr.type_code] = attr
|
||||
return unknown_opt_tran_attrs
|
||||
|
||||
|
||||
def create_end_of_rib_update():
|
||||
"""Construct end-of-rib (EOR) Update instance."""
|
||||
mpunreach_attr = pathattr.MpUnreachNlri(RF_IPv4_VPN, [])
|
||||
pathattr_map = {pathattr.MpUnreachNlri.ATTR_NAME: mpunreach_attr}
|
||||
eor = Update(pathattr_map)
|
||||
return eor
|
||||
|
||||
|
||||
# Bgp update message instance that can used as End of RIB marker.
|
||||
UPDATE_EOR = create_end_of_rib_update()
|
||||
265
ryu/services/protocols/bgp/utils/circlist.py
Normal file
265
ryu/services/protocols/bgp/utils/circlist.py
Normal file
@ -0,0 +1,265 @@
|
||||
# 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.
|
||||
|
||||
|
||||
class CircularListType(object):
|
||||
"""Instances of this class represent a specific type of list.
|
||||
Nodes are linked in a circular fashion, using attributes on the
|
||||
nodes themselves.
|
||||
|
||||
Example:
|
||||
|
||||
ItemList = CircularListType(next_attr='_next',
|
||||
prev_attr='_prev')
|
||||
|
||||
l = ItemList()
|
||||
l.prepend(item)
|
||||
|
||||
The created list has the following properties:
|
||||
|
||||
- A node can be inserted O(1) time at the head, tail, or
|
||||
after/before another specified node.
|
||||
|
||||
- A node can be removed in O(1) time from any list it may be on,
|
||||
without providing a reference to the list.
|
||||
|
||||
- The current node in an iteration can be deleted safely.
|
||||
|
||||
"""
|
||||
|
||||
class List(object):
|
||||
"""An object that represents a list.
|
||||
|
||||
This class is not expected to be used directly by clients. Rather, they
|
||||
would use the 'create' method of a CircularListType object to create an
|
||||
instance.
|
||||
"""
|
||||
|
||||
# Define the set of valid attributes so as to make the list
|
||||
# head lightweight.
|
||||
#
|
||||
# We override __getattr__ and __setattr__ so as to store the
|
||||
# the next and previous references on the list head in
|
||||
# _next_slot_ and _prev_slot_ respectively.
|
||||
__slots__ = ["list_type", "head", "_next_slot_",
|
||||
"_prev_slot_"]
|
||||
|
||||
def __init__(self, list_type):
|
||||
self.list_type = list_type
|
||||
|
||||
# Save memory by using the List object itself as the head.
|
||||
self.head = self
|
||||
self.list_type.node_init(self.head)
|
||||
|
||||
def __getattr__(self, name):
|
||||
if(name == self.list_type.next_name):
|
||||
return self._next_slot_
|
||||
|
||||
if(name == self.list_type.prev_name):
|
||||
return self._prev_slot_
|
||||
|
||||
raise AttributeError(name)
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
if(name in CircularListType.List.__slots__):
|
||||
object.__setattr__(self, name, value)
|
||||
return
|
||||
|
||||
if(name == self.list_type.next_name):
|
||||
self._next_slot_ = value
|
||||
return
|
||||
|
||||
if(name == self.list_type.prev_name):
|
||||
self._prev_slot_ = value
|
||||
return
|
||||
|
||||
raise AttributeError(name)
|
||||
|
||||
def is_empty(self):
|
||||
return not self.list_type.node_is_on_list(self.head)
|
||||
|
||||
def clear(self):
|
||||
"""Remove all items from the list."""
|
||||
|
||||
# Make sure that all items are unlinked.
|
||||
for node in self:
|
||||
self.remove(node)
|
||||
|
||||
def is_on_list(self, node):
|
||||
return self.list_type.node_is_on_list(node)
|
||||
|
||||
def append(self, node):
|
||||
self.list_type.node_insert_before(self.head, node)
|
||||
|
||||
def prepend(self, node):
|
||||
self.list_type.node_insert_after(self.head, node)
|
||||
|
||||
def __iter__(self):
|
||||
return self.generator()
|
||||
|
||||
def remove(self, node):
|
||||
"""List the given node from the list.
|
||||
|
||||
Note that this does not verify that the node is on this
|
||||
list. It could even be on a different list.
|
||||
"""
|
||||
self.list_type.node_unlink(node)
|
||||
|
||||
self.list_type.node_del_attrs(node)
|
||||
|
||||
def pop_first(self):
|
||||
"""Remove the first item in the list and return it."""
|
||||
node = self.list_type.node_next(self.head)
|
||||
if(node is self.head):
|
||||
return None
|
||||
|
||||
self.remove(node)
|
||||
return node
|
||||
|
||||
def generator(self):
|
||||
"""Enables iteration over the list.
|
||||
|
||||
The current item can safely be removed from the list during
|
||||
iteration.
|
||||
"""
|
||||
# Keep a pointer to the next node when returning the
|
||||
# current node. This allows the caller to remove the
|
||||
# current node safely.
|
||||
node = self.list_type.node_next(self.head)
|
||||
next = self.list_type.node_next(node)
|
||||
while(node is not self.head):
|
||||
yield node
|
||||
|
||||
node = next
|
||||
next = self.list_type.node_next(node)
|
||||
|
||||
#
|
||||
# CircularListType methods
|
||||
#
|
||||
|
||||
def __init__(self, next_attr_name=None, prev_attr_name=None):
|
||||
"""Initializes this list.
|
||||
|
||||
next_attr_name: The name of the attribute that holds a reference
|
||||
to the next item in the list.
|
||||
|
||||
prev_attr_name: the name of the attribute that holds a reference
|
||||
to the previous item in the list.
|
||||
"""
|
||||
|
||||
# Keep an interned version of the attribute names. This should
|
||||
# speed up the process of looking up the attributes.
|
||||
self.next_name = intern(next_attr_name)
|
||||
self.prev_name = intern(prev_attr_name)
|
||||
|
||||
def create(self):
|
||||
return CircularListType.List(self)
|
||||
|
||||
def __call__(self):
|
||||
"""Make a CircularListType instance look like a class by
|
||||
creating a list object.
|
||||
"""
|
||||
return self.create()
|
||||
|
||||
def node_init(self, node):
|
||||
assert(not self.node_is_on_list(node))
|
||||
|
||||
# Set the node to point to itself as the previous and next
|
||||
# entries.
|
||||
self.node_set_next(node, node)
|
||||
self.node_set_prev(node, node)
|
||||
|
||||
def node_next(self, node):
|
||||
try:
|
||||
return getattr(node, self.next_name)
|
||||
except AttributeError:
|
||||
return None
|
||||
|
||||
def node_set_next(self, node, next):
|
||||
setattr(node, self.next_name, next)
|
||||
|
||||
def node_prev(self, node):
|
||||
try:
|
||||
return getattr(node, self.prev_name)
|
||||
except AttributeError:
|
||||
return None
|
||||
|
||||
def node_set_prev(self, node, prev):
|
||||
setattr(node, self.prev_name, prev)
|
||||
|
||||
def node_del_attrs(self, node):
|
||||
"""Remove all attributes that are used for putting this node
|
||||
on this type of list.
|
||||
"""
|
||||
try:
|
||||
delattr(node, self.next_name)
|
||||
delattr(node, self.prev_name)
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
def node_is_on_list(self, node):
|
||||
"""Returns True if this node is on *some* list.
|
||||
|
||||
A node is not on any list if it is linked to itself, or if it
|
||||
does not have the next and/prev attributes at all.
|
||||
"""
|
||||
next = self.node_next(node)
|
||||
if next == node or next is None:
|
||||
assert(self.node_prev(node) is next)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def node_insert_after(self, node, new_node):
|
||||
"""Insert the new node after node."""
|
||||
|
||||
assert(not self.node_is_on_list(new_node))
|
||||
assert(node is not new_node)
|
||||
|
||||
next = self.node_next(node)
|
||||
assert(next is not None)
|
||||
self.node_set_next(node, new_node)
|
||||
self.node_set_prev(new_node, node)
|
||||
|
||||
self.node_set_next(new_node, next)
|
||||
self.node_set_prev(next, new_node)
|
||||
|
||||
def node_insert_before(self, node, new_node):
|
||||
"""Insert the new node before node."""
|
||||
|
||||
assert(not self.node_is_on_list(new_node))
|
||||
assert(node is not new_node)
|
||||
|
||||
prev = self.node_prev(node)
|
||||
assert(prev is not None)
|
||||
self.node_set_prev(node, new_node)
|
||||
self.node_set_next(new_node, node)
|
||||
|
||||
self.node_set_prev(new_node, prev)
|
||||
self.node_set_next(prev, new_node)
|
||||
|
||||
def node_unlink(self, node):
|
||||
|
||||
if not self.node_is_on_list(node):
|
||||
return
|
||||
|
||||
prev = self.node_prev(node)
|
||||
next = self.node_next(node)
|
||||
|
||||
self.node_set_next(prev, next)
|
||||
self.node_set_prev(next, prev)
|
||||
|
||||
self.node_set_next(node, node)
|
||||
self.node_set_prev(node, node)
|
||||
562
ryu/services/protocols/bgp/utils/dictconfig.py
Normal file
562
ryu/services/protocols/bgp/utils/dictconfig.py
Normal file
@ -0,0 +1,562 @@
|
||||
# Copyright 2009-2010 by Vinay Sajip. All Rights Reserved.
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and its
|
||||
# documentation for any purpose and without fee is hereby granted,
|
||||
# provided that the above copyright notice appear in all copies and that
|
||||
# both that copyright notice and this permission notice appear in
|
||||
# supporting documentation, and that the name of Vinay Sajip
|
||||
# not be used in advertising or publicity pertaining to distribution
|
||||
# of the software without specific, written prior permission.
|
||||
# VINAY SAJIP DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING
|
||||
# ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
|
||||
# VINAY SAJIP BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR
|
||||
# ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER
|
||||
# IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
|
||||
# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
# Source: https://bitbucket.org/vinay.sajip/dictconfig/raw/53b3c32dea4694cd3fb2f14b3159d66d3da10bc0/src/dictconfig.py
|
||||
# flake8: noqa
|
||||
import logging.handlers
|
||||
import re
|
||||
import sys
|
||||
import types
|
||||
|
||||
IDENTIFIER = re.compile('^[a-z_][a-z0-9_]*$', re.I)
|
||||
|
||||
|
||||
def valid_ident(s):
|
||||
m = IDENTIFIER.match(s)
|
||||
if not m:
|
||||
raise ValueError('Not a valid Python identifier: %r' % s)
|
||||
return True
|
||||
|
||||
#
|
||||
# This function is defined in logging only in recent versions of Python
|
||||
#
|
||||
try:
|
||||
from logging import _checkLevel
|
||||
except ImportError:
|
||||
def _checkLevel(level):
|
||||
if isinstance(level, int):
|
||||
rv = level
|
||||
elif str(level) == level:
|
||||
if level not in logging._levelNames:
|
||||
raise ValueError('Unknown level: %r' % level)
|
||||
rv = logging._levelNames[level]
|
||||
else:
|
||||
raise TypeError('Level not an integer or a '
|
||||
'valid string: %r' % level)
|
||||
return rv
|
||||
|
||||
# The ConvertingXXX classes are wrappers around standard Python containers,
|
||||
# and they serve to convert any suitable values in the container. The
|
||||
# conversion converts base dicts, lists and tuples to their wrapped
|
||||
# equivalents, whereas strings which match a conversion format are converted
|
||||
# appropriately.
|
||||
#
|
||||
# Each wrapper should have a configurator attribute holding the actual
|
||||
# configurator to use for conversion.
|
||||
|
||||
|
||||
class ConvertingDict(dict):
|
||||
"""A converting dictionary wrapper."""
|
||||
|
||||
def __getitem__(self, key):
|
||||
value = dict.__getitem__(self, key)
|
||||
result = self.configurator.convert(value)
|
||||
# If the converted value is different, save for next time
|
||||
if value is not result:
|
||||
self[key] = result
|
||||
if type(result) in (ConvertingDict, ConvertingList,
|
||||
ConvertingTuple):
|
||||
result.parent = self
|
||||
result.key = key
|
||||
return result
|
||||
|
||||
def get(self, key, default=None):
|
||||
value = dict.get(self, key, default)
|
||||
result = self.configurator.convert(value)
|
||||
# If the converted value is different, save for next time
|
||||
if value is not result:
|
||||
self[key] = result
|
||||
if type(result) in (ConvertingDict, ConvertingList,
|
||||
ConvertingTuple):
|
||||
result.parent = self
|
||||
result.key = key
|
||||
return result
|
||||
|
||||
def pop(self, key, default=None):
|
||||
value = dict.pop(self, key, default)
|
||||
result = self.configurator.convert(value)
|
||||
if value is not result:
|
||||
if type(result) in (ConvertingDict, ConvertingList,
|
||||
ConvertingTuple):
|
||||
result.parent = self
|
||||
result.key = key
|
||||
return result
|
||||
|
||||
|
||||
class ConvertingList(list):
|
||||
"""A converting list wrapper."""
|
||||
def __getitem__(self, key):
|
||||
value = list.__getitem__(self, key)
|
||||
result = self.configurator.convert(value)
|
||||
# If the converted value is different, save for next time
|
||||
if value is not result:
|
||||
self[key] = result
|
||||
if type(result) in (ConvertingDict, ConvertingList,
|
||||
ConvertingTuple):
|
||||
result.parent = self
|
||||
result.key = key
|
||||
return result
|
||||
|
||||
def pop(self, idx= -1):
|
||||
value = list.pop(self, idx)
|
||||
result = self.configurator.convert(value)
|
||||
if value is not result:
|
||||
if type(result) in (ConvertingDict, ConvertingList,
|
||||
ConvertingTuple):
|
||||
result.parent = self
|
||||
return result
|
||||
|
||||
|
||||
class ConvertingTuple(tuple):
|
||||
"""A converting tuple wrapper."""
|
||||
def __getitem__(self, key):
|
||||
value = tuple.__getitem__(self, key)
|
||||
result = self.configurator.convert(value)
|
||||
if value is not result:
|
||||
if type(result) in (ConvertingDict, ConvertingList,
|
||||
ConvertingTuple):
|
||||
result.parent = self
|
||||
result.key = key
|
||||
return result
|
||||
|
||||
|
||||
class BaseConfigurator(object):
|
||||
"""
|
||||
The configurator base class which defines some useful defaults.
|
||||
"""
|
||||
|
||||
CONVERT_PATTERN = re.compile(r'^(?P<prefix>[a-z]+)://(?P<suffix>.*)$')
|
||||
|
||||
WORD_PATTERN = re.compile(r'^\s*(\w+)\s*')
|
||||
DOT_PATTERN = re.compile(r'^\.\s*(\w+)\s*')
|
||||
INDEX_PATTERN = re.compile(r'^\[\s*(\w+)\s*\]\s*')
|
||||
DIGIT_PATTERN = re.compile(r'^\d+$')
|
||||
|
||||
value_converters = {
|
||||
'ext': 'ext_convert',
|
||||
'cfg': 'cfg_convert',
|
||||
}
|
||||
|
||||
# We might want to use a different one, e.g. importlib
|
||||
importer = __import__
|
||||
|
||||
def __init__(self, config):
|
||||
self.config = ConvertingDict(config)
|
||||
self.config.configurator = self
|
||||
|
||||
def resolve(self, s):
|
||||
"""
|
||||
Resolve strings to objects using standard import and attribute
|
||||
syntax.
|
||||
"""
|
||||
name = s.split('.')
|
||||
used = name.pop(0)
|
||||
try:
|
||||
found = self.importer(used)
|
||||
for frag in name:
|
||||
used += '.' + frag
|
||||
try:
|
||||
found = getattr(found, frag)
|
||||
except AttributeError:
|
||||
self.importer(used)
|
||||
found = getattr(found, frag)
|
||||
return found
|
||||
except ImportError:
|
||||
e, tb = sys.exc_info()[1:]
|
||||
v = ValueError('Cannot resolve %r: %s' % (s, e))
|
||||
v.__cause__, v.__traceback__ = e, tb
|
||||
raise v
|
||||
|
||||
def ext_convert(self, value):
|
||||
"""Default converter for the ext:// protocol."""
|
||||
return self.resolve(value)
|
||||
|
||||
def cfg_convert(self, value):
|
||||
"""Default converter for the cfg:// protocol."""
|
||||
rest = value
|
||||
m = self.WORD_PATTERN.match(rest)
|
||||
if m is None:
|
||||
raise ValueError("Unable to convert %r" % value)
|
||||
else:
|
||||
rest = rest[m.end():]
|
||||
d = self.config[m.groups()[0]]
|
||||
# print d, rest
|
||||
while rest:
|
||||
m = self.DOT_PATTERN.match(rest)
|
||||
if m:
|
||||
d = d[m.groups()[0]]
|
||||
else:
|
||||
m = self.INDEX_PATTERN.match(rest)
|
||||
if m:
|
||||
idx = m.groups()[0]
|
||||
if not self.DIGIT_PATTERN.match(idx):
|
||||
d = d[idx]
|
||||
else:
|
||||
try:
|
||||
# try as number first (most likely)
|
||||
n = int(idx)
|
||||
d = d[n]
|
||||
except TypeError:
|
||||
d = d[idx]
|
||||
if m:
|
||||
rest = rest[m.end():]
|
||||
else:
|
||||
raise ValueError('Unable to convert '
|
||||
'%r at %r' % (value, rest))
|
||||
# rest should be empty
|
||||
return d
|
||||
|
||||
def convert(self, value):
|
||||
"""
|
||||
Convert values to an appropriate type. dicts, lists and tuples are
|
||||
replaced by their converting alternatives. Strings are checked to
|
||||
see if they have a conversion format and are converted if they do.
|
||||
"""
|
||||
if not isinstance(value, ConvertingDict) and isinstance(value, dict):
|
||||
value = ConvertingDict(value)
|
||||
value.configurator = self
|
||||
elif not isinstance(value, ConvertingList) and isinstance(value, list):
|
||||
value = ConvertingList(value)
|
||||
value.configurator = self
|
||||
elif not isinstance(value, ConvertingTuple) and\
|
||||
isinstance(value, tuple):
|
||||
value = ConvertingTuple(value)
|
||||
value.configurator = self
|
||||
elif isinstance(value, basestring): # str for py3k
|
||||
m = self.CONVERT_PATTERN.match(value)
|
||||
if m:
|
||||
d = m.groupdict()
|
||||
prefix = d['prefix']
|
||||
converter = self.value_converters.get(prefix, None)
|
||||
if converter:
|
||||
suffix = d['suffix']
|
||||
converter = getattr(self, converter)
|
||||
value = converter(suffix)
|
||||
return value
|
||||
|
||||
def configure_custom(self, config):
|
||||
"""Configure an object with a user-supplied factory."""
|
||||
c = config.pop('()')
|
||||
if (not hasattr(c, '__call__') and hasattr(types, 'ClassType') and
|
||||
type(c) != types.ClassType):
|
||||
c = self.resolve(c)
|
||||
props = config.pop('.', None)
|
||||
# Check for valid identifiers
|
||||
kwargs = dict([(k, config[k]) for k in config if valid_ident(k)])
|
||||
result = c(**kwargs)
|
||||
if props:
|
||||
for name, value in props.items():
|
||||
setattr(result, name, value)
|
||||
return result
|
||||
|
||||
def as_tuple(self, value):
|
||||
"""Utility function which converts lists to tuples."""
|
||||
if isinstance(value, list):
|
||||
value = tuple(value)
|
||||
return value
|
||||
|
||||
|
||||
class DictConfigurator(BaseConfigurator):
|
||||
"""
|
||||
Configure logging using a dictionary-like object to describe the
|
||||
configuration.
|
||||
"""
|
||||
|
||||
def configure(self):
|
||||
"""Do the configuration."""
|
||||
|
||||
config = self.config
|
||||
if 'version' not in config:
|
||||
raise ValueError("dictionary doesn't specify a version")
|
||||
if config['version'] != 1:
|
||||
raise ValueError("Unsupported version: %s" % config['version'])
|
||||
incremental = config.pop('incremental', False)
|
||||
EMPTY_DICT = {}
|
||||
logging._acquireLock()
|
||||
try:
|
||||
if incremental:
|
||||
handlers = config.get('handlers', EMPTY_DICT)
|
||||
# incremental handler config only if handler name
|
||||
# ties in to logging._handlers (Python 2.7)
|
||||
if sys.version_info[:2] == (2, 7):
|
||||
for name in handlers:
|
||||
if name not in logging._handlers:
|
||||
raise ValueError('No handler found with '
|
||||
'name %r' % name)
|
||||
else:
|
||||
try:
|
||||
handler = logging._handlers[name]
|
||||
handler_config = handlers[name]
|
||||
level = handler_config.get('level', None)
|
||||
if level:
|
||||
handler.setLevel(_checkLevel(level))
|
||||
except StandardError, e:
|
||||
raise ValueError('Unable to configure handler '
|
||||
'%r: %s' % (name, e))
|
||||
loggers = config.get('loggers', EMPTY_DICT)
|
||||
for name in loggers:
|
||||
try:
|
||||
self.configure_logger(name, loggers[name], True)
|
||||
except StandardError, e:
|
||||
raise ValueError('Unable to configure logger '
|
||||
'%r: %s' % (name, e))
|
||||
root = config.get('root', None)
|
||||
if root:
|
||||
try:
|
||||
self.configure_root(root, True)
|
||||
except StandardError, e:
|
||||
raise ValueError('Unable to configure root '
|
||||
'logger: %s' % e)
|
||||
else:
|
||||
disable_existing = config.pop('disable_existing_loggers', True)
|
||||
|
||||
logging._handlers.clear()
|
||||
del logging._handlerList[:]
|
||||
|
||||
# Do formatters first - they don't refer to anything else
|
||||
formatters = config.get('formatters', EMPTY_DICT)
|
||||
for name in formatters:
|
||||
try:
|
||||
formatters[name] = self.configure_formatter(
|
||||
formatters[name])
|
||||
except StandardError, e:
|
||||
raise ValueError('Unable to configure '
|
||||
'formatter %r: %s' % (name, e))
|
||||
# Next, do filters - they don't refer to anything else, either
|
||||
filters = config.get('filters', EMPTY_DICT)
|
||||
for name in filters:
|
||||
try:
|
||||
filters[name] = self.configure_filter(filters[name])
|
||||
except StandardError, e:
|
||||
raise ValueError('Unable to configure '
|
||||
'filter %r: %s' % (name, e))
|
||||
|
||||
# Next, do handlers - they refer to formatters and filters
|
||||
# As handlers can refer to other handlers, sort the keys
|
||||
# to allow a deterministic order of configuration
|
||||
handlers = config.get('handlers', EMPTY_DICT)
|
||||
for name in sorted(handlers):
|
||||
try:
|
||||
handler = self.configure_handler(handlers[name])
|
||||
handler.name = name
|
||||
handlers[name] = handler
|
||||
except StandardError, e:
|
||||
raise ValueError('Unable to configure handler '
|
||||
'%r: %s' % (name, e))
|
||||
# Next, do loggers - they refer to handlers and filters
|
||||
|
||||
# we don't want to lose the existing loggers,
|
||||
# since other threads may have pointers to them.
|
||||
# existing is set to contain all existing loggers,
|
||||
# and as we go through the new configuration we
|
||||
# remove any which are configured. At the end,
|
||||
# what's left in existing is the set of loggers
|
||||
# which were in the previous configuration but
|
||||
# which are not in the new configuration.
|
||||
root = logging.root
|
||||
existing = root.manager.loggerDict.keys()
|
||||
# The list needs to be sorted so that we can
|
||||
# avoid disabling child loggers of explicitly
|
||||
# named loggers. With a sorted list it is easier
|
||||
# to find the child loggers.
|
||||
existing.sort()
|
||||
# We'll keep the list of existing loggers
|
||||
# which are children of named loggers here...
|
||||
child_loggers = []
|
||||
# now set up the new ones...
|
||||
loggers = config.get('loggers', EMPTY_DICT)
|
||||
for name in loggers:
|
||||
if name in existing:
|
||||
i = existing.index(name)
|
||||
prefixed = name + "."
|
||||
pflen = len(prefixed)
|
||||
num_existing = len(existing)
|
||||
i = i + 1 # look at the entry after name
|
||||
while (i < num_existing) and\
|
||||
(existing[i][:pflen] == prefixed):
|
||||
child_loggers.append(existing[i])
|
||||
i = i + 1
|
||||
existing.remove(name)
|
||||
try:
|
||||
self.configure_logger(name, loggers[name])
|
||||
except StandardError, e:
|
||||
raise ValueError('Unable to configure logger '
|
||||
'%r: %s' % (name, e))
|
||||
|
||||
# Disable any old loggers. There's no point deleting
|
||||
# them as other threads may continue to hold references
|
||||
# and by disabling them, you stop them doing any logging.
|
||||
# However, don't disable children of named loggers, as that's
|
||||
# probably not what was intended by the user.
|
||||
for log in existing:
|
||||
logger = root.manager.loggerDict[log]
|
||||
if log in child_loggers:
|
||||
logger.level = logging.NOTSET
|
||||
logger.handlers = []
|
||||
logger.propagate = True
|
||||
elif disable_existing:
|
||||
logger.disabled = True
|
||||
|
||||
# And finally, do the root logger
|
||||
root = config.get('root', None)
|
||||
if root:
|
||||
try:
|
||||
self.configure_root(root)
|
||||
except StandardError, e:
|
||||
raise ValueError('Unable to configure root '
|
||||
'logger: %s' % e)
|
||||
finally:
|
||||
logging._releaseLock()
|
||||
|
||||
def configure_formatter(self, config):
|
||||
"""Configure a formatter from a dictionary."""
|
||||
if '()' in config:
|
||||
factory = config['()'] # for use in exception handler
|
||||
try:
|
||||
result = self.configure_custom(config)
|
||||
except TypeError, te:
|
||||
if "'format'" not in str(te):
|
||||
raise
|
||||
# Name of parameter changed from fmt to format.
|
||||
# Retry with old name.
|
||||
# This is so that code can be used with older Python versions
|
||||
# (e.g. by Django)
|
||||
config['fmt'] = config.pop('format')
|
||||
config['()'] = factory
|
||||
result = self.configure_custom(config)
|
||||
else:
|
||||
fmt = config.get('format', None)
|
||||
dfmt = config.get('datefmt', None)
|
||||
result = logging.Formatter(fmt, dfmt)
|
||||
return result
|
||||
|
||||
def configure_filter(self, config):
|
||||
"""Configure a filter from a dictionary."""
|
||||
if '()' in config:
|
||||
result = self.configure_custom(config)
|
||||
else:
|
||||
name = config.get('name', '')
|
||||
result = logging.Filter(name)
|
||||
return result
|
||||
|
||||
def add_filters(self, filterer, filters):
|
||||
"""Add filters to a filterer from a list of names."""
|
||||
for f in filters:
|
||||
try:
|
||||
filterer.addFilter(self.config['filters'][f])
|
||||
except StandardError, e:
|
||||
raise ValueError('Unable to add filter %r: %s' % (f, e))
|
||||
|
||||
def configure_handler(self, config):
|
||||
"""Configure a handler from a dictionary."""
|
||||
formatter = config.pop('formatter', None)
|
||||
if formatter:
|
||||
try:
|
||||
formatter = self.config['formatters'][formatter]
|
||||
except StandardError, e:
|
||||
raise ValueError('Unable to set formatter '
|
||||
'%r: %s' % (formatter, e))
|
||||
level = config.pop('level', None)
|
||||
filters = config.pop('filters', None)
|
||||
if '()' in config:
|
||||
c = config.pop('()')
|
||||
if (not hasattr(c, '__call__') and
|
||||
hasattr(types, 'ClassType') and
|
||||
type(c) != types.ClassType):
|
||||
c = self.resolve(c)
|
||||
factory = c
|
||||
else:
|
||||
klass = self.resolve(config.pop('class'))
|
||||
# Special case for handler which refers to another handler
|
||||
if issubclass(klass, logging.handlers.MemoryHandler) and\
|
||||
'target' in config:
|
||||
try:
|
||||
trgt = self.config['handlers'][config['target']]
|
||||
config['target'] = trgt
|
||||
except StandardError, e:
|
||||
raise ValueError('Unable to set target handler '
|
||||
'%r: %s' % (config['target'], e))
|
||||
elif issubclass(klass, logging.handlers.SMTPHandler) and\
|
||||
'mailhost' in config:
|
||||
config['mailhost'] = self.as_tuple(config['mailhost'])
|
||||
elif issubclass(klass, logging.handlers.SysLogHandler) and\
|
||||
'address' in config:
|
||||
config['address'] = self.as_tuple(config['address'])
|
||||
factory = klass
|
||||
kwargs = dict([(k, config[k]) for k in config if valid_ident(k)])
|
||||
try:
|
||||
result = factory(**kwargs)
|
||||
except TypeError, te:
|
||||
if "'stream'" not in str(te):
|
||||
raise
|
||||
# The argument name changed from strm to stream
|
||||
# Retry with old name.
|
||||
# This is so that code can be used with older Python versions
|
||||
# (e.g. by Django)
|
||||
kwargs['strm'] = kwargs.pop('stream')
|
||||
result = factory(**kwargs)
|
||||
if formatter:
|
||||
result.setFormatter(formatter)
|
||||
if level is not None:
|
||||
result.setLevel(_checkLevel(level))
|
||||
if filters:
|
||||
self.add_filters(result, filters)
|
||||
return result
|
||||
|
||||
def add_handlers(self, logger, handlers):
|
||||
"""Add handlers to a logger from a list of names."""
|
||||
for h in handlers:
|
||||
try:
|
||||
logger.addHandler(self.config['handlers'][h])
|
||||
except StandardError, e:
|
||||
raise ValueError('Unable to add handler %r: %s' % (h, e))
|
||||
|
||||
def common_logger_config(self, logger, config, incremental=False):
|
||||
"""
|
||||
Perform configuration which is common to root and non-root loggers.
|
||||
"""
|
||||
level = config.get('level', None)
|
||||
if level is not None:
|
||||
logger.setLevel(_checkLevel(level))
|
||||
if not incremental:
|
||||
# Remove any existing handlers
|
||||
for h in logger.handlers[:]:
|
||||
logger.removeHandler(h)
|
||||
handlers = config.get('handlers', None)
|
||||
if handlers:
|
||||
self.add_handlers(logger, handlers)
|
||||
filters = config.get('filters', None)
|
||||
if filters:
|
||||
self.add_filters(logger, filters)
|
||||
|
||||
def configure_logger(self, name, config, incremental=False):
|
||||
"""Configure a non-root logger from a dictionary."""
|
||||
logger = logging.getLogger(name)
|
||||
self.common_logger_config(logger, config, incremental)
|
||||
propagate = config.get('propagate', None)
|
||||
if propagate is not None:
|
||||
logger.propagate = propagate
|
||||
|
||||
def configure_root(self, config, incremental=False):
|
||||
"""Configure a root logger from a dictionary."""
|
||||
root = logging.getLogger()
|
||||
self.common_logger_config(root, config, incremental)
|
||||
|
||||
dictConfigClass = DictConfigurator
|
||||
|
||||
|
||||
def dictConfig(config):
|
||||
"""Configure logging using a dictionary."""
|
||||
dictConfigClass(config).configure()
|
||||
140
ryu/services/protocols/bgp/utils/evtlet.py
Normal file
140
ryu/services/protocols/bgp/utils/evtlet.py
Normal file
@ -0,0 +1,140 @@
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Concurrent networking library - Eventlet, based utilities classes.
|
||||
"""
|
||||
import eventlet
|
||||
from eventlet import event
|
||||
import logging
|
||||
|
||||
LOG = logging.getLogger('utils.evtlet')
|
||||
|
||||
|
||||
class EventletIOFactory(object):
|
||||
|
||||
@staticmethod
|
||||
def create_custom_event():
|
||||
LOG.debug('Create CustomEvent called')
|
||||
return CustomEvent()
|
||||
|
||||
@staticmethod
|
||||
def create_looping_call(funct, *args, **kwargs):
|
||||
LOG.debug('create_looping_call called')
|
||||
return LoopingCall(funct, *args, **kwargs)
|
||||
|
||||
|
||||
class CustomEvent(object):
|
||||
"""Encapsulates eventlet event to provide a event which can recur.
|
||||
|
||||
It has the same interface as threading.Event but works for eventlet.
|
||||
"""
|
||||
def __init__(self,):
|
||||
self._event = event.Event()
|
||||
self._is_set = False
|
||||
|
||||
def is_set(self):
|
||||
"""Return true if and only if the internal flag is true."""
|
||||
return self._is_set
|
||||
|
||||
def set(self):
|
||||
"""Set the internal flag to true.
|
||||
|
||||
All threads waiting for it to become true are awakened.
|
||||
Threads that call wait() once the flag is true will not block at all.
|
||||
"""
|
||||
if self._event and not self._event.ready():
|
||||
self._event.send()
|
||||
self._is_set = True
|
||||
|
||||
def clear(self):
|
||||
"""Reset the internal flag to false.
|
||||
|
||||
Subsequently, threads calling wait() will block until set() is called
|
||||
to set the internal flag to true again.
|
||||
"""
|
||||
if self._is_set:
|
||||
self._is_set = False
|
||||
self._event = event.Event()
|
||||
|
||||
def wait(self):
|
||||
"""Block until the internal flag is true.
|
||||
|
||||
If the internal flag is true on entry, return immediately. Otherwise,
|
||||
block until another thread calls set() to set the flag to true, or
|
||||
until the optional timeout occurs.
|
||||
"""
|
||||
if not self._is_set:
|
||||
self._event.wait()
|
||||
|
||||
|
||||
class LoopingCall(object):
|
||||
"""Call a function repeatedly.
|
||||
"""
|
||||
def __init__(self, funct, *args, **kwargs):
|
||||
self._funct = funct
|
||||
self._args = args
|
||||
self._kwargs = kwargs
|
||||
self._running = False
|
||||
self._interval = 0
|
||||
self._self_thread = None
|
||||
|
||||
@property
|
||||
def running(self):
|
||||
return self._running
|
||||
|
||||
@property
|
||||
def interval(self):
|
||||
return self._interval
|
||||
|
||||
def __call__(self):
|
||||
if self._running:
|
||||
# Schedule next iteration of the call.
|
||||
self._self_thread = eventlet.spawn_after(self._interval, self)
|
||||
self._funct(*self._args, **self._kwargs)
|
||||
|
||||
def start(self, interval, now=True):
|
||||
"""Start running pre-set function every interval seconds.
|
||||
"""
|
||||
if interval < 0:
|
||||
raise ValueError('interval must be >= 0')
|
||||
|
||||
if self._running:
|
||||
self.stop()
|
||||
|
||||
self._running = True
|
||||
self._interval = interval
|
||||
if now:
|
||||
self._self_thread = eventlet.spawn_after(0, self)
|
||||
else:
|
||||
self._self_thread = eventlet.spawn_after(self._interval, self)
|
||||
|
||||
def stop(self):
|
||||
"""Stop running scheduled function.
|
||||
"""
|
||||
self._running = False
|
||||
if self._self_thread is not None:
|
||||
self._self_thread.cancel()
|
||||
self._self_thread = None
|
||||
|
||||
def reset(self):
|
||||
"""Skip the next iteration and reset timer.
|
||||
"""
|
||||
if self._self_thread is not None:
|
||||
# Cancel currently scheduled call
|
||||
self._self_thread.cancel()
|
||||
self._self_thread = None
|
||||
# Schedule a new call
|
||||
self._self_thread = eventlet.spawn_after(self._interval, self)
|
||||
102
ryu/services/protocols/bgp/utils/internable.py
Normal file
102
ryu/services/protocols/bgp/utils/internable.py
Normal file
@ -0,0 +1,102 @@
|
||||
# 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.
|
||||
|
||||
import weakref
|
||||
|
||||
dict_name = intern('_internable_dict')
|
||||
|
||||
|
||||
#
|
||||
# Internable
|
||||
#
|
||||
class Internable(object):
|
||||
"""Class that allows instances to be 'interned'. That is, given an
|
||||
instance of this class, one can obtain a canonical (interned)
|
||||
copy.
|
||||
|
||||
This saves memory when there are likely to be many identical
|
||||
instances of the class -- users hold references to a single
|
||||
interned object instead of references to different objects that
|
||||
are identical.
|
||||
|
||||
The interned version of a given instance is created on demand if
|
||||
necessary, and automatically cleaned up when nobody holds a
|
||||
reference to it.
|
||||
|
||||
Instances of sub-classes must be usable as dictionary keys for
|
||||
Internable to work.
|
||||
"""
|
||||
|
||||
class Stats(object):
|
||||
|
||||
def __init__(self):
|
||||
self.d = {}
|
||||
|
||||
def incr(self, name):
|
||||
self.d[name] = self.d.get(name, 0) + 1
|
||||
|
||||
def __repr__(self):
|
||||
return repr(self.d)
|
||||
|
||||
def __str__(self):
|
||||
return str(self.d)
|
||||
|
||||
@classmethod
|
||||
def _internable_init(kls):
|
||||
# Objects to be interned are held as keys in a dictionary that
|
||||
# only holds weak references to keys. As a result, when the
|
||||
# last reference to an interned object goes away, the object
|
||||
# will be removed from the dictionary.
|
||||
kls._internable_dict = weakref.WeakKeyDictionary()
|
||||
kls._internable_stats = Internable.Stats()
|
||||
|
||||
@classmethod
|
||||
def intern_stats(kls):
|
||||
return kls._internable_stats
|
||||
|
||||
def intern(self):
|
||||
"""Returns either itself or a canonical copy of itself."""
|
||||
|
||||
# If this is an interned object, return it
|
||||
if hasattr(self, '_interned'):
|
||||
return self._internable_stats.incr('self')
|
||||
|
||||
#
|
||||
# Got to find or create an interned object identical to this
|
||||
# one. Auto-initialize the class if need be.
|
||||
#
|
||||
kls = self.__class__
|
||||
|
||||
if not hasattr(kls, dict_name):
|
||||
kls._internable_init()
|
||||
|
||||
obj = kls._internable_dict.get(self)
|
||||
if (obj):
|
||||
# Found an interned copy.
|
||||
kls._internable_stats.incr('found')
|
||||
return obj
|
||||
|
||||
# Create an interned copy. Take care to only keep a weak
|
||||
# reference to the object itself.
|
||||
def object_collected(obj):
|
||||
kls._internable_stats.incr('collected')
|
||||
# print("Object %s garbage collected" % obj)
|
||||
pass
|
||||
|
||||
ref = weakref.ref(self, object_collected)
|
||||
kls._internable_dict[self] = ref
|
||||
self._interned = True
|
||||
kls._internable_stats.incr('inserted')
|
||||
return self
|
||||
35
ryu/services/protocols/bgp/utils/logs.py
Normal file
35
ryu/services/protocols/bgp/utils/logs.py
Normal file
@ -0,0 +1,35 @@
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class ApgwFormatter(logging.Formatter):
|
||||
LOG_TIME_FORMAT = '%Y-%m-%dT%H:%M:%S.%fZ'
|
||||
COMPONENT_NAME = 'BGPSpeaker'
|
||||
|
||||
def format(self, record):
|
||||
msg = {
|
||||
'component_name': self.COMPONENT_NAME,
|
||||
'timestamp': datetime.utcfromtimestamp(
|
||||
time.time()
|
||||
).strftime(self.LOG_TIME_FORMAT),
|
||||
'msg': unicode(record.msg),
|
||||
'level': record.levelname
|
||||
|
||||
}
|
||||
|
||||
if hasattr(record, 'log_type'):
|
||||
assert record.log_type in ('log', 'stats', 'state')
|
||||
msg['log_type'] = record.log_type
|
||||
else:
|
||||
msg['log_type'] = 'log'
|
||||
if hasattr(record, 'resource_id'):
|
||||
msg['resource_id'] = record.resource_id
|
||||
if hasattr(record, 'resource_name'):
|
||||
msg['resource_name'] = record.resource_name
|
||||
|
||||
record.msg = json.dumps(msg)
|
||||
|
||||
return super(ApgwFormatter, self).format(record)
|
||||
11
ryu/services/protocols/bgp/utils/other.py
Normal file
11
ryu/services/protocols/bgp/utils/other.py
Normal file
@ -0,0 +1,11 @@
|
||||
def bytes2hex(given_bytes):
|
||||
return ''.join(["%02X " % ord(x) for x in given_bytes]).strip()
|
||||
|
||||
|
||||
def hex2byte(given_hex):
|
||||
given_hex = ''.join(given_hex.split())
|
||||
result = []
|
||||
for offset in range(0, len(given_hex), 2):
|
||||
result.append(chr(int(given_hex[offset:offset + 2], 16)))
|
||||
|
||||
return ''.join(result)
|
||||
219
ryu/services/protocols/bgp/utils/rtfilter.py
Normal file
219
ryu/services/protocols/bgp/utils/rtfilter.py
Normal file
@ -0,0 +1,219 @@
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Module for RT Filter related functionality.
|
||||
"""
|
||||
import logging
|
||||
|
||||
from ryu.services.protocols.bgp.protocols.bgp.nlri import RF_RTC_UC
|
||||
from ryu.services.protocols.bgp.protocols.bgp.nlri import RtNlri
|
||||
from ryu.services.protocols.bgp.protocols.bgp.pathattr import AsPath
|
||||
from ryu.services.protocols.bgp.protocols.bgp.pathattr import Origin
|
||||
from ryu.services.protocols.bgp.base import OrderedDict
|
||||
from ryu.services.protocols.bgp.info_base.rtc import RtcPath
|
||||
|
||||
LOG = logging.getLogger('bgpspeaker.util.rtfilter')
|
||||
|
||||
|
||||
class RouteTargetManager(object):
|
||||
def __init__(self, core_service, neighbors_conf, vrfs_conf):
|
||||
self._core_service = core_service
|
||||
# TODO(PH): Consider extending VrfsConfListener and
|
||||
# NeighborsConfListener
|
||||
self._neighbors_conf = neighbors_conf
|
||||
self._vrfs_conf = vrfs_conf
|
||||
|
||||
# Peer to its current RT filter map
|
||||
# <key>/value = <peer_ip>/<rt filter set>
|
||||
self._peer_to_rtfilter_map = {}
|
||||
|
||||
# Collection of import RTs of all configured VRFs
|
||||
self._all_vrfs_import_rts_set = set()
|
||||
|
||||
# Collection of current RTC AS for all configured Neighbors
|
||||
self._all_rtc_as_set = set()
|
||||
# Interested RTs according to current entries in RTC global table
|
||||
self._global_interested_rts = set()
|
||||
|
||||
@property
|
||||
def peer_to_rtfilter_map(self):
|
||||
return self._peer_to_rtfilter_map.copy()
|
||||
|
||||
@peer_to_rtfilter_map.setter
|
||||
def peer_to_rtfilter_map(self, new_map):
|
||||
self._peer_to_rtfilter_map = new_map.copy()
|
||||
|
||||
@property
|
||||
def global_interested_rts(self):
|
||||
return set(self._global_interested_rts)
|
||||
|
||||
def add_rt_nlri(self, route_target, is_withdraw=False):
|
||||
assert route_target
|
||||
# Since we allow RTC AS setting for each neighbor, we collect all we
|
||||
# collect allRTC AS settings and add a RT NLRI using each AS number
|
||||
rtc_as_set = set()
|
||||
# Add RT NLRI with local AS
|
||||
rtc_as_set.add(self._core_service.asn)
|
||||
# Collect RTC AS from neighbor settings
|
||||
rtc_as_set.update(self._neighbors_conf.rtc_as_set)
|
||||
# Add RT NLRI path (withdraw) for each RTC AS
|
||||
for rtc_as in rtc_as_set:
|
||||
self._add_rt_nlri_for_as(rtc_as, route_target, is_withdraw)
|
||||
|
||||
def _add_rt_nlri_for_as(self, rtc_as, route_target, is_withdraw=False):
|
||||
from ryu.services.protocols.bgp.speaker.core import EXPECTED_ORIGIN
|
||||
rt_nlri = RtNlri(rtc_as, route_target)
|
||||
# Create a dictionary for path-attrs.
|
||||
pattrs = OrderedDict()
|
||||
if not is_withdraw:
|
||||
# MpReachNlri and/or MpUnReachNlri attribute info. is contained
|
||||
# in the path. Hence we do not add these attributes here.
|
||||
pattrs[Origin.ATTR_NAME] = Origin(EXPECTED_ORIGIN)
|
||||
pattrs[AsPath.ATTR_NAME] = AsPath([])
|
||||
|
||||
# Create Path instance and initialize appropriately.
|
||||
path = RtcPath(None, rt_nlri, 0, is_withdraw=is_withdraw,
|
||||
pattrs=pattrs)
|
||||
tm = self._core_service.table_manager
|
||||
tm.learn_path(path)
|
||||
|
||||
def update_rtc_as_set(self):
|
||||
"""Syncs RT NLRIs for new and removed RTC_ASes.
|
||||
|
||||
This method should be called when a neighbor is added or removed.
|
||||
"""
|
||||
# Compute the diffs in RTC_ASes
|
||||
curr_rtc_as_set = self._neighbors_conf.rtc_as_set
|
||||
# Always add local AS to RTC AS set
|
||||
curr_rtc_as_set.add(self._core_service.asn)
|
||||
removed_rtc_as_set = self._all_rtc_as_set - curr_rtc_as_set
|
||||
new_rtc_as_set = curr_rtc_as_set - self._all_rtc_as_set
|
||||
|
||||
# Update to new RTC_AS set
|
||||
self._all_rtc_as_set = curr_rtc_as_set
|
||||
|
||||
# Sync RT NLRI by adding/withdrawing as appropriate
|
||||
for new_rtc_as in new_rtc_as_set:
|
||||
for import_rt in self._all_vrfs_import_rts_set:
|
||||
self._add_rt_nlri_for_as(new_rtc_as, import_rt)
|
||||
for removed_rtc_as in removed_rtc_as_set:
|
||||
for import_rt in self._all_vrfs_import_rts_set:
|
||||
self._add_rt_nlri_for_as(removed_rtc_as, import_rt,
|
||||
is_withdraw=True)
|
||||
|
||||
def update_local_rt_nlris(self):
|
||||
"""Does book-keeping of local RT NLRIs based on all configured VRFs.
|
||||
|
||||
Syncs all import RTs and RT NLRIs.
|
||||
The method should be called when any VRFs are added/removed/changed.
|
||||
"""
|
||||
current_conf_import_rts = set()
|
||||
for vrf in self._vrfs_conf.vrf_confs:
|
||||
current_conf_import_rts.update(vrf.import_rts)
|
||||
|
||||
removed_rts = self._all_vrfs_import_rts_set - current_conf_import_rts
|
||||
new_rts = current_conf_import_rts - self._all_vrfs_import_rts_set
|
||||
self._all_vrfs_import_rts_set = current_conf_import_rts
|
||||
|
||||
# Add new and withdraw removed local RtNlris
|
||||
for new_rt in new_rts:
|
||||
self.add_rt_nlri(new_rt)
|
||||
for removed_rt in removed_rts:
|
||||
self.add_rt_nlri(removed_rt, is_withdraw=True)
|
||||
|
||||
def on_rt_filter_chg_sync_peer(self, peer, new_rts, old_rts, table):
|
||||
LOG.debug('RT Filter changed for peer %s, new_rts %s, old_rts %s ' %
|
||||
(peer, new_rts, old_rts))
|
||||
for dest in table.itervalues():
|
||||
# If this destination does not have best path, we ignore it
|
||||
if not dest.best_path:
|
||||
continue
|
||||
|
||||
desired_rts = set(dest.best_path.get_rts())
|
||||
|
||||
# If this path was sent to peer and if all path RTs are now not of
|
||||
# interest, we need to send withdraw for this path to this peer
|
||||
if dest.was_sent_to(peer):
|
||||
if not (desired_rts - old_rts):
|
||||
dest.withdraw_if_sent_to(peer)
|
||||
else:
|
||||
# New RT could be Default RT, which means we need to share this
|
||||
# path
|
||||
desired_rts.add(RtNlri.DEFAULT_RT)
|
||||
# If we have RT filter has new RTs that are common with path
|
||||
# RTs, then we send this path to peer
|
||||
if desired_rts.intersection(new_rts):
|
||||
peer.communicate_path(dest.best_path)
|
||||
|
||||
def _compute_global_intrested_rts(self):
|
||||
"""Computes current global interested RTs for global tables.
|
||||
|
||||
Computes interested RTs based on current RT filters for peers. This
|
||||
filter should be used to check if for RTs on a path that is installed
|
||||
in any global table (expect RT Table).
|
||||
"""
|
||||
interested_rts = set()
|
||||
for rtfilter in self._peer_to_rtfilter_map.values():
|
||||
interested_rts.update(rtfilter)
|
||||
|
||||
interested_rts.update(self._vrfs_conf.vrf_interested_rts)
|
||||
# Remove default RT as it is not a valid RT for paths
|
||||
# TODO(PH): Check if we have better alternative than add and remove
|
||||
interested_rts.add(RtNlri.DEFAULT_RT)
|
||||
interested_rts.remove(RtNlri.DEFAULT_RT)
|
||||
return interested_rts
|
||||
|
||||
def update_interested_rts(self):
|
||||
"""Updates interested RT list.
|
||||
|
||||
Check if interested RTs have changes from previous check.
|
||||
Takes appropriate action for new interesting RTs and removal of un-
|
||||
interesting RTs.
|
||||
"""
|
||||
prev_global_rts = self._global_interested_rts
|
||||
curr_global_rts = self._compute_global_intrested_rts()
|
||||
|
||||
new_global_rts = curr_global_rts - prev_global_rts
|
||||
removed_global_rts = prev_global_rts - curr_global_rts
|
||||
|
||||
# Update current interested RTs for next iteration
|
||||
self._global_interested_rts = curr_global_rts
|
||||
|
||||
LOG.debug('Global Interested RT changed, new RTs %s, removed RTs %s' %
|
||||
(new_global_rts, removed_global_rts))
|
||||
tm = self._core_service.table_manager
|
||||
tm.on_interesting_rts_change(new_global_rts, removed_global_rts)
|
||||
|
||||
def filter_by_origin_as(self, new_best_path, qualified_peers):
|
||||
path_rf = new_best_path.route_family
|
||||
|
||||
# We need not filter any peer if this is not a RT NLRI path or if
|
||||
# source of this path is remote peer (i.e. if this is not a local path)
|
||||
if path_rf != RF_RTC_UC or new_best_path.source is not None:
|
||||
return qualified_peers
|
||||
else:
|
||||
filtered_qualified_peers = []
|
||||
rt_origin_as = new_best_path.nlri.origin_as
|
||||
# Collect peers whose RTC_AS setting match paths RT Origin AS
|
||||
for qualified_peer in qualified_peers:
|
||||
neigh_conf = \
|
||||
self._neighbors_conf.get_neighbor_conf(
|
||||
qualified_peer.ip_address)
|
||||
if neigh_conf.rtc_as == rt_origin_as:
|
||||
filtered_qualified_peers.append(qualified_peer)
|
||||
|
||||
# Update qualified peers to include only filtered peers
|
||||
return filtered_qualified_peers
|
||||
100
ryu/services/protocols/bgp/utils/stats.py
Normal file
100
ryu/services/protocols/bgp/utils/stats.py
Normal file
@ -0,0 +1,100 @@
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Module for stats related classes and utilities.
|
||||
"""
|
||||
import datetime
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
|
||||
from ryu.services.protocols.bgp.rtconf.base import ConfWithId
|
||||
|
||||
|
||||
_STATS_LOGGER = logging.getLogger('stats')
|
||||
|
||||
# Various stats related constants.
|
||||
DEFAULT_LOG_LEVEL = logging.INFO
|
||||
|
||||
RESOURCE_ID = 'resource_id'
|
||||
RESOURCE_NAME = 'resource_name'
|
||||
TIMESTAMP = 'timestamp'
|
||||
LOG_LEVEL = 'log_level'
|
||||
|
||||
STATS_RESOURCE = 'stats_resource'
|
||||
STATS_SOURCE = 'stats_source'
|
||||
|
||||
# VRF related stat constants
|
||||
REMOTE_ROUTES = 'remote_routes'
|
||||
LOCAL_ROUTES = 'local_routes'
|
||||
|
||||
# Peer related stat constant.
|
||||
UPDATE_MSG_IN = 'update_message_in'
|
||||
UPDATE_MSG_OUT = 'update_message_out'
|
||||
TOTAL_MSG_IN = 'total_message_in'
|
||||
TOTAL_MSG_OUT = 'total_message_out'
|
||||
FMS_EST_TRANS = 'fsm_established_transitions'
|
||||
UPTIME = 'uptime'
|
||||
|
||||
|
||||
def log(stats_resource=None, stats_source=None, log_level=DEFAULT_LOG_LEVEL,
|
||||
**kwargs):
|
||||
"""Utility to log given stats to *stats* logger.
|
||||
|
||||
Stats to log are given by `stats_source` and in its absence we log
|
||||
`kwargs`. *stats* logger is configured independently from any logger.
|
||||
Only stats should be logged to this logger. Will add current timestamp
|
||||
to the logged stats if not given.
|
||||
|
||||
Parameters:
|
||||
- `stats_resource`: any object that complies with `id` and `name`
|
||||
attrs.
|
||||
- `stats_source`: any callable that give a `dict` that will be
|
||||
logged to *stats* logger.
|
||||
- `log_level`: str representing level at which to log this stats
|
||||
message.
|
||||
- `**kwargs`: if `stats_source` is not given, we log this `dict`.
|
||||
"""
|
||||
|
||||
# Get stats from source if given.
|
||||
if stats_source is not None:
|
||||
kwargs = stats_source()
|
||||
|
||||
if stats_resource is None:
|
||||
if RESOURCE_ID not in kwargs or RESOURCE_NAME not in kwargs:
|
||||
raise ValueError('Missing required stats labels.')
|
||||
else:
|
||||
if not (hasattr(stats_resource, ConfWithId.ID) and
|
||||
hasattr(stats_resource, ConfWithId.NAME)):
|
||||
raise ValueError('Given stats source is missing id or name'
|
||||
' attributes.')
|
||||
kwargs[RESOURCE_ID] = stats_resource.id
|
||||
kwargs[RESOURCE_NAME] = stats_resource.name
|
||||
|
||||
if TIMESTAMP not in kwargs:
|
||||
kwargs[TIMESTAMP] = datetime.datetime.utcfromtimestamp(
|
||||
time.time()).strftime("%Y-%m-%dT%H:%M:%S.%fZ")
|
||||
|
||||
_STATS_LOGGER.log(log_level,
|
||||
json.dumps(kwargs))
|
||||
|
||||
|
||||
def logd(**kwargs):
|
||||
log(log_level=logging.DEBUG, **kwargs)
|
||||
|
||||
|
||||
def logi(**kwargs):
|
||||
log(log_level=logging.INFO, **kwargs)
|
||||
234
ryu/services/protocols/bgp/utils/validation.py
Normal file
234
ryu/services/protocols/bgp/utils/validation.py
Normal file
@ -0,0 +1,234 @@
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Module provides utilities for validation.
|
||||
"""
|
||||
import socket
|
||||
|
||||
|
||||
def is_valid_ipv4(ipv4):
|
||||
"""Returns True if given is a valid ipv4 address.
|
||||
|
||||
Given value should be a dot-decimal notation string.
|
||||
"""
|
||||
valid = True
|
||||
|
||||
if not isinstance(ipv4, str):
|
||||
valid = False
|
||||
else:
|
||||
try:
|
||||
a, b, c, d = map(lambda x: int(x), ipv4.split('.'))
|
||||
if (a < 0 or a > 255 or b < 0 or b > 255 or c < 0 or c > 255 or
|
||||
d < 0 or d > 255):
|
||||
valid = False
|
||||
except ValueError:
|
||||
valid = False
|
||||
|
||||
return valid
|
||||
|
||||
|
||||
def is_valid_ipv4_prefix(ipv4_prefix):
|
||||
"""Returns True if *ipv4_prefix* is a valid prefix with mask.
|
||||
|
||||
Samples:
|
||||
- valid prefix: 1.1.1.0/32, 244.244.244.1/10
|
||||
- invalid prefix: 255.2.2.2/2, 2.2.2/22, etc.
|
||||
"""
|
||||
if not isinstance(ipv4_prefix, str):
|
||||
return False
|
||||
|
||||
valid = True
|
||||
tokens = ipv4_prefix.split('/')
|
||||
if len(tokens) != 2:
|
||||
valid = False
|
||||
else:
|
||||
if not is_valid_ipv4(tokens[0]):
|
||||
valid = False
|
||||
else:
|
||||
# Validate mask
|
||||
try:
|
||||
# Mask is a number
|
||||
mask = int(tokens[1])
|
||||
# Mask is number between 0 to 32
|
||||
if mask < 0 or mask > 32:
|
||||
valid = False
|
||||
except ValueError:
|
||||
valid = False
|
||||
|
||||
return valid
|
||||
|
||||
|
||||
def is_valid_ipv6(ipv6):
|
||||
"""Returns True if given `ipv6` is a valid IPv6 address
|
||||
|
||||
Uses `socket.inet_pton` to determine validity.
|
||||
"""
|
||||
valid = True
|
||||
try:
|
||||
socket.inet_pton(socket.AF_INET6, ipv6)
|
||||
except socket.error:
|
||||
valid = False
|
||||
|
||||
return valid
|
||||
|
||||
|
||||
def is_valid_ipv6_prefix(ipv6_prefix):
|
||||
"""Returns True if given `ipv6_prefix` is a valid IPv6 prefix."""
|
||||
|
||||
# Validate input type
|
||||
if not isinstance(ipv6_prefix, str):
|
||||
return False
|
||||
|
||||
valid = True
|
||||
tokens = ipv6_prefix.split('/')
|
||||
if len(tokens) != 2:
|
||||
valid = False
|
||||
else:
|
||||
if not is_valid_ipv6(tokens[0]):
|
||||
valid = False
|
||||
else:
|
||||
# Validate mask
|
||||
try:
|
||||
# Mask is a number
|
||||
mask = int(tokens[1])
|
||||
# Mask is number between 0 to 128
|
||||
if mask < 0 or mask > 128:
|
||||
valid = False
|
||||
except ValueError:
|
||||
valid = False
|
||||
|
||||
return valid
|
||||
|
||||
|
||||
def is_valid_old_asn(asn):
|
||||
"""Returns true if given asn is a 16 bit number.
|
||||
|
||||
Old AS numbers are 16 but unsigned number.
|
||||
"""
|
||||
valid = True
|
||||
# AS number should be a 16 bit number
|
||||
if (not isinstance(asn, (int, long)) or (asn < 0) or
|
||||
(asn > ((2 ** 16) - 1))):
|
||||
valid = False
|
||||
|
||||
return valid
|
||||
|
||||
|
||||
def is_valid_vpnv4_prefix(prefix):
|
||||
"""Returns True if given prefix is a string represent vpnv4 prefix.
|
||||
|
||||
Vpnv4 prefix is made up of RD:Ipv4, where RD is represents route
|
||||
distinguisher and Ipv4 represents valid dot-decimal ipv4 notation string.
|
||||
"""
|
||||
valid = True
|
||||
|
||||
if not isinstance(prefix, str):
|
||||
valid = False
|
||||
else:
|
||||
# Split the prefix into route distinguisher and IP
|
||||
tokens = prefix.split(':')
|
||||
if len(tokens) != 3:
|
||||
valid = False
|
||||
else:
|
||||
# Check if first two tokens can form a valid RD
|
||||
try:
|
||||
# admin_subfield
|
||||
int(tokens[0])
|
||||
# assigned_subfield
|
||||
int(tokens[1])
|
||||
except ValueError:
|
||||
valid = False
|
||||
|
||||
# Check if ip part is valid
|
||||
valid = is_valid_ipv4_prefix(tokens[2])
|
||||
|
||||
return valid
|
||||
|
||||
|
||||
def is_valid_med(med):
|
||||
"""Returns True if value of *med* is valid as per RFC.
|
||||
|
||||
According to RFC MED is a four octet non-negative integer.
|
||||
"""
|
||||
valid = True
|
||||
|
||||
if not isinstance(med, (int, long)):
|
||||
valid = False
|
||||
else:
|
||||
if med < 0 or med > (2 ** 32) - 1:
|
||||
valid = False
|
||||
|
||||
return valid
|
||||
|
||||
|
||||
def is_valid_mpls_label(label):
|
||||
"""Validates `label` according to MPLS label rules
|
||||
|
||||
RFC says:
|
||||
This 20-bit field.
|
||||
A value of 0 represents the "IPv4 Explicit NULL Label".
|
||||
A value of 1 represents the "Router Alert Label".
|
||||
A value of 2 represents the "IPv6 Explicit NULL Label".
|
||||
A value of 3 represents the "Implicit NULL Label".
|
||||
Values 4-15 are reserved.
|
||||
"""
|
||||
valid = True
|
||||
|
||||
if (not isinstance(label, (int, long)) or
|
||||
(label >= 4 and label <= 15) or
|
||||
(label < 0 or label > 2 ** 20)):
|
||||
valid = False
|
||||
|
||||
return valid
|
||||
|
||||
|
||||
def is_valid_route_disc(route_disc):
|
||||
"""Validates *route_disc* as string representation of route distinguisher.
|
||||
|
||||
Returns True if *route_disc* is as per our convention of RD, else False.
|
||||
Our convention is to represent RD as a string in format:
|
||||
*admin_sub_field:assigned_num_field* and *admin_sub_field* can be valid
|
||||
IPv4 string representation.
|
||||
Valid examples: '65000:222', '1.2.3.4:4432'.
|
||||
Invalid examples: '1.11.1: 333'
|
||||
"""
|
||||
# TODO(PH): Provide complete implementation.
|
||||
return is_valid_ext_comm_attr(route_disc)
|
||||
|
||||
|
||||
def is_valid_ext_comm_attr(attr):
|
||||
"""Validates *attr* as string representation of RT or SOO.
|
||||
|
||||
Returns True if *attr* is as per our convention of RT or SOO, else
|
||||
False. Our convention is to represent RT/SOO is a string with format:
|
||||
*global_admin_part:local_admin_path*
|
||||
"""
|
||||
is_valid = True
|
||||
|
||||
if not isinstance(attr, str):
|
||||
is_valid = False
|
||||
else:
|
||||
first, second = attr.split(':')
|
||||
try:
|
||||
if '.' in first:
|
||||
socket.inet_aton(first)
|
||||
else:
|
||||
int(first)
|
||||
int(second)
|
||||
except (ValueError, socket.error):
|
||||
is_valid = False
|
||||
|
||||
return is_valid
|
||||
Loading…
x
Reference in New Issue
Block a user