├── doc └── README.md ├── yabmp ├── core │ ├── __init__.py │ ├── factory.py │ └── protocol.py ├── tests │ └── __init__.py ├── channel │ ├── __init__.py │ ├── config.py │ ├── factory.py │ ├── protocol.py │ ├── publisher.py │ └── consumer.py ├── common │ ├── __init__.py │ ├── exception.py │ └── constants.py ├── message │ ├── __init__.py │ └── bmp.py ├── cmd │ └── __init__.py ├── __init__.py ├── config.py ├── handler │ ├── __init__.py │ └── default.py ├── service.py ├── hooks.py └── log.py ├── test-requirements.txt ├── .travis.yml ├── .mailmap ├── start.sh ├── requirements.txt ├── Dockerfile ├── etc └── yabmp │ └── yabmp.ini.sample ├── setup.cfg ├── setup.py ├── .gitignore ├── tox.ini ├── bin └── yabmpd ├── README.rst ├── example └── mybmpd.py └── LICENSE /doc/README.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /yabmp/core/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /yabmp/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /yabmp/channel/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /yabmp/common/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /yabmp/message/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test-requirements.txt: -------------------------------------------------------------------------------- 1 | nose 2 | flake8 -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | install: 3 | - pip install tox 4 | script: 5 | - tox 6 | env: 7 | - TOXENV=py27 8 | - TOXENV=flake8 -------------------------------------------------------------------------------- /.mailmap: -------------------------------------------------------------------------------- 1 | Format is: 2 | # 3 | # 4 | Peng Xiao 5 | -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | exec bin/yabmpd --bind_port=${BMP_BIND_PORT} --log-file=/root/data/bmp/${BMP_BIND_PORT}/log/yabmp.log 4 | --message-write_dir=/root/data/bmp/${BMP_BIND_PORT}/msg -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | #yabgp 2 | https://github.com/smartbgp/yabgp/archive/master.zip 3 | netaddr>=0.7.12 4 | pbr==2.0.0 5 | oslo.config==2.1.0 6 | Twisted==16.0.0 7 | bitstring==3.1.5 8 | six==1.10.0 9 | pika==0.9.14 -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:2.7.14-alpine 2 | 3 | LABEL maintainer="lupzhu " 4 | 5 | RUN apk add --no-cache gcc musl-dev g++ 6 | 7 | ENV BMP_BIND_PORT=20000 8 | 9 | ADD ./ /yabmp 10 | 11 | WORKDIR /yabmp 12 | 13 | RUN pip install -r requirements.txt && chmod +x bin/yabmpd && chmod +x start.sh 14 | 15 | EXPOSE 20000 16 | 17 | VOLUME ["~/data"] 18 | 19 | ENTRYPOINT ["./start.sh"] 20 | -------------------------------------------------------------------------------- /etc/yabmp/yabmp.ini.sample: -------------------------------------------------------------------------------- 1 | [DEFAULT] 2 | 3 | # log file name and location 4 | # log-file = 5 | 6 | # show debug output 7 | # verbose = False 8 | 9 | # log to standard error 10 | # use-stderr = True 11 | 12 | # log file directory 13 | # log-dir 14 | 15 | # log configuration file 16 | # log-config-file = 17 | 18 | [bmp] 19 | 20 | # BMP server bind host ip address 21 | # bind_host = 0.0.0.0 22 | 23 | # BMP server bind port number 24 | # bind_port = 20000 25 | 26 | # the BMP messages storage path 27 | # write_dir = ~/data/bmp/ 28 | 29 | # The Max size of one BGP message file, the unit is MB 30 | # write_msg_max_size = 500 -------------------------------------------------------------------------------- /yabmp/cmd/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015 Cisco Systems, Inc. 2 | # All rights reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | from yabmp.service import prepare_service 17 | 18 | 19 | def main(): 20 | prepare_service() 21 | -------------------------------------------------------------------------------- /yabmp/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015 Cisco Systems, Inc. 2 | # All rights reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | """version information""" 17 | 18 | version_info = (0, 2, 0) 19 | version = '.'.join(map(str, version_info)) 20 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = yabmp 3 | summary = A Python BMP Receiver 4 | license = Apache License 2.0 5 | author = SmartBGP project team 6 | author-email = penxiao@cisco.com 7 | home-page = http://smartbgp.github.io/ 8 | description-file = README.rst 9 | platform = any 10 | classifier = 11 | Development Status :: 5 - Production/Stable 12 | License :: OSI Approved :: Apache Software License 13 | Topic :: System :: Networking 14 | Natural Language :: English 15 | Programming Language :: Python 16 | Operating System :: Unix 17 | keywords = 18 | BGP 19 | SDN 20 | 21 | [global] 22 | setup-hooks = 23 | yabmp.hooks.setup_hook 24 | 25 | [files] 26 | packages = 27 | yabmp 28 | data_files = 29 | etc/yabmp/yabmp.ini = 30 | etc/yabmp/yabmp.ini.sample 31 | [entry_points] 32 | console_scripts = 33 | yabmpd = yabmp.server:main -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015 Cisco Systems, Inc. 2 | # All rights reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | import setuptools 17 | 18 | import yabmp.hooks 19 | 20 | yabmp.hooks.save_orig() 21 | 22 | setuptools.setup( 23 | name='yabmp', 24 | setup_requires=['pbr'], 25 | pbr=True) 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | 46 | # Translations 47 | *.mo 48 | *.pot 49 | 50 | # Django stuff: 51 | *.log 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | # PyBuilder 57 | target/ 58 | 59 | /.idea 60 | etc/openbmp/openbmp.ini 61 | .DS_Store 62 | -------------------------------------------------------------------------------- /yabmp/channel/config.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015 Cisco Systems, Inc. 2 | # All rights reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | """ message queue config """ 17 | 18 | import os 19 | 20 | from oslo_config import cfg 21 | 22 | CONF = cfg.CONF 23 | 24 | rabbit_mq = [ 25 | cfg.StrOpt('rabbit_url', 26 | default=os.environ.get('RABBITMQ_URL', 'amqp://guest:guest@localhost:5672/%2F'), 27 | help='The RabbitMQ connection url') 28 | ] 29 | -------------------------------------------------------------------------------- /yabmp/config.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015 Cisco Systems, Inc. 2 | # All rights reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | """ basic config """ 17 | 18 | from oslo_config import cfg 19 | 20 | CONF = cfg.CONF 21 | 22 | bmp_options = [ 23 | 24 | cfg.StrOpt('bind_host', 25 | default='0.0.0.0', 26 | help='Address to bind the BMP server to'), 27 | cfg.IntOpt('bind_port', 28 | default=20000, 29 | help='Port the bind the BMP server to') 30 | ] 31 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # Tox (http://tox.testrun.org/) is a tool for running tests 2 | # in multiple virtualenvs. This configuration file will run the 3 | # test suite on all supported python versions. To use it, "pip install tox" 4 | # and then run "tox" from this directory. 5 | 6 | [tox] 7 | # List the environment that will be run by default 8 | minversion = 1.6 9 | envlist = py27,pep8 10 | skipsdist = True 11 | 12 | [testenv] 13 | deps = -r{toxinidir}/requirements.txt 14 | -r{toxinidir}/test-requirements.txt 15 | commands = nosetests 16 | 17 | [testenv:flake8] 18 | sitepackages = False 19 | commands = 20 | flake8 {posargs} 21 | 22 | [testenv:pep8] 23 | sitepackages = False 24 | commands = 25 | flake8 {posargs} 26 | 27 | [flake8] 28 | # E712 is ignored on purpose, since it is normal to use 'column == true' 29 | # in sqlalchemy. 30 | # H803 skipped on purpose per list discussion. 31 | # E125 is deliberately excluded. See https://github.com/jcrocholl/pep8/issues/126 32 | # The rest of the ignores are TODOs 33 | # New from hacking 0.9: E129, E131, E265, E713, H407, H405, H904 34 | # Stricter in hacking 0.9: F402 35 | # E251 Skipped due to https://github.com/jcrocholl/pep8/issues/301 36 | 37 | max-line-length=120 38 | exclude = .venv,.git,.tox,dist,doc,*lib/python*,*egg,build,tools -------------------------------------------------------------------------------- /yabmp/core/factory.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015 Cisco Systems, Inc. 2 | # All rights reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | """ BMP Factory""" 17 | 18 | import logging 19 | 20 | from twisted.internet import protocol 21 | 22 | from yabmp.core.protocol import BMP 23 | 24 | 25 | LOG = logging.getLogger() 26 | 27 | 28 | class BMPFactory(protocol.Factory): 29 | """Base factory for creating BMP protocol instances.""" 30 | 31 | protocol = BMP 32 | 33 | def __init__(self, handler): 34 | LOG.info('Initial BMP Factory!') 35 | self.handler = handler 36 | 37 | def buildProtocol(self, addr): 38 | """Builds a BMPProtocol instance. 39 | """ 40 | proto = protocol.Factory.buildProtocol(self, addr) 41 | return proto 42 | -------------------------------------------------------------------------------- /bin/yabmpd: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | 4 | # Copyright 2015 Cisco Systems, Inc. 5 | # All rights reserved. 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 8 | # not use this file except in compliance with the License. You may obtain 9 | # a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 15 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 16 | # License for the specific language governing permissions and limitations 17 | # under the License. 18 | 19 | """ BMP daemon """ 20 | 21 | import os 22 | import sys 23 | 24 | possible_topdir = os.path.normpath(os.path.join(os.path.abspath(__file__), 25 | os.pardir, 26 | os.pardir)) 27 | if os.path.exists(os.path.join(possible_topdir, 28 | 'yabmp', 29 | '__init__.py')): 30 | sys.path.insert(0, possible_topdir) 31 | 32 | else: 33 | possible_topdir = '/' 34 | 35 | from yabmp.cmd import main 36 | 37 | 38 | if __name__ == '__main__': 39 | sys.exit(main()) 40 | -------------------------------------------------------------------------------- /yabmp/handler/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015-2017 Cisco Systems, Inc. 2 | # All rights reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | import abc 17 | import logging 18 | 19 | import six 20 | 21 | LOG = logging.getLogger(__name__) 22 | 23 | 24 | @six.add_metaclass(abc.ABCMeta) 25 | class BaseHandler(object): 26 | """basic yabmp handler 27 | """ 28 | 29 | def __init__(self): 30 | pass 31 | 32 | @abc.abstractmethod 33 | def init(self): 34 | """init some configuration 35 | """ 36 | raise NotImplementedError() 37 | 38 | @abc.abstractmethod 39 | def on_connection_made(self, peer_host, peer_port): 40 | """process for connection made 41 | """ 42 | raise NotImplementedError() 43 | 44 | @abc.abstractmethod 45 | def on_connection_lost(self, peer_host, peer_port): 46 | """process for connection lost 47 | """ 48 | raise NotImplementedError() 49 | 50 | @abc.abstractmethod 51 | def on_message_received(self, msg, msg_type): 52 | """process for message received 53 | """ 54 | raise NotImplementedError() 55 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | YABMP 2 | ===== 3 | 4 | |Python Version| |Version| |License| |Build Status| |Code Climate| 5 | 6 | Overview 7 | ~~~~~~~~ 8 | 9 | `YABMP` is a receiver-side implementation of the `BMP` (BGP Monitoring Protocol) in the Python language. It serves as a reference for how to step through the messages and write their contents to files. 10 | 11 | This implementation covers RFC 7854 BGP Monitoring Protocol version 3. 12 | 13 | RFCs to read to help you understand the code better: 14 | 15 | * RFC1863 - A BGP/IDRP Route Server alternative to a full mesh routing 16 | * RFC1997 - BGP Communities Attribute 17 | * RFC2042 - Registering New BGP Attribute Types 18 | * RFC2858 - Multiprotocol Extensions for BGP-4 19 | * RFC4271 - A Border Gateway Protocol 4 (BGP-4) 20 | * RFC4893 - BGP Support for Four-octet AS Number Space 21 | * Other BGP related RFCs. 22 | 23 | Quick Start 24 | ~~~~~~~~~~~ 25 | 26 | Use `pip install yabmp` or install from source. 27 | 28 | .. code:: bash 29 | 30 | $ virtualenv yabmp-virl 31 | $ source yabmp-virl/bin/activate 32 | $ git clone https://github.com/smartbgp/yabmp 33 | $ cd yabmp 34 | $ pip install -r requirements.txt 35 | $ cd bin 36 | $ python yabmpd -h 37 | 38 | 39 | .. code:: bash 40 | 41 | $ python yabmpd & 42 | 43 | Will starting bmpd server listen to port = 20000 and ip = 0.0.0.0 44 | 45 | Support 46 | ~~~~~~~ 47 | 48 | Send email to xiaoquwl@gmail.com, or use GitHub issue system/pull request. 49 | 50 | 51 | .. |License| image:: https://img.shields.io/hexpm/l/plug.svg 52 | :target: https://github.com/smartbgp/yabmp/blob/master/LICENSE 53 | .. |Build Status| image:: https://travis-ci.org/smartbgp/yabmp.svg 54 | :target: https://travis-ci.org/smartbgp/yabmp 55 | .. |Code Climate| image:: https://codeclimate.com/github/smartbgp/yabmp/badges/gpa.svg 56 | :target: https://codeclimate.com/github/smartbgp/yabmp 57 | .. |Python Version| image:: https://img.shields.io/pypi/pyversions/Django.svg 58 | :target: https://github.com/smartbgp/yabbmp 59 | .. |Version| image:: https://img.shields.io/pypi/v/yabmp.svg? 60 | :target: http://badge.fury.io/py/yabmp -------------------------------------------------------------------------------- /yabmp/common/exception.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015 Cisco Systems, Inc. 2 | # All rights reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | """ 17 | BMP base exception handling. 18 | """ 19 | 20 | _FATAL_EXCEPTION_FORMAT_ERRORS = False 21 | 22 | 23 | class BMPException(Exception): 24 | """Base BMP Exception. 25 | """ 26 | message = "An unknown exception occurred." 27 | 28 | def __init__(self, **kwargs): 29 | try: 30 | super(BMPException, self).__init__(self.message % kwargs) 31 | self.msg = self.message % kwargs 32 | except Exception: 33 | if _FATAL_EXCEPTION_FORMAT_ERRORS: 34 | raise 35 | else: 36 | # at least get the core message out if something happened 37 | super(BMPException, self).__init__(self.message) 38 | 39 | def __unicode__(self): 40 | return self.msg.decode('utf8', 'ignore') 41 | # return unicode(self.msg) 42 | 43 | 44 | class BMPVersionError(BMPException): 45 | message = 'Bad BMP version, local version:%(local_version)s, remote version:%(remote_version)s' 46 | 47 | 48 | class UnknownMessageTypeError(BMPException): 49 | message = 'unknown message type, type=%(type)s' 50 | 51 | 52 | class BadMessageHeaderLength(BMPException): 53 | message = 'Bad message header length' 54 | 55 | 56 | class UnknownPeerTypeValue(BMPException): 57 | message = 'unknown peer type value, value=%(peer_type)s' 58 | 59 | 60 | class UnknownPeerFlagValue(BMPException): 61 | message = 'unknown peer flag value, value=%(peer_flags)s' 62 | -------------------------------------------------------------------------------- /yabmp/service.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015 Cisco Systems, Inc. 2 | # All rights reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | import logging 17 | 18 | from yabmp import config 19 | from yabmp import version 20 | from yabmp import log 21 | from yabmp.core.factory import BMPFactory 22 | from yabmp.handler.default import DefaultHandler 23 | 24 | from twisted.internet import reactor 25 | from oslo_config import cfg 26 | 27 | log.early_init_log(logging.DEBUG) 28 | 29 | CONF = cfg.CONF 30 | 31 | CONF.register_cli_opts(config.bmp_options) 32 | 33 | LOG = logging.getLogger(__name__) 34 | 35 | 36 | def prepare_service(args=None, handler=None): 37 | """prepare the twisted service 38 | 39 | :param hander: handler object 40 | """ 41 | if not handler: 42 | handler = DefaultHandler() 43 | try: 44 | CONF(args=args, project='yabmp', version=version, 45 | default_config_files=['/etc/yabmp/yabmp.ini']) 46 | except cfg.ConfigFilesNotFoundError: 47 | CONF(args=args, project='yabmp', version=version) 48 | 49 | log.init_log() 50 | LOG.info('Log (Re)opened.') 51 | LOG.info("Configuration:") 52 | 53 | cfg.CONF.log_opt_values(LOG, logging.INFO) 54 | 55 | handler.init() 56 | # start bmp server 57 | try: 58 | reactor.listenTCP( 59 | CONF.bind_port, 60 | BMPFactory(handler=handler), 61 | interface=CONF.bind_host) 62 | LOG.info( 63 | "Starting bmpd server listen to port = %s and ip = %s", 64 | CONF.bind_port, CONF.bind_host) 65 | reactor.run() 66 | except Exception as e: 67 | LOG.error(e) 68 | 69 | 70 | def main(): 71 | prepare_service(args=None, handler=None) 72 | -------------------------------------------------------------------------------- /yabmp/channel/factory.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015-2016 Cisco Systems, Inc. 2 | # All rights reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | """Twisted message queue factory 17 | reference from https://github.com/pika/pika/blob/master/examples/twisted_service.py 18 | """ 19 | 20 | import logging 21 | 22 | import pika 23 | from twisted.internet import protocol 24 | from twisted.internet import reactor 25 | 26 | from .protocol import PikaProtocol 27 | 28 | LOG = logging.getLogger(__name__) 29 | 30 | 31 | class PikaFactory(protocol.ReconnectingClientFactory): 32 | 33 | def __init__(self, url, routing_key): 34 | self.parameters = pika.URLParameters(url) 35 | self.client = None 36 | self.queued_messages = [] 37 | self.routing_key = routing_key 38 | 39 | def startedConnecting(self, connector): 40 | LOG.info('Started to connect to AMQP') 41 | 42 | def buildProtocol(self, addr): 43 | self.resetDelay() 44 | LOG.info('Connected AMQP') 45 | self.client = PikaProtocol(self.parameters) 46 | self.client.factory = self 47 | self.client.ready.addCallback(self.client.connected) 48 | return self.client 49 | 50 | def clientConnectionLost(self, connector, reason): 51 | LOG.info('Lost connection. Reason: %s', reason.getErrorMessage()) 52 | protocol.ReconnectingClientFactory.clientConnectionLost(self, connector, reason.getErrorMessage()) 53 | 54 | def clientConnectionFailed(self, connector, reason): 55 | LOG.info('Connection failed. Reason: %s', reason.getErrorMessage()) 56 | protocol.ReconnectingClientFactory.clientConnectionFailed(self, connector, reason.getErrorMessage()) 57 | 58 | def send_message(self, exchange=None, routing_key=None, message=None): 59 | if not routing_key: 60 | routing_key = self.routing_key 61 | self.queued_messages.append((exchange, routing_key, message)) 62 | if self.client is not None: 63 | self.client.send() 64 | 65 | def connect(self): 66 | 67 | try: 68 | reactor.connectTCP( 69 | host=self.parameters.host, 70 | port=self.parameters.port, 71 | factory=self) 72 | except Exception as e: 73 | LOG.error(e) 74 | -------------------------------------------------------------------------------- /yabmp/hooks.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015 Cisco Systems, Inc. 2 | # All rights reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | # copy from # get from https://github.com/osrg/ryu/blob/master/ryu/hooks.py 17 | import sys 18 | from setuptools.command import easy_install 19 | 20 | from yabmp import version 21 | 22 | 23 | # Global variables in this module doesn't work as we expect 24 | # because, during the setup procedure, this module seems to be 25 | # copied (as a file) and can be loaded multiple times. 26 | # We save them into __main__ module instead. 27 | def _main_module(): 28 | return sys.modules['__main__'] 29 | 30 | 31 | def save_orig(): 32 | """Save original easy_install.get_script_args. 33 | This is necessary because pbr's setup_hook is sometimes called 34 | before ours.""" 35 | _main_module()._orig_get_script_args = easy_install.get_script_args 36 | 37 | 38 | def setup_hook(config): 39 | """Filter config parsed from a setup.cfg to inject our defaults.""" 40 | metadata = config['metadata'] 41 | requires = metadata.get('requires_dist', '').split('\n') 42 | if sys.platform == 'win32': 43 | requires.append('pywin32') 44 | requires.append('wmi') 45 | metadata['requires_dist'] = "\n".join(requires) 46 | config['metadata'] = metadata 47 | metadata['version'] = str(version) 48 | 49 | # pbr's setup_hook replaces easy_install.get_script_args with 50 | # their own version, override_get_script_args, prefering simpler 51 | # scripts which are not aware of multi-version. 52 | # prevent that by doing the opposite. it's a horrible hack 53 | # but we are in patching wars already... 54 | from pbr import packaging 55 | 56 | def my_get_script_args(*args, **kwargs): 57 | return _main_module()._orig_get_script_args(*args, **kwargs) 58 | 59 | packaging.override_get_script_args = my_get_script_args 60 | easy_install.get_script_args = my_get_script_args 61 | 62 | # another hack to allow setup from tarball. 63 | orig_get_version = packaging.get_version 64 | 65 | def my_get_version(package_name, pre_version=None): 66 | if package_name == 'yabmp': 67 | return str(version) 68 | return orig_get_version(package_name, pre_version) 69 | 70 | packaging.get_version = my_get_version 71 | -------------------------------------------------------------------------------- /yabmp/channel/protocol.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015-2016 Cisco Systems, Inc. 2 | # All rights reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | """ twisted message queue protocol 17 | reference from https://github.com/pika/pika/blob/master/examples/twisted_service.py 18 | """ 19 | 20 | import logging 21 | import json 22 | 23 | from pika import spec 24 | from pika.adapters import twisted_connection 25 | from twisted.internet.defer import inlineCallbacks 26 | 27 | PREFETCH_COUNT = 2 28 | LOG = logging.getLogger(__name__) 29 | 30 | 31 | class PikaProtocol(twisted_connection.TwistedProtocolConnection): 32 | is_connected = False 33 | name = 'AMQP:Protocol' 34 | factory = None 35 | 36 | @inlineCallbacks 37 | def connected(self, connection): 38 | 39 | self.channel = yield connection.channel() 40 | yield self.channel.basic_qos(prefetch_count=PREFETCH_COUNT) 41 | self.is_connected = True 42 | yield self.setup('', self.factory.routing_key) 43 | 44 | # try to send all waiting messages 45 | LOG.info('connected to rabbitmq') 46 | if self.factory.queued_messages: 47 | LOG.info('try to send all waiting message in queue') 48 | self.send() 49 | 50 | @inlineCallbacks 51 | def setup(self, exchange, routing_key): 52 | """This function does the work to read from an exchange.""" 53 | LOG.info("setup rabbitmq exchange or queue") 54 | if not exchange == '': 55 | yield self.channel.exchange_declare(exchange=exchange, type='topic', durable=True, auto_delete=False) 56 | else: 57 | yield self.channel.queue_declare(queue=routing_key, durable=True, auto_delete=False) 58 | 59 | def send(self): 60 | """If connected, send all waiting messages.""" 61 | if self.is_connected: 62 | while len(self.factory.queued_messages) > 0: 63 | (exchange, r_key, message,) = self.factory.queued_messages.pop(0) 64 | self.send_message(exchange, r_key, message) 65 | 66 | @inlineCallbacks 67 | def send_message(self, exchange, routing_key, msg): 68 | """Send a single message.""" 69 | LOG.debug('%s (%s): %s', exchange, routing_key, repr(msg)) 70 | # yield self.channel.exchange_declare(exchange=exchange, type='topic', durable=True, auto_delete=False) 71 | prop = spec.BasicProperties(delivery_mode=2) 72 | try: 73 | message = json.dumps(msg) 74 | yield self.channel.basic_publish(exchange=exchange, routing_key=routing_key, body=message, properties=prop) 75 | except Exception as error: 76 | LOG.error('Error while sending message: %s', error) 77 | 78 | def connectionLost(self, reason): 79 | 80 | """Called when the associated connection was lost. 81 | 82 | :param reason: the reason of lost connection. 83 | """ 84 | LOG.debug('Called connectionLost') 85 | self.is_connected = False 86 | LOG.info("Connection lost:%s", reason.getErrorMessage()) 87 | -------------------------------------------------------------------------------- /yabmp/common/constants.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015 Cisco Systems, Inc. 2 | # All rights reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | """BMP constants""" 17 | 18 | # The length of the fixed header part of a BMP message. 19 | HEADER_LEN = 6 20 | 21 | # Version of the protocol, as specified in the header. 22 | VERSION = 3 23 | 24 | # Message types. 25 | 26 | MSG_TYPE_ROUTE_MONITORING = 0 27 | MSG_TYPE_STATISTICS_REPORT = 1 28 | MSG_TYPE_PEER_DOWN_NOTIFICATION = 2 29 | MSG_TYPE_PEER_UP_NOTIFICATION = 3 30 | MSG_TYPE_INITIATION = 4 31 | MSG_TYPE_TERMINATION = 5 32 | MSG_TYPE_ROUTE_MIRRORING = 6 33 | MSG_TYPE_STR = { 34 | MSG_TYPE_ROUTE_MONITORING: "Route Monitoring", 35 | MSG_TYPE_STATISTICS_REPORT: "Statistics Report", 36 | MSG_TYPE_PEER_DOWN_NOTIFICATION: "Peer Down Notification", 37 | MSG_TYPE_PEER_UP_NOTIFICATION: "Peer Up Notification", 38 | MSG_TYPE_INITIATION: "Initiation Message", 39 | MSG_TYPE_TERMINATION: "Termination Message", 40 | MSG_TYPE_ROUTE_MIRRORING: "Route Mirroring" 41 | } 42 | 43 | # Peer types. 44 | PEER_TYPE_GLOBAL = 0 45 | PEER_TYPE_RD_INSTANCE = 1 46 | PEER_TYPE_LOCAL = 2 47 | PEER_TYPE_STR = {PEER_TYPE_GLOBAL: "Global", 48 | PEER_TYPE_RD_INSTANCE: "RD Instance", 49 | PEER_TYPE_LOCAL: "Local Instance"} 50 | 51 | PEER_FLAGS = ['V', 'L', 'A'] 52 | 53 | BMP_STAT_TYPE = { 54 | 0: 'Number of prefixes rejected by inbound policy', 55 | 1: 'Number of (known) duplicate prefix advertisements', 56 | 2: 'Number of (known) duplicate withdraws', 57 | 3: 'Number of updates invalidated due to CLUSTER_LIST loop', 58 | 4: 'Number of updates invalidated due to AS_PATH loop', 59 | 5: 'Number of updates invalidated due to ORIGINATOR_ID', 60 | 6: 'Number of updates invalidated due to AS_CONFED loop', 61 | 7: 'Number of routes in Adj-RIBs-In', 62 | 8: 'Number of routes in Loc-RIB', 63 | 9: 'Number of routes in per-AFI/SAFI Adj-RIB-In', 64 | 10: 'Number of routes in per-AFI/SAFI Loc-RIB', 65 | 11: 'Number of updates subjected to treat-as-withdraw', 66 | 12: 'Number of prefixes subjected to treat-as-withdraw', 67 | 13: 'Number of duplicate update messages received', 68 | 32767: 'SRTT', 69 | 32768: 'RTTO', 70 | 32769: 'RTV', 71 | 32770: 'KRTT', 72 | 32771: 'minRTT', 73 | 32772: 'maxRTT', 74 | 32773: 'ACK hold', 75 | 32774: 'Datagrams' 76 | } 77 | 78 | INIT_MSG_INFOR_TYPE = { 79 | 0: 'String', 80 | 1: 'sysDescr', 81 | 2: 'sysName' 82 | } 83 | 84 | TERMI_MSG_INFOR_TYPE = { 85 | 0: 'String', 86 | 1: 'Reason' 87 | } 88 | 89 | TERMI_MSG_INFOR_TYPE_REASON_TYPE = { 90 | 0: 'Session administratively closed', 91 | 1: 'Unspecified reason', 92 | 2: 'Out of resources', 93 | 3: 'Redundant connection', 94 | 4: 'Permanently administratively closed' 95 | } 96 | 97 | ROUTE_MIRRORING_TLV_TYPE = { 98 | 0: 'BGP Message TLV', 99 | 1: 'Information TLV' 100 | } 101 | 102 | ROUTE_MIRRORING_INFORMATION_TYPE_CODE = { 103 | 0: 'Errored PDU', 104 | 1: 'Message Lost' 105 | 106 | } 107 | -------------------------------------------------------------------------------- /yabmp/channel/publisher.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Cisco Systems, Inc. 2 | # All rights reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | """RabbitMQ Message Publisher 17 | """ 18 | 19 | import logging 20 | import traceback 21 | import json 22 | 23 | import pika 24 | 25 | LOG = logging.getLogger(__name__) 26 | logging.getLogger("pika").propagate = False 27 | 28 | 29 | class Publisher(object): 30 | """Publisher class 31 | """ 32 | 33 | def __init__(self, url): 34 | 35 | self.parameters = pika.URLParameters(url) 36 | self._connection = None 37 | self._channel = None 38 | 39 | def connect(self): 40 | """This method connects to RabbitMQ, returning the connection handle. 41 | 42 | :rtype: pika.BlockingConnection 43 | 44 | """ 45 | LOG.debug('Connecting to rabbitmq server') 46 | try: 47 | # Open a connection to RabbitMQ on localhost using all default parameters 48 | self._connection = pika.BlockingConnection(self.parameters) 49 | 50 | # Open the channel 51 | self._channel = self._connection.channel() 52 | return True 53 | except Exception as e: 54 | LOG.error(e) 55 | LOG.debug(traceback.format_exc()) 56 | return False 57 | 58 | def close(self): 59 | """Close connnection and channel 60 | """ 61 | if self._channel: 62 | self._channel.close() 63 | if self._connection: 64 | self._connection.close() 65 | 66 | def declare_exchange(self, _exchange, _type): 67 | """Declare exchange 68 | """ 69 | if not self.connect(): 70 | return False 71 | self._channel.exchange_declare(exchange=_exchange, type=_type) 72 | return True 73 | 74 | def bind_queue(self, _exchange, _queue): 75 | """bind a queue to exchange 76 | """ 77 | self._channel.queue_bind(exchange=_exchange, queue=_queue) 78 | 79 | def declare_queue(self, name): 80 | """Declare Queue 81 | """ 82 | if not self.connect(): 83 | return False 84 | self._channel.queue_declare(queue=name, durable=True) 85 | 86 | def publish_message(self, _exchange, _routing_key, _body): 87 | """ 88 | try to publish message to exchange 89 | :param _exchange: exchange name 90 | :param _routing_key: routing key 91 | :param _body: message body 92 | :return: True or False 93 | """ 94 | if not self.connect(): 95 | return False 96 | 97 | try: 98 | # try to declare exchange 99 | # if _exchange: 100 | # self._channel.exchange_declare(exchange=_exchange, type='direct') 101 | properties = pika.BasicProperties( 102 | content_type='application/json', 103 | delivery_mode=1 104 | ) 105 | # Turn on delivery confirmations 106 | self._channel.confirm_delivery() 107 | # Send a route policy 108 | if self._channel.basic_publish( 109 | exchange=_exchange, 110 | routing_key=_routing_key, 111 | body=json.dumps(_body), 112 | properties=properties): 113 | LOG.debug('Message publish was confirmed') 114 | return True 115 | else: 116 | LOG.error('Message publish could not be confirmed') 117 | return False 118 | except Exception as e: 119 | LOG.error(e) 120 | LOG.debug(traceback.format_exc()) 121 | return False 122 | -------------------------------------------------------------------------------- /yabmp/log.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015 Cisco Systems, Inc. 2 | # All rights reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | """ logging handler. Reference from https://github.com/osrg/ryu/blob/master/ryu/log.py 17 | """ 18 | 19 | from __future__ import print_function 20 | import inspect 21 | import logging 22 | import logging.config 23 | import logging.handlers 24 | import os 25 | import sys 26 | from oslo_config import cfg 27 | 28 | if sys.version_info[0] == 2: 29 | import ConfigParser 30 | elif sys.version_info[0] == 3: 31 | from configparser import ConfigParser 32 | 33 | CONF = cfg.CONF 34 | 35 | CONF.register_cli_opts([ 36 | cfg.BoolOpt('verbose', default=False, help='show debug output'), 37 | cfg.BoolOpt('use-stderr', default=True, help='log to standard error'), 38 | cfg.StrOpt('log-dir', default=None, help='log file directory'), 39 | cfg.StrOpt('log-file', 40 | default=os.path.join(os.environ.get('HOME') or '.', 'data/bmp/local/log/yabmp.log'), 41 | help='log file name'), 42 | cfg.StrOpt('log-file-mode', default='0644', 43 | help='default log file permission'), 44 | cfg.StrOpt('log-config-file', default=None, 45 | help='Path to a logging config file to use') 46 | ]) 47 | 48 | CONF.register_cli_opt(cfg.IntOpt( 49 | 'log-backup-count', 50 | default=5, 51 | help='the number of backup log file' 52 | )) 53 | 54 | DEBUG_LOG_FORMAT = '%(asctime)s.%(msecs)03d %(process)d %(levelname)s %(name)s ' \ 55 | '%(funcName)s %(lineno)d [-] %(message)s' 56 | INFOR_LOG_FORMAT = '%(asctime)s.%(msecs)03d %(process)d %(levelname)s %(name)s [-] %(message)s' 57 | _EARLY_LOG_HANDLER = None 58 | 59 | 60 | def early_init_log(level=None): 61 | global _EARLY_LOG_HANDLER 62 | _EARLY_LOG_HANDLER = logging.StreamHandler(sys.stderr) 63 | 64 | log = logging.getLogger() 65 | log.addHandler(_EARLY_LOG_HANDLER) 66 | if level is not None: 67 | log.setLevel(level) 68 | 69 | 70 | def _get_log_file(): 71 | if CONF.log_file: 72 | return CONF.log_file 73 | if CONF.log_dir: 74 | return os.path.join(CONF.log_dir, 75 | os.path.basename(inspect.stack()[-1][1])) + '.log' 76 | return None 77 | 78 | 79 | def _set_log_format(handers, _format): 80 | for hander in handers: 81 | hander.setFormatter(logging.Formatter(_format)) 82 | 83 | 84 | def init_log(): 85 | global _EARLY_LOG_HANDLER 86 | 87 | log = logging.getLogger() 88 | if CONF.log_config_file: 89 | try: 90 | logging.config.fileConfig(CONF.log_config_file, 91 | disable_existing_loggers=False) 92 | if CONF.verbose: 93 | log.setLevel(logging.DEBUG) 94 | for handler in log.handlers: 95 | handler.setFormatter(logging.Formatter(DEBUG_LOG_FORMAT)) 96 | except ConfigParser.Error as e: 97 | print('Failed to parse %s: %s' % (CONF.log_config_file, e), 98 | file=sys.stderr) 99 | sys.exit(2) 100 | return 101 | 102 | if CONF.use_stderr: 103 | log.addHandler(logging.StreamHandler(sys.stderr)) 104 | if _EARLY_LOG_HANDLER is not None: 105 | log.removeHandler(_EARLY_LOG_HANDLER) 106 | _EARLY_LOG_HANDLER = None 107 | 108 | log_file = _get_log_file() 109 | if log_file is not None: 110 | if not os.path.exists(os.path.dirname(log_file)): 111 | os.makedirs(os.path.dirname(log_file)) 112 | log.addHandler(logging.handlers.RotatingFileHandler( 113 | log_file, maxBytes=5 * 1024 * 1024, backupCount=CONF.log_backup_count)) 114 | mode = int(CONF.log_file_mode, 8) 115 | os.chmod(log_file, mode) 116 | for handler in log.handlers: 117 | handler.setFormatter(logging.Formatter(INFOR_LOG_FORMAT)) 118 | 119 | if CONF.verbose: 120 | log.setLevel(logging.DEBUG) 121 | for handler in log.handlers: 122 | handler.setFormatter(logging.Formatter(DEBUG_LOG_FORMAT)) 123 | else: 124 | log.setLevel(logging.INFO) 125 | _set_log_format(log.handlers, INFOR_LOG_FORMAT) 126 | -------------------------------------------------------------------------------- /example/mybmpd.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015 Cisco Systems, Inc. 2 | # All rights reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | import sys 17 | import time 18 | import logging 19 | from oslo_config import cfg 20 | from yabmp.handler import BaseHandler 21 | from yabmp.channel.publisher import Publisher 22 | from yabmp.channel import config as channel_config 23 | from yabmp.service import prepare_service 24 | 25 | CONF = cfg.CONF 26 | 27 | LOG = logging.getLogger(__name__) 28 | 29 | 30 | class ReHandler(BaseHandler): 31 | """rewrite handler to cast message of peer up and down 32 | """ 33 | def __init__(self): 34 | super(ReHandler, self).__init__() 35 | self.bgp_peer_dict = dict() 36 | self.puber = None 37 | 38 | def init(self): 39 | self.puber = Publisher(url=cfg.CONF.rabbit_mq.rabbit_url) 40 | 41 | def on_connection_made(self, peer_host, peer_port): 42 | """process for connection made 43 | """ 44 | try: 45 | self.puber.declare_queue(name='yabmp_%s' % peer_host) 46 | self.puber.declare_exchange(_exchange='yabmp_%s' % peer_host, _type='direct') 47 | self.puber.bind_queue(_exchange='yabmp_%s' % peer_host, _queue='yabmp_%s' % peer_host) 48 | msg_body = { 49 | "type": 0, 50 | "data": { 51 | "time": time.time(), 52 | "client_host": peer_host, 53 | "client_port": peer_port 54 | } 55 | } 56 | self.puber.publish_message( 57 | _exchange='yabmp_%s' % peer_host, 58 | _routing_key='yabmp_%s' % peer_host, 59 | _body=msg_body) 60 | except Exception as e: 61 | LOG.info(e) 62 | 63 | def on_connection_lost(self, peer_host, peer_port): 64 | """process for connection lost 65 | """ 66 | try: 67 | self.puber.declare_queue(name='yabmp_%s' % peer_host) 68 | self.puber.declare_exchange(_exchange='yabmp_%s' % peer_host, _type='direct') 69 | self.puber.bind_queue(_exchange='yabmp_%s' % peer_host, _queue='yabmp_%s' % peer_host) 70 | msg_body = { 71 | "type": 1, 72 | "data": { 73 | "time": time.time(), 74 | "client_host": peer_host, 75 | "client_port": peer_port 76 | } 77 | } 78 | self.puber.publish_message( 79 | _exchange='yabmp_%s' % peer_host, 80 | _routing_key='yabmp_%s' % peer_host, 81 | _body=msg_body) 82 | except Exception as e: 83 | LOG.info(e) 84 | 85 | def on_message_received(self, peer_host, peer_port, msg, msg_type): 86 | """process for message received 87 | """ 88 | if msg_type in [0, 1, 4, 5, 6]: 89 | return 90 | elif msg_type in [2, 3]: 91 | self.puber.declare_queue(name='yabmp_%s' % peer_host) 92 | self.puber.declare_exchange(_exchange='yabmp_%s' % peer_host, _type='direct') 93 | self.puber.bind_queue(_exchange='yabmp_%s' % peer_host, _queue='yabmp_%s' % peer_host) 94 | peer_ip = msg[0]['addr'] 95 | msg_body = { 96 | "type": msg_type, 97 | "data": { 98 | "time": time.time(), 99 | "client_ip": peer_host, 100 | "client_port": peer_port, 101 | "bgp_peer_ip": peer_ip 102 | } 103 | } 104 | self.puber.publish_message( 105 | _exchange='yabmp_%s' % peer_host, 106 | _routing_key='yabmp_%s' % peer_host, 107 | _body=msg_body) 108 | else: 109 | return 110 | 111 | 112 | def cli_opts_register(): 113 | CONF.register_cli_opts(channel_config.rabbit_mq, group='rabbit_mq') 114 | 115 | 116 | def main(): 117 | try: 118 | handler = ReHandler() 119 | cli_opts_register() 120 | prepare_service(handler=handler) 121 | except Exception as e: 122 | print(e) 123 | 124 | 125 | if __name__ == '__main__': 126 | sys.exit(main()) 127 | -------------------------------------------------------------------------------- /yabmp/core/protocol.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015 Cisco Systems, Inc. 2 | # All rights reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | import logging 17 | import struct 18 | import traceback 19 | from twisted.internet import protocol 20 | 21 | from yabmp.common import constants as bmp_cons 22 | from yabmp.common import exception as excp 23 | from yabmp.message.bmp import BMPMessage 24 | 25 | LOG = logging.getLogger() 26 | 27 | 28 | class BMP(protocol.Protocol): 29 | """ 30 | BGP Monitoring Protocol 31 | """ 32 | 33 | def __init__(self): 34 | 35 | LOG.info('Building a new BGP protocol instance') 36 | self.receive_buffer = b'' 37 | self.message = BMPMessage() 38 | self.bgp_peer_dict = {} 39 | self.client_ip = None 40 | self.client_port = None 41 | 42 | def connectionMade(self): 43 | """ 44 | TCP Conection made 45 | """ 46 | self.client_ip = self.transport.getPeer().host 47 | self.client_port = self.transport.getPeer().port 48 | LOG.info( 49 | "BMP Client %s:%s connected.", self.client_ip, self.client_port) 50 | self.factory.handler.on_connection_made( 51 | self.client_ip, self.client_port) 52 | 53 | def connectionLost(self, reason): 54 | """ 55 | TCP conection lost 56 | :param reason: 57 | """ 58 | LOG.info( 59 | "BMP Client %s disconnected,Connection was closed cleanly: %s", 60 | self.transport.getPeer().host, 61 | reason.getErrorMessage() 62 | ) 63 | self.client_ip = self.transport.getPeer().host 64 | self.client_port = self.transport.getPeer().port 65 | self.factory.handler.on_connection_lost( 66 | self.client_ip, self.client_port 67 | ) 68 | 69 | def closeConnection(self): 70 | """Close the connection""" 71 | if self.transport.connected: 72 | self.transport.loseConnection() 73 | 74 | def dataReceived(self, data): 75 | """ Data has been received. 76 | :param data: 77 | """ 78 | self.receive_buffer += data 79 | try: 80 | while self.parse_buffer(): 81 | pass 82 | except Exception as e: 83 | LOG.error(e) 84 | error_str = traceback.format_exc() 85 | LOG.debug(error_str) 86 | self.closeConnection() 87 | 88 | def parse_buffer(self): 89 | """ 90 | """ 91 | buf = self.receive_buffer 92 | if len(buf) < bmp_cons.HEADER_LEN: 93 | # Every BMP message is at least 44 octets. Maybe the rest 94 | # hasn't arrived yet. 95 | return False 96 | 97 | # Parse the header 98 | # +-+-+-+-+-+-+-+-+ 99 | # | Version | 100 | # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 101 | # | Message Length | 102 | # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 103 | # | Msg. Type | 104 | # +---------------+ 105 | version, length, msg_type = struct.unpack('!BIB', buf[:6]) 106 | 107 | if version != bmp_cons.VERSION: 108 | # close the connection 109 | raise excp.BMPVersionError(local_version=bmp_cons.VERSION, remote_version=version) 110 | if msg_type not in bmp_cons.MSG_TYPE_STR.keys(): 111 | raise excp.UnknownMessageTypeError(type=msg_type) 112 | if length > len(buf): 113 | # the hold message does not comming yet. 114 | return False 115 | msg_value = buf[6:length] 116 | self.message.msg_type = msg_type 117 | LOG.debug('Received BMP message, type=%s' % msg_type) 118 | self.message.raw_body = msg_value 119 | LOG.debug('Decoding message...') 120 | try: 121 | results = self.message.consume() 122 | if results: 123 | # write msg file 124 | self.factory.handler.on_message_received( 125 | self.client_ip, self.client_port, results, msg_type) 126 | else: 127 | LOG.error('decoding message failed.') 128 | 129 | except Exception as e: 130 | LOG.error(e) 131 | error_str = traceback.format_exc() 132 | LOG.debug(error_str) 133 | LOG.debug('Finished decoding.') 134 | self.message = BMPMessage() 135 | LOG.debug('-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+') 136 | self.receive_buffer = self.receive_buffer[length:] 137 | return True 138 | -------------------------------------------------------------------------------- /yabmp/handler/default.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015-2017 Cisco Systems, Inc. 2 | # All rights reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | import os 17 | import logging 18 | import sys 19 | import time 20 | 21 | from oslo_config import cfg 22 | 23 | from yabmp.handler import BaseHandler 24 | 25 | CONF = cfg.CONF 26 | 27 | LOG = logging.getLogger(__name__) 28 | 29 | MSG_PROCESS_OPTS = [ 30 | cfg.StrOpt( 31 | 'write_dir', 32 | default=os.path.join(os.environ.get('HOME', './'), 'data/bmp/local/msg'), 33 | help='The BMP messages storage path'), 34 | cfg.IntOpt( 35 | 'write_msg_max_size', 36 | default=500, 37 | help='The Max size of one BMP message file, the unit is MB') 38 | ] 39 | 40 | 41 | class DefaultHandler(BaseHandler): 42 | """default handler 43 | """ 44 | def __init__(self): 45 | super(DefaultHandler, self).__init__() 46 | CONF.register_cli_opts(MSG_PROCESS_OPTS, group='message') 47 | self.bgp_peer_dict = dict() 48 | 49 | def init(self): 50 | if not os.path.exists(CONF.message.write_dir): 51 | try: 52 | os.makedirs(CONF.message.write_dir) 53 | LOG.info('Create message output path: %s', CONF.message.write_dir) 54 | except Exception as e: 55 | LOG.error(e, exc_info=True) 56 | sys.exit() 57 | 58 | def on_connection_made(self, peer_host, peer_port): 59 | """process for connection made 60 | """ 61 | file_path = os.path.join(CONF.message.write_dir, peer_host) 62 | if not os.path.exists(file_path): 63 | os.makedirs(file_path) 64 | LOG.info('Create directory: %s for peer %s', file_path, peer_host) 65 | 66 | def on_connection_lost(self, peer_host, peer_port): 67 | """process for connection lost 68 | """ 69 | pass 70 | 71 | def on_message_received(self, peer_host, peer_port, msg, msg_type): 72 | """process for message received 73 | """ 74 | if msg_type in [4, 5, 6]: 75 | return 76 | peer_ip = msg[0]['addr'] 77 | if peer_ip not in self.bgp_peer_dict: 78 | self.bgp_peer_dict[peer_ip] = {} 79 | peer_msg_path = os.path.join( 80 | os.path.join(CONF.message.write_dir, peer_host), peer_ip) 81 | if not os.path.exists(peer_msg_path): 82 | # this peer first come out 83 | # create a peer path and open the first message file 84 | os.makedirs(peer_msg_path) 85 | LOG.info('Create directory for peer: %s' % peer_msg_path) 86 | msg_file_name = os.path.join(peer_msg_path, '%s.msg' % time.time()) 87 | self.bgp_peer_dict[peer_ip]['msg_seq'] = 1 88 | else: 89 | # this peer is not first come out 90 | # find the latest message file and get the last message sequence number 91 | file_list = os.listdir(peer_msg_path) 92 | file_list.sort() 93 | msg_file_name = os.path.join(peer_msg_path, file_list[-1]) 94 | self.bgp_peer_dict[peer_ip]['msg_seq'] = self.get_last_seq(msg_file_name) 95 | self.bgp_peer_dict[peer_ip]['file'] = open(msg_file_name, 'a') 96 | if msg_type == 0: # route monitoring message 97 | if msg[0]['flags']['L']: 98 | # pos-policy RIB 99 | msg_list = [time.time(), self.bgp_peer_dict[peer_ip]['msg_seq'], 130, msg[1], (1, 1)] 100 | else: 101 | # pre-policy RIB 102 | msg_list = [time.time(), self.bgp_peer_dict[peer_ip]['msg_seq'], msg[1][0], msg[1][1], (1, 1)] 103 | self.bgp_peer_dict[peer_ip]['file'].write(str(msg_list) + '\n') 104 | self.bgp_peer_dict[peer_ip]['msg_seq'] += 1 105 | self.bgp_peer_dict[peer_ip]['file'].flush() 106 | elif msg_type == 1: # statistic message 107 | msg_list = [time.time(), self.bgp_peer_dict[peer_ip]['msg_seq'], 129, msg[1], (0, 0)] 108 | self.bgp_peer_dict[peer_ip]['file'].write(str(msg_list) + '\n') 109 | self.bgp_peer_dict[peer_ip]['msg_seq'] += 1 110 | self.bgp_peer_dict[peer_ip]['file'].flush() 111 | elif msg_type == 2: # peer down message 112 | msg_list = [time.time(), self.bgp_peer_dict[peer_ip]['msg_seq'], 3, msg[1], (0, 0)] 113 | self.bgp_peer_dict[peer_ip]['file'].write(str(msg_list) + '\n') 114 | self.bgp_peer_dict[peer_ip]['msg_seq'] += 1 115 | self.bgp_peer_dict[peer_ip]['file'].flush() 116 | 117 | elif msg_type == 3: # peer up message 118 | msg_list = [time.time(), self.bgp_peer_dict[peer_ip]['msg_seq'], 1, msg[1]['received_open_msg'], (0, 0)] 119 | self.bgp_peer_dict[peer_ip]['file'].write(str(msg_list) + '\n') 120 | self.bgp_peer_dict[peer_ip]['msg_seq'] += 1 121 | self.bgp_peer_dict[peer_ip]['file'].flush() 122 | 123 | @staticmethod 124 | def get_last_seq(file_name): 125 | 126 | """ 127 | Get the last sequence number in the log file. 128 | """ 129 | 130 | lines_2find = 1 131 | f = open(file_name, 'rb') 132 | f.seek(0, 2) # go to the end of the file 133 | bytes_in_file = f.tell() 134 | lines_found, total_bytes_scanned = 0, 0 135 | while (lines_2find + 1 > lines_found and 136 | bytes_in_file > total_bytes_scanned): 137 | byte_block = min(1024, bytes_in_file - total_bytes_scanned) 138 | f.seek(-(byte_block + total_bytes_scanned), 2) 139 | total_bytes_scanned += byte_block 140 | lines_found += f.read(1024).count(b'\n') 141 | f.seek(-total_bytes_scanned, 2) 142 | line_list = list(f.readlines()) 143 | last_line = line_list[-lines_2find:][0] 144 | try: 145 | last_line = eval(last_line) 146 | return last_line[1] + 1 147 | except Exception as e: 148 | LOG.info(e) 149 | return 1 150 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /yabmp/channel/consumer.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015-2017 Cisco Systems, Inc. 2 | # All rights reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | """RabbitMQ Message Consumer 17 | """ 18 | 19 | import logging 20 | 21 | import pika 22 | 23 | from yacore.channel import Channel 24 | 25 | LOG = logging.getLogger(__name__) 26 | logging.getLogger("pika").propagate = False 27 | 28 | 29 | class Consumer(Channel): 30 | """This is an message consumer that will handle unexpected interactions 31 | with RabbitMQ such as channel and connection closures. 32 | 33 | If RabbitMQ closes the connection, it will reopen it. You should 34 | look at the output, as there are limited reasons why the connection may 35 | be closed, which usually are tied to permission related issues or 36 | socket timeouts. 37 | 38 | If the channel is closed, it will indicate a problem with one of the 39 | commands that were issued and that should surface in the output as well. 40 | 41 | """ 42 | 43 | def __init__(self, url, queue_name, routing_key='', exchange=''): 44 | """Create a new instance of the consumer class, passing in the AMQP 45 | URL used to connect to RabbitMQ. 46 | 47 | :param str url: The rabbitmq url 48 | 49 | """ 50 | self.parameters = pika.URLParameters(url) 51 | self.queue_name = queue_name 52 | self.routing_key = routing_key 53 | self.exchange = exchange 54 | self._connection = None 55 | self._channel = None 56 | self._closing = False 57 | self._consumer_tag = None 58 | 59 | def connect(self): 60 | """This method connects to RabbitMQ, returning the connection handle. 61 | When the connection is established, the on_connection_open method 62 | will be invoked by pika. 63 | 64 | :rtype: pika.SelectConnection 65 | 66 | """ 67 | LOG.info('Connecting to rabbitmq server') 68 | return pika.SelectConnection(self.parameters, 69 | self.on_connection_open, 70 | stop_ioloop_on_close=False) 71 | 72 | def on_connection_open(self, unused_connection): 73 | """This method is called by pika once the connection to RabbitMQ has 74 | been established. It passes the handle to the connection object in 75 | case we need it, but in this case, we'll just mark it unused. 76 | 77 | :type unused_connection: pika.SelectConnection 78 | 79 | """ 80 | LOG.info('Connection opened') 81 | self.add_on_connection_close_callback() 82 | self.open_channel() 83 | 84 | def add_on_connection_close_callback(self): 85 | """This method adds an on close callback that will be invoked by pika 86 | when RabbitMQ closes the connection to the publisher unexpectedly. 87 | 88 | """ 89 | LOG.info('Adding connection close callback') 90 | self._connection.add_on_close_callback(self.on_connection_closed) 91 | 92 | def on_connection_closed(self, connection, reply_code, reply_text): 93 | """This method is invoked by pika when the connection to RabbitMQ is 94 | closed unexpectedly. Since it is unexpected, we will reconnect to 95 | RabbitMQ if it disconnects. 96 | 97 | :param pika.connection.Connection connection: The closed connection obj 98 | :param int reply_code: The server provided reply_code if given 99 | :param str reply_text: The server provided reply_text if given 100 | 101 | """ 102 | self._channel = None 103 | if self._closing: 104 | self._connection.ioloop.stop() 105 | else: 106 | LOG.warning('Connection closed, reopening in 5 seconds: (%s) %s', 107 | reply_code, reply_text) 108 | self._connection.add_timeout(5, self.reconnect) 109 | 110 | def reconnect(self): 111 | """Will be invoked by the IOLoop timer if the connection is 112 | closed. See the on_connection_closed method. 113 | 114 | """ 115 | # This is the old connection IOLoop instance, stop its ioloop 116 | self._connection.ioloop.stop() 117 | 118 | if not self._closing: 119 | 120 | # Create a new connection 121 | self._connection = self.connect() 122 | 123 | # There is now a new connection, needs a new ioloop to run 124 | self._connection.ioloop.start() 125 | 126 | def open_channel(self): 127 | """Open a new channel with RabbitMQ by issuing the Channel.Open RPC 128 | command. When RabbitMQ responds that the channel is open, the 129 | on_channel_open callback will be invoked by pika. 130 | 131 | """ 132 | LOG.info('Creating a new channel') 133 | self._connection.channel(on_open_callback=self.on_channel_open) 134 | 135 | def on_channel_open(self, channel): 136 | """This method is invoked by pika when the channel has been opened. 137 | The channel object is passed in so we can make use of it. 138 | 139 | Since the channel is now open, we'll declare the exchange to use. 140 | 141 | :param pika.channel.Channel channel: The channel object 142 | 143 | """ 144 | LOG.info('Channel opened') 145 | self._channel = channel 146 | self.add_on_channel_close_callback() 147 | self.setup_queue() 148 | 149 | def add_on_channel_close_callback(self): 150 | """This method tells pika to call the on_channel_closed method if 151 | RabbitMQ unexpectedly closes the channel. 152 | 153 | """ 154 | LOG.info('Adding channel close callback') 155 | self._channel.add_on_close_callback(self.on_channel_closed) 156 | 157 | def on_channel_closed(self, channel, reply_code, reply_text): 158 | """Invoked by pika when RabbitMQ unexpectedly closes the channel. 159 | Channels are usually closed if you attempt to do something that 160 | violates the protocol, such as re-declare an exchange or queue with 161 | different parameters. In this case, we'll close the connection 162 | to shutdown the object. 163 | 164 | :param pika.channel.Channel: The closed channel 165 | :param int reply_code: The numeric reason the channel was closed 166 | :param str reply_text: The text reason the channel was closed 167 | 168 | """ 169 | LOG.warning('Channel %i was closed: (%s) %s', 170 | channel, reply_code, reply_text) 171 | self._connection.close() 172 | 173 | def setup_queue(self): 174 | """Setup the queue on RabbitMQ by invoking the Queue.Declare RPC 175 | command. When it is complete, the on_queue_declareok method will 176 | be invoked by pika. 177 | """ 178 | LOG.info('Declaring queue %s', self.queue_name) 179 | self._channel.queue_declare( 180 | callback=self.start_consuming(), queue=self.queue_name, durable=True) 181 | LOG.info('Bind the queue to exchange') 182 | self._channel.queue_bind( 183 | exchange=self.exchange, 184 | queue=self.queue_name, 185 | routing_key=self.routing_key) 186 | self._channel.basic_qos(prefetch_count=1) 187 | 188 | def start_consuming(self): 189 | """This method sets up the consumer by first calling 190 | add_on_cancel_callback so that the object is notified if RabbitMQ 191 | cancels the consumer. It then issues the Basic.Consume RPC command 192 | which returns the consumer tag that is used to uniquely identify the 193 | consumer with RabbitMQ. We keep the value to use it when we want to 194 | cancel consuming. The on_message method is passed in as a callback pika 195 | will invoke when a message is fully received. 196 | 197 | """ 198 | LOG.info('Issuing consumer related RPC commands') 199 | self.add_on_cancel_callback() 200 | self._consumer_tag = self._channel.basic_consume(self.on_message, 201 | self.queue_name) 202 | 203 | def add_on_cancel_callback(self): 204 | """Add a callback that will be invoked if RabbitMQ cancels the consumer 205 | for some reason. If RabbitMQ does cancel the consumer, 206 | on_consumer_cancelled will be invoked by pika. 207 | 208 | """ 209 | LOG.info('Adding consumer cancellation callback') 210 | self._channel.add_on_cancel_callback(self.on_consumer_cancelled) 211 | 212 | def on_consumer_cancelled(self, method_frame): 213 | """Invoked by pika when RabbitMQ sends a Basic.Cancel for a consumer 214 | receiving messages. 215 | 216 | :param pika.frame.Method method_frame: The Basic.Cancel frame 217 | 218 | """ 219 | LOG.info('Consumer was cancelled remotely, shutting down: %r', method_frame) 220 | if self._channel: 221 | self._channel.close() 222 | 223 | def on_message(self, unused_channel, basic_deliver, properties, body): 224 | """Invoked by pika when a message is delivered from RabbitMQ. The 225 | channel is passed for your convenience. The basic_deliver object that 226 | is passed in carries the exchange, routing key, delivery tag and 227 | a redelivered flag for the message. The properties passed in is an 228 | instance of BasicProperties with the message properties and the body 229 | is the message that was sent. 230 | 231 | :param pika.channel.Channel unused_channel: The channel object 232 | :param pika.Spec.Basic.Deliver basic_deliver: basic_deliver method 233 | :param pika.Spec.BasicProperties properties: properties 234 | :param str|unicode body: The message body 235 | 236 | """ 237 | LOG.debug('Received message # %s from %s: %s', 238 | basic_deliver.delivery_tag, properties.app_id, body) 239 | self.acknowledge_message(basic_deliver.delivery_tag) 240 | 241 | def acknowledge_message(self, delivery_tag): 242 | """Acknowledge the message delivery from RabbitMQ by sending a 243 | Basic.Ack RPC method for the delivery tag. 244 | 245 | :param int delivery_tag: The delivery tag from the Basic.Deliver frame 246 | 247 | """ 248 | LOG.debug('Acknowledging message %s', delivery_tag) 249 | self._channel.basic_ack(delivery_tag) 250 | 251 | def stop_consuming(self): 252 | """Tell RabbitMQ that you would like to stop consuming by sending the 253 | Basic.Cancel RPC command. 254 | 255 | """ 256 | if self._channel: 257 | LOG.info('Sending a Basic.Cancel RPC command to RabbitMQ') 258 | self._channel.basic_cancel(self.on_cancelok, self._consumer_tag) 259 | 260 | def on_cancelok(self, unused_frame): 261 | """This method is invoked by pika when RabbitMQ acknowledges the 262 | cancellation of a consumer. At this point we will close the channel. 263 | This will invoke the on_channel_closed method once the channel has been 264 | closed, which will in-turn close the connection. 265 | 266 | :param pika.frame.Method unused_frame: The Basic.CancelOk frame 267 | 268 | """ 269 | LOG.info('RabbitMQ acknowledged the cancellation of the consumer') 270 | self.close_channel() 271 | 272 | def close_channel(self): 273 | """Call to close the channel with RabbitMQ cleanly by issuing the 274 | Channel.Close RPC command. 275 | 276 | """ 277 | LOG.info('Closing the channel') 278 | self._channel.close() 279 | 280 | def run(self): 281 | """Run the example consumer by connecting to RabbitMQ and then 282 | starting the IOLoop to block and allow the SelectConnection to operate. 283 | 284 | """ 285 | self._connection = self.connect() 286 | self._connection.ioloop.start() 287 | 288 | def stop(self): 289 | """Cleanly shutdown the connection to RabbitMQ by stopping the consumer 290 | with RabbitMQ. When RabbitMQ confirms the cancellation, on_cancelok 291 | will be invoked by pika, which will then closing the channel and 292 | connection. The IOLoop is started again because this method is invoked 293 | when CTRL-C is pressed raising a KeyboardInterrupt exception. This 294 | exception stops the IOLoop which needs to be running for pika to 295 | communicate with RabbitMQ. All of the commands issued prior to starting 296 | the IOLoop will be buffered but not processed. 297 | 298 | """ 299 | LOG.info('Stopping') 300 | self._closing = True 301 | self.stop_consuming() 302 | self._connection.ioloop.start() 303 | LOG.info('Stopped') 304 | 305 | def close_connection(self): 306 | """This method closes the connection to RabbitMQ.""" 307 | LOG.info('Closing connection') 308 | self._connection.close() 309 | -------------------------------------------------------------------------------- /yabmp/message/bmp.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015 Cisco Systems, Inc. 2 | # All rights reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | import struct 17 | import binascii 18 | import logging 19 | import traceback 20 | import itertools 21 | 22 | import netaddr 23 | from bitstring import BitArray 24 | 25 | from yabgp.message.notification import Notification 26 | from yabgp.message.update import Update 27 | from yabgp.message.route_refresh import RouteRefresh 28 | from yabgp.message.open import Open 29 | from yabgp.common import constants as bgp_cons 30 | 31 | from yabmp.common import constants as bmp_cons 32 | from yabmp.common import exception as excp 33 | 34 | LOG = logging.getLogger() 35 | 36 | 37 | class BMPMessage(object): 38 | """ 39 | BMP message class. 40 | definition of BMP message and methons used to decode message. 41 | """ 42 | 43 | def __init__(self): 44 | 45 | self.version = None 46 | self.msg_type = None 47 | self.raw_body = None 48 | self.msg_body = None 49 | 50 | @staticmethod 51 | def rd2str(rd): 52 | """ 53 | Convert 8 bytes of router distinguisher into string 54 | according to rfc4364 section 4.2. 55 | The first two bytes defines decode format 56 | 0 asn:nn 57 | 1 ip:nn 58 | 2 asn4:nn 59 | """ 60 | rd_type = int.from_bytes(rd[0:2], "big"); 61 | if rd_type == 0: 62 | return str(int.from_bytes(rd[2:4], "big")) + ":" + str(int.from_bytes(rd[4:8], "big")) 63 | elif rd_type == 1: 64 | ip_value = int(binascii.b2a_hex(rd[2:6]), 16) 65 | return str(netaddr.IPAddress(ip_value, version=4)) + ":" + str(int.from_bytes(rd[6:8], "big")) 66 | elif rd_type == 2: 67 | return str(int.from_bytes(rd[2:6], "big")) + ":" + str(int.from_bytes(rd[6:8], "big")) 68 | else: 69 | return str (rd[0:8]) 70 | 71 | @staticmethod 72 | def parse_per_peer_header(raw_peer_header): 73 | """ 74 | decode per-peer header. 75 | every bmp message has this header, and the header length is 42 bytes. 76 | :param raw_peer_header: hex value of the header 77 | :return: 78 | """ 79 | # 0 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 80 | # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 81 | # | Peer Type | Peer Flags | 82 | # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 83 | # | Peer Distinguisher (present based on peer type) | 84 | # | | 85 | # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 86 | # | Peer Address (16 bytes) | 87 | # ~ ~ 88 | # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 89 | # | Peer AS | 90 | # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 91 | # | Peer BGP ID | 92 | # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 93 | # | Timestamp (seconds) | 94 | # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 95 | # | Timestamp (microseconds) | 96 | # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 97 | per_header_dict = { 98 | 'type': None, 99 | 'flags': None, 100 | 'dist': None, 101 | 'addr': None, 102 | 'as': None, 103 | 'bgpID': None, 104 | 'time': None 105 | } 106 | LOG.debug('decode per-peer header') 107 | per_header_dict['type'] = struct.unpack('!B', raw_peer_header[0:1])[0] 108 | # Peer Type = 0: Global Instance Peer 109 | # Peer Type = 1: RD Instance Peer 110 | # Peer Type = 2: Local Instance Peer 111 | if per_header_dict['type'] not in [0, 1, 2]: 112 | raise excp.UnknownPeerTypeValue(peer_type=per_header_dict['type']) 113 | LOG.debug('peer type: %s ' % per_header_dict['type']) 114 | 115 | # Peer Flags 116 | peer_flags_value = binascii.b2a_hex(raw_peer_header[1:2]) 117 | hex_rep = hex(int(peer_flags_value, 16)) 118 | bit_array = BitArray(hex_rep) 119 | valid_flags = [''.join(item)+'00000' for item in itertools.product('01', repeat=3)] 120 | valid_flags.append('0000') 121 | if bit_array.bin in valid_flags: 122 | flags = dict(zip(bmp_cons.PEER_FLAGS, bit_array.bin)) 123 | per_header_dict['flags'] = flags 124 | LOG.debug('Per Peer header flags %s' % flags) 125 | else: 126 | raise excp.UnknownPeerFlagValue(peer_flags=peer_flags_value) 127 | LOG.debug('peer flag: %s ' % per_header_dict['flags']) 128 | if per_header_dict['type'] in [1, 2]: 129 | per_header_dict['dist'] = BMPMessage.rd2str(raw_peer_header[2:10]) 130 | LOG.debug('peer dist: %s' % per_header_dict['dist']) 131 | 132 | ip_value = int(binascii.b2a_hex(raw_peer_header[10:26]), 16) 133 | if int(per_header_dict['flags']['V']): 134 | per_header_dict['addr'] = str(netaddr.IPAddress(ip_value, version=6)) 135 | else: 136 | per_header_dict['addr'] = str(netaddr.IPAddress(ip_value, version=4)) 137 | 138 | per_header_dict['as'] = int(binascii.b2a_hex(raw_peer_header[26:30]), 16) 139 | LOG.debug('peer as: %s' % per_header_dict['as']) 140 | per_header_dict['bgpID'] = str(netaddr.IPAddress(int(binascii.b2a_hex(raw_peer_header[30:34]), 16))) 141 | LOG.debug('peer bgp id: %s' % per_header_dict['bgpID']) 142 | per_header_dict['time'] = (int(binascii.b2a_hex(raw_peer_header[34:38]), 16), 143 | int(binascii.b2a_hex(raw_peer_header[38:42]), 16)) 144 | LOG.debug('timestamp: %s.%s' % (per_header_dict['time'][0], per_header_dict['time'][1])) 145 | return per_header_dict 146 | 147 | @staticmethod 148 | def parse_route_monitoring_msg(msg): 149 | """ 150 | Route Monitoring messages are used for initial synchronization of 151 | ADJ-RIBs-In. They are also used for ongoing monitoring of received 152 | advertisements and withdraws. 153 | Following the common BMP header and per-peer header is a BGP Update 154 | PDU. 155 | :param msg: 156 | :return: 157 | """ 158 | LOG.debug('decode route monitoring message') 159 | bgp_msg_type = struct.unpack('!B', msg[18:19])[0] 160 | LOG.debug('bgp message type=%s' % bgp_msg_type) 161 | msg = msg[bgp_cons.HDR_LEN:] 162 | if bgp_msg_type == 2: 163 | # decode update message 164 | results = Update().parse(None, msg, asn4=True) 165 | if results['sub_error']: 166 | LOG.error('error: decode update message error!, error code: %s' % results['sub_error']) 167 | LOG.error('Raw data: %s' % repr(results['hex'])) 168 | return None 169 | return_result = { 170 | 'attr': results['attr'], 171 | 'nlri': results['nlri'], 172 | 'withdraw': results['withdraw']} 173 | LOG.debug('bgp update message: %s' % return_result) 174 | return bgp_msg_type, return_result 175 | elif bgp_msg_type == 5: 176 | bgp_route_refresh_msg = RouteRefresh().parse(msg=msg) 177 | LOG.debug('bgp route refresh message: afi=%s,res=%s,safi=%s' % (bgp_route_refresh_msg[0], 178 | bgp_route_refresh_msg[1], 179 | bgp_route_refresh_msg[2])) 180 | return bgp_msg_type, {'afi': bgp_route_refresh_msg[0], 181 | 'sub_type': bgp_route_refresh_msg[1], 182 | 'safi': bgp_route_refresh_msg[2]} 183 | 184 | @staticmethod 185 | def parse_route_mirroring_msg(msg): 186 | """ 187 | Route Mirroring messages are used for verbatim duplication of 188 | messages as received. Following the common BMP header and per-peer 189 | header is a set of TLVs that contain information about a message 190 | or set of messages. 191 | :param msg: 192 | :return: 193 | """ 194 | LOG.debug('decode route mirroring message') 195 | 196 | msg_dict = {} 197 | open_l = [] 198 | update = [] 199 | notification = [] 200 | route_refresh = [] 201 | while msg: 202 | mirror_type, length = struct.unpack('!HH', msg[0:4]) 203 | mirror_value = msg[4: 4 + length] 204 | msg = msg[4 + length:] 205 | if mirror_type == 0: 206 | # BGP message type 207 | bgp_msg_type = struct.unpack('!B', mirror_value[18:19])[0] 208 | LOG.debug('bgp message type=%s' % bgp_msg_type) 209 | bgp_msg_body = mirror_value[bgp_cons.HDR_LEN:] 210 | if bgp_msg_type == 2: 211 | # Update message 212 | bgp_update_msg = Update().parse(None, bgp_msg_body, asn4=True) 213 | if bgp_update_msg['sub_error']: 214 | LOG.error('error: decode update message error!, error code: %s' % bgp_update_msg['sub_error']) 215 | LOG.error('Raw data: %s' % repr(bgp_update_msg['hex'])) 216 | else: 217 | update.append(bgp_update_msg) 218 | elif bgp_msg_type == 5: 219 | # Route Refresh message 220 | bgp_route_refresh_msg = RouteRefresh().parse(msg=bgp_msg_body) 221 | LOG.debug('bgp route refresh message: afi=%s,res=%s,safi=%s' % (bgp_route_refresh_msg[0], 222 | bgp_route_refresh_msg[1], 223 | bgp_route_refresh_msg[2])) 224 | route_refresh.append(bgp_route_refresh_msg) 225 | elif bgp_msg_type == 1: 226 | # Open message 227 | open_msg = Open().parse(bgp_msg_body) 228 | open_l.append(open_msg) 229 | elif bgp_msg_type == 3: 230 | # Notification message 231 | notification_msg = Notification().parse(bgp_msg_body) 232 | notification.append(notification_msg) 233 | elif mirror_type == 1: 234 | # Information type. 235 | # Amount of this TLV is not specified but we can assume 236 | # only one per mirroring message is present. 237 | info_code_type = struct.unpack('!H', mirror_value)[0] 238 | msg_dict['1'] = info_code_type 239 | else: 240 | msg_dict[mirror_type] = binascii.unhexlify(binascii.hexlify(mirror_value)) 241 | LOG.info('unknow mirroring type, type = %s' % mirror_type) 242 | 243 | msg_dict['0'] = { 244 | 'update': update, 245 | 'route_refresh': route_refresh, 246 | 'open': open_l, 247 | 'notification': notification 248 | } 249 | return msg_dict 250 | 251 | @staticmethod 252 | def parse_statistic_report_msg(msg): 253 | """ 254 | These messages contain information that could be used by the 255 | monitoring station to observe interesting events that occur on the 256 | router. 257 | :return: 258 | """ 259 | # 0 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 260 | # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 261 | # | Stats Count | 262 | # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 263 | # Each counter is encoded as follows, 264 | # 0 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 265 | # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 266 | # | Stat Type | Stat Len | 267 | # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 268 | # | Stat Data | 269 | # ~ ~ 270 | # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 271 | LOG.info('decode statistic report message') 272 | count_num = int(binascii.b2a_hex(msg[0:4]), 16) 273 | count_dict = {} 274 | msg = msg[4:] 275 | while count_num: 276 | stat_type, stat_len = struct.unpack('!HH', msg[0:4]) 277 | stat_data = msg[4:4+stat_len] 278 | msg = msg[4+stat_len:] 279 | stat_value = int(binascii.b2a_hex(stat_data), 16) 280 | count_dict[stat_type] = stat_value 281 | if stat_type not in bmp_cons.BMP_STAT_TYPE: 282 | LOG.warning('unknown statistic report type, type=%s' % stat_type) 283 | else: 284 | LOG.info('stat_type=%s, stat_value=%s' % (bmp_cons.BMP_STAT_TYPE[stat_type], stat_value)) 285 | count_num -= 1 286 | return count_dict 287 | 288 | @staticmethod 289 | def parse_peer_down_notification(msg): 290 | """ 291 | This message is used to indicate that a peering session was terminated. 292 | :param msg: 293 | :return: 294 | """ 295 | # 0 1 2 3 4 5 6 7 8 296 | # +-+-+-+-+-+-+-+-+ 297 | # | Reason | 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 298 | # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 299 | # | Data (present if Reason = 1, 2 or 3) | 300 | # ~ ~ 301 | # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 302 | LOG.info('decode peer down notification') 303 | reason = int(binascii.b2a_hex(msg[0:1]), 16) 304 | LOG.info('reason: %s' % reason) 305 | data = msg[1:] 306 | 307 | if reason == 1: 308 | LOG.info('Reason : 1 The local system closed the session. Following the ' 309 | 'Reason is a BGP PDU containing a BGP NOTIFICATION message that' 310 | 'would have been sent to the peer') 311 | Notification().parse(message=data) 312 | elif reason == 2: 313 | LOG.info('Reason :2 The local system closed the session. No notification' 314 | 'message was sent. Following the reason code is a two-byte field' 315 | 'containing the code corresponding to the FSM Event which caused' 316 | 'the system to close the session (see Section 8.1 of [RFC4271]).' 317 | 'Two bytes both set to zero are used to indicate that no relevant' 318 | 'Event code is defined') 319 | elif reason == 3: 320 | LOG.info('Reason : 3 The remote system closed the session with a notification' 321 | 'message. Following the Reason is a BGP PDU containing the BGP' 322 | 'NOTIFICATION message as received from the peer.') 323 | elif reason == 4: 324 | LOG.info('Reason : 4 The remote system closed the session without a notification message') 325 | else: 326 | LOG.waring('unknown peer down notification reason') 327 | return reason 328 | 329 | @staticmethod 330 | def parse_peer_up_notification(msg, peer_flag): 331 | """ 332 | The Peer Up message is used to indicate that a peering session has 333 | come up (i.e., has transitioned into ESTABLISHED state). Following 334 | the common BMP header and per-peer header is the following: 335 | :param msg: 336 | :param peer_flag: see parse_per_peer_header 337 | :return: 338 | """ 339 | # 0 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 340 | # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 341 | # | Local Address (16 bytes) | 342 | # ~ ~ 343 | # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 344 | # | Local Port | Remote Port | 345 | # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 346 | # | Sent OPEN Message #| 347 | # ~ ~ 348 | # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 349 | # | Received OPEN Message | 350 | # ~ ~ 351 | # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 352 | LOG.info('decode peer up notification') 353 | ip_value = int(binascii.b2a_hex(msg[0:16]), 16) 354 | if int(peer_flag['V']): 355 | # ipv6 address 356 | ip_address = str(netaddr.IPAddress(ip_value, version=6)) 357 | else: 358 | ip_address = str(netaddr.IPAddress(ip_value, version=4)) 359 | LOG.info('local address: %s' % ip_address) 360 | local_port = int(binascii.b2a_hex(msg[16:18]), 16) 361 | LOG.info('local port: %s' % local_port) 362 | remote_port = int(binascii.b2a_hex(msg[18:20]), 16) 363 | LOG.info('remote port: %s' % remote_port) 364 | # decode sent and received open message 365 | open_msg_data = msg[20:] 366 | length = struct.unpack('!H', open_msg_data[16:18])[0] 367 | sent_open_msg = Open().parse(open_msg_data[bgp_cons.HDR_LEN: length]) 368 | open_msg_data = open_msg_data[length:] 369 | received_open_msg = Open().parse(open_msg_data[bgp_cons.HDR_LEN:]) 370 | LOG.info('sent open: %s' % sent_open_msg) 371 | LOG.info('received open: %s' % received_open_msg) 372 | return { 373 | 'local_address': ip_address, 374 | 'local_port': local_port, 375 | 'remote_port': remote_port, 376 | 'sent_open_msg': sent_open_msg, 377 | 'received_open_msg': received_open_msg 378 | } 379 | 380 | @staticmethod 381 | def parse_initiation_msg(msg): 382 | """ 383 | The initiation message provides a means for the monitored router to 384 | inform the monitoring station of its vendor, software version, and so 385 | on. An initiation message MUST be sent as the first message after 386 | the TCP session comes up. An initiation message MAY be sent at any 387 | point thereafter, if warranted by a change on the monitored router. 388 | The initiation message consists of the common BMP header followed by 389 | two or more TLVs containing information about the monitored router, 390 | as follows: 391 | :return: 392 | """ 393 | # 0 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 394 | # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 395 | # | Information Type | Information Length | 396 | # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 397 | # | Information (variable) | 398 | # ~ ~ 399 | # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 400 | LOG.info('decode initiation message') 401 | msg_dict = {} 402 | while msg: 403 | info_type, length = struct.unpack('!HH', msg[0:4]) 404 | info_value = msg[4: 4 + length] 405 | msg = msg[4 + length:] 406 | if info_type in bmp_cons.INIT_MSG_INFOR_TYPE: 407 | msg_dict[bmp_cons.INIT_MSG_INFOR_TYPE[info_type]] = binascii.unhexlify(binascii.hexlify(info_value)) 408 | else: 409 | msg_dict[info_type] = binascii.unhexlify(binascii.hexlify(info_value)) 410 | LOG.info('unknow information type, type = %s' % info_type) 411 | LOG.info('initiation message = %s' % msg_dict) 412 | return msg_dict 413 | 414 | @staticmethod 415 | def parse_termination_msg(msg): 416 | """ 417 | The termination message provides a way for a monitored router to 418 | indicate why it is terminating a session. Although use of this 419 | message is RECOMMENDED, a monitoring station must always be prepared 420 | for the session to terminate with no message. Once the router has 421 | sent a termination message, it MUST close the TCP session without 422 | sending any further messages. Likewise, the monitoring station MUST 423 | close the TCP session after receiving a termination message. 424 | The termination message consists of the common BMP header followed by 425 | one or more TLVs containing information about the reason for the 426 | termination, as follows: 427 | :return: 428 | """ 429 | # 0 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 430 | # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 431 | # | Information Type | Information Length | 432 | # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 433 | # | Information (variable) #| 434 | # ~ ~ 435 | # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 436 | LOG.info('decode termination message') 437 | msg_dict = {} 438 | while msg: 439 | info_type, length = struct.unpack('!HH', msg[0:4]) 440 | info_value = msg[4: 4 + length] 441 | msg = msg[4 + length:] 442 | if info_type in bmp_cons.TERMI_MSG_INFOR_TYPE: 443 | msg_dict[bmp_cons.TERMI_MSG_INFOR_TYPE[info_type]] = binascii.unhexlify(binascii.hexlify(info_value)) 444 | else: 445 | msg_dict[info_type] = binascii.unhexlify(binascii.hexlify(info_value)) 446 | LOG.info('unknow information type, type = %s' % info_type) 447 | LOG.info('termination message = %s' % msg_dict) 448 | return msg_dict 449 | 450 | def consume(self): 451 | 452 | if self.msg_type in [0, 1, 2, 3, 6]: 453 | try: 454 | per_peer_header = self.parse_per_peer_header(self.raw_body[0:42]) 455 | self.msg_body = self.raw_body[42:] 456 | if self.msg_type == 0: 457 | return per_peer_header, self.parse_route_monitoring_msg(self.msg_body) 458 | elif self.msg_type == 1: 459 | return per_peer_header, self.parse_statistic_report_msg(self.msg_body) 460 | elif self.msg_type == 2: 461 | return per_peer_header, self.parse_peer_down_notification(self.msg_body) 462 | elif self.msg_type == 3: 463 | return per_peer_header, self.parse_peer_up_notification(self.msg_body, per_peer_header['flags']) 464 | elif self.msg_type == 6: 465 | return per_peer_header, self.parse_route_mirroring_msg(self.msg_body) 466 | except Exception as e: 467 | LOG.error(e) 468 | error_str = traceback.format_exc() 469 | LOG.debug(error_str) 470 | # can not decode this BMP message 471 | return None 472 | 473 | elif self.msg_type == 4: 474 | return None, self.parse_initiation_msg(self.raw_body) 475 | elif self.msg_type == 5: 476 | return None, self.parse_termination_msg(self.raw_body) 477 | --------------------------------------------------------------------------------