This adds some scenario tests using a scenario test tool. Signed-off-by: Fumihiko Kakuma <[email protected]> --- ryu/tests/integrated/bgp/__init__.py | 0 ryu/tests/integrated/bgp/base.py | 81 +++++++++++ ryu/tests/integrated/bgp/ryubgp_app.py | 238 +++++++++++++++++++++++++++++++++ ryu/tests/integrated/bgp/test_basic.py | 47 +++++++ ryu/tests/integrated/run_test.py | 50 +++++++ 5 files changed, 416 insertions(+) create mode 100644 ryu/tests/integrated/bgp/__init__.py create mode 100644 ryu/tests/integrated/bgp/base.py create mode 100644 ryu/tests/integrated/bgp/ryubgp_app.py create mode 100644 ryu/tests/integrated/bgp/test_basic.py create mode 100644 ryu/tests/integrated/run_test.py
diff --git a/ryu/tests/integrated/bgp/__init__.py b/ryu/tests/integrated/bgp/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ryu/tests/integrated/bgp/base.py b/ryu/tests/integrated/bgp/base.py new file mode 100644 index 0000000..d1e4331 --- /dev/null +++ b/ryu/tests/integrated/bgp/base.py @@ -0,0 +1,81 @@ +# Copyright (C) 2016 Nippon Telegraph and Telephone Corporation. +# Copyright (C) 2016 Fumihiko Kakuma <kakuma at valinux co jp> +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import absolute_import + +import unittest + +from ryu.tests.integrated.common import docker_base as ctn_base +from ryu.tests.integrated.common import ryubgp +from ryu.tests.integrated.common import quagga + + +class BgpSpeakerTestBase(unittest.TestCase): + + checktime = 120 + + @classmethod + def setUpClass(cls): + cls.images = [] + cls.containers = [] + cls.bridges = [] + + cls.brdc1 = ctn_base.Bridge(name='brdc1', + subnet='192.168.10.0/24') + cls.bridges.append(cls.brdc1) + + cls.dockerimg = ctn_base.DockerImage() + cls.r_img = cls.dockerimg.create_ryu() + cls.images.append(cls.r_img) + cls.q_img = cls.dockerimg.create_quagga() + cls.images.append(cls.q_img) + + cls.r1 = ryubgp.RyuBGPContainer(name='r1', asn=64512, + router_id='192.168.0.1', + ctn_image_name=cls.r_img) + cls.containers.append(cls.r1) + cls.r1.add_route('10.10.0.0/28') + cls.r1.run(wait=True) + cls.r1_ip_cidr = cls.brdc1.addif(cls.r1) + cls.r1_ip = cls.r1_ip_cidr.split('/')[0] + + cls.q1 = quagga.QuaggaBGPContainer(name='q1', asn=64522, + router_id='192.168.0.2', + ctn_image_name=cls.q_img) + cls.containers.append(cls.q1) + cls.q1.add_route('192.168.160.0/24') + cls.q1.run(wait=True) + cls.q1_ip_cidr = cls.brdc1.addif(cls.q1) + cls.q1_ip = cls.q1_ip_cidr.split('/')[0] + + cls.r1.add_peer(cls.q1, bridge=cls.brdc1.name) + cls.q1.add_peer(cls.r1, bridge=cls.brdc1.name) + + super(BgpSpeakerTestBase, cls).setUpClass() + + @classmethod + def tearDownClass(cls): + for ctn in cls.containers: + try: + ctn.stop() + except ctn_base.CommandError as e: + pass + ctn.remove() + for br in cls.bridges: + br.delete() + for img in cls.images: + cls.dockerimg.remove(img) + super(BgpSpeakerTestBase, cls).tearDownClass() diff --git a/ryu/tests/integrated/bgp/ryubgp_app.py b/ryu/tests/integrated/bgp/ryubgp_app.py new file mode 100644 index 0000000..7e364b1 --- /dev/null +++ b/ryu/tests/integrated/bgp/ryubgp_app.py @@ -0,0 +1,238 @@ +# Copyright (C) 2014 Nippon Telegraph and Telephone Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" + Defines bases classes to create a BGP application. +""" +from __future__ import absolute_import + +import imp +import logging +import traceback +from ryu import cfg + +from ryu.lib import hub +from ryu.base.app_manager import RyuApp + +from ryu.services.protocols.bgp.api.base import call +from ryu.services.protocols.bgp.base import add_bgp_error_metadata +from ryu.services.protocols.bgp.base import BGPSException +from ryu.services.protocols.bgp.base import BIN_ERROR +from ryu.services.protocols.bgp.core_manager import CORE_MANAGER +from ryu.services.protocols.bgp import net_ctrl +from ryu.services.protocols.bgp.rtconf.base import RuntimeConfigError +from ryu.services.protocols.bgp.rtconf.common import BGP_SERVER_PORT +from ryu.services.protocols.bgp.rtconf.common import DEFAULT_BGP_SERVER_PORT +from ryu.services.protocols.bgp.rtconf.common import \ + DEFAULT_REFRESH_MAX_EOR_TIME +from ryu.services.protocols.bgp.rtconf.common import \ + DEFAULT_REFRESH_STALEPATH_TIME +from ryu.services.protocols.bgp.rtconf.common import DEFAULT_LABEL_RANGE +from ryu.services.protocols.bgp.rtconf.common import LABEL_RANGE +from ryu.services.protocols.bgp.rtconf.common import LOCAL_AS +from ryu.services.protocols.bgp.rtconf.common import REFRESH_MAX_EOR_TIME +from ryu.services.protocols.bgp.rtconf.common import REFRESH_STALEPATH_TIME +from ryu.services.protocols.bgp.rtconf.common import ROUTER_ID +from ryu.services.protocols.bgp.rtconf import neighbors +from ryu.services.protocols.bgp.rtconf import vrfs +from ryu.services.protocols.bgp.utils.validation import is_valid_ipv4 +from ryu.services.protocols.bgp.operator import ssh + +try: + from logging.config import dictConfig +except Exception: + from ryu.services.protocols.bgp.utils.dictconfig import dictConfig + + +LOG = logging.getLogger(__name__) +CONF = cfg.CONF + +CONF.register_opts([ + cfg.IntOpt('bind-port', default=50002, help='rpc-port'), + cfg.StrOpt('bind-ip', default='0.0.0.0', help='rpc-bind-ip'), + cfg.StrOpt('bgp-config-file', default=None, + help='bgp-config-file') +]) + + +@add_bgp_error_metadata(code=BIN_ERROR, + sub_code=1, + def_desc='Unknown bootstrap exception.') +class ApplicationException(BGPSException): + """Specific Base exception related to `BSPSpeaker`.""" + pass + + +class RyuBGPSpeaker(RyuApp): + def __init__(self, *args, **kwargs): + self.bind_ip = RyuBGPSpeaker.validate_rpc_ip(CONF.bind_ip) + self.bind_port = RyuBGPSpeaker.validate_rpc_port(CONF.bind_port) + self.config_file = CONF.bgp_config_file + super(RyuBGPSpeaker, self).__init__(*args, **kwargs) + + def start(self): + # Only two main green threads are required for APGW bgp-agent. + # One for NetworkController, another for BGPS core. + + # If configuration file was provided and loaded successfully. We start + # BGPS core using these settings. If no configuration file is provided + # or if configuration file is missing minimum required settings BGPS + # core is not started. + if self.config_file: + LOG.debug('Loading config. from settings file.') + settings = self.load_config(self.config_file) + # Configure log settings, if available. + if getattr(settings, 'LOGGING', None): + dictConfig(settings.LOGGING) + + if getattr(settings, 'BGP', None): + self._start_core(settings) + + if getattr(settings, 'SSH', None) is not None: + hub.spawn(ssh.SSH_CLI_CONTROLLER.start, None, **settings.SSH) + # Start Network Controller to server RPC peers. + t = hub.spawn(net_ctrl.NET_CONTROLLER.start, *[], + **{net_ctrl.NC_RPC_BIND_IP: self.bind_ip, + net_ctrl.NC_RPC_BIND_PORT: self.bind_port}) + LOG.debug('Started Network Controller') + + super(RyuBGPSpeaker, self).start() + + return t + + @classmethod + def validate_rpc_ip(cls, ip): + """Validates given ip for use as rpc host bind address. + """ + if not is_valid_ipv4(ip): + raise ApplicationException(desc='Invalid rpc ip address.') + return ip + + @classmethod + def validate_rpc_port(cls, port): + """Validates give port for use as rpc server port. + """ + if not port: + raise ApplicationException(desc='Invalid rpc port number.') + if isinstance(port, str): + port = int(port) + + return port + + def load_config(self, config_file): + """Validates give file as settings file for BGPSpeaker. + + Load the configuration from file as settings module. + """ + if not config_file or not isinstance(config_file, str): + raise ApplicationException('Invalid configuration file.') + + # Check if file can be read + try: + return imp.load_source('settings', config_file) + except Exception as e: + raise ApplicationException(desc=str(e)) + + def _start_core(self, settings): + """Starts BGPS core using setting and given pool. + """ + # Get common settings + routing_settings = settings.BGP.get('routing') + common_settings = {} + + # Get required common settings. + try: + common_settings[LOCAL_AS] = routing_settings.pop(LOCAL_AS) + common_settings[ROUTER_ID] = routing_settings.pop(ROUTER_ID) + except KeyError as e: + raise ApplicationException( + desc='Required minimum configuration missing %s' % + e) + + # Get optional common settings + common_settings[BGP_SERVER_PORT] = \ + routing_settings.get(BGP_SERVER_PORT, DEFAULT_BGP_SERVER_PORT) + common_settings[REFRESH_STALEPATH_TIME] = \ + routing_settings.get(REFRESH_STALEPATH_TIME, + DEFAULT_REFRESH_STALEPATH_TIME) + common_settings[REFRESH_MAX_EOR_TIME] = \ + routing_settings.get(REFRESH_MAX_EOR_TIME, + DEFAULT_REFRESH_MAX_EOR_TIME) + common_settings[LABEL_RANGE] = \ + routing_settings.get(LABEL_RANGE, DEFAULT_LABEL_RANGE) + + # Start BGPS core service + waiter = hub.Event() + call('core.start', waiter=waiter, **common_settings) + waiter.wait() + + LOG.debug('Core started %s', CORE_MANAGER.started) + # Core manager started add configured neighbor and vrfs + if CORE_MANAGER.started: + # Add neighbors. + self._add_neighbors(routing_settings) + + # Add Vrfs. + self._add_vrfs(routing_settings) + + # Add Networks + self._add_networks(routing_settings) + + def _add_neighbors(self, routing_settings): + """Add bgp peers/neighbors from given settings to BGPS runtime. + + All valid neighbors are loaded. Miss-configured neighbors are ignored + and error is logged. + """ + bgp_neighbors = routing_settings.setdefault('bgp_neighbors', {}) + for ip, bgp_neighbor in bgp_neighbors.items(): + try: + bgp_neighbor[neighbors.IP_ADDRESS] = ip + call('neighbor.create', **bgp_neighbor) + LOG.debug('Added neighbor %s', ip) + except RuntimeConfigError as re: + LOG.error(re) + LOG.error(traceback.format_exc()) + continue + + def _add_vrfs(self, routing_settings): + """Add VRFs from given settings to BGPS runtime. + + If any of the VRFs are miss-configured errors are logged. + All valid VRFs are loaded. + """ + vpns_conf = routing_settings.setdefault('vpns', {}) + for vrfname, vrf in vpns_conf.items(): + try: + vrf[vrfs.VRF_NAME] = vrfname + call('vrf.create', **vrf) + LOG.debug('Added vrf %s', vrf) + except RuntimeConfigError as e: + LOG.error(e) + continue + + def _add_networks(self, routing_settings): + """Add networks from given settings to BGPS runtime. + + If any of the networks are miss-configured errors are logged. + All valid networks are loaded. + """ + networks = routing_settings.setdefault('networks', []) + for prefix in networks: + try: + call('network.add', prefix=prefix) + LOG.debug('Added network %s', prefix) + except RuntimeConfigError as e: + LOG.error(e) + continue diff --git a/ryu/tests/integrated/bgp/test_basic.py b/ryu/tests/integrated/bgp/test_basic.py new file mode 100644 index 0000000..6321191 --- /dev/null +++ b/ryu/tests/integrated/bgp/test_basic.py @@ -0,0 +1,47 @@ +# Copyright (C) 2016 Nippon Telegraph and Telephone Corporation. +# Copyright (C) 2016 Fumihiko Kakuma <kakuma at valinux co jp> +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import absolute_import + +import time + +from . import base +from ryu.tests.integrated.common import docker_base as ctn_base + + +class BgpSpeakerBasicTest(base.BgpSpeakerTestBase): + def setUp(self): + super(BgpSpeakerBasicTest, self).setUp() + self.r1.stop_ryubgp(retry=True) + self.r1.start_ryubgp(retry=True) + + def test_check_neighbor_established(self): + for i in range(0, self.checktime): + neighbor_state = self.q1.get_neighbor_state(self.r1) + if neighbor_state == ctn_base.BGP_FSM_ESTABLISHED: + break + time.sleep(1) + self.assertEqual(neighbor_state, ctn_base.BGP_FSM_ESTABLISHED) + + def test_check_rib_nexthop(self): + for i in range(0, self.checktime): + neighbor_state = self.q1.get_neighbor_state(self.r1) + if neighbor_state == ctn_base.BGP_FSM_ESTABLISHED: + break + time.sleep(1) + self.assertEqual(neighbor_state, ctn_base.BGP_FSM_ESTABLISHED) + rib = self.q1.get_global_rib(prefix='10.10.0.0/28') + self.assertEqual(self.r1_ip, rib[0]['nexthop']) diff --git a/ryu/tests/integrated/run_test.py b/ryu/tests/integrated/run_test.py new file mode 100644 index 0000000..c7001d0 --- /dev/null +++ b/ryu/tests/integrated/run_test.py @@ -0,0 +1,50 @@ +# Copyright (C) 2016 Nippon Telegraph and Telephone Corporation. +# Copyright (C) 2016 Fumihiko Kakuma <kakuma at valinux co jp> +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import absolute_import + +import logging +import os +import sys +import unittest + +from ryu import log + + +def load_tests(loader, tests, pattern): + dirname = os.path.dirname(os.path.abspath(__file__)) + base_path = os.path.abspath(dirname + '/../../..') + suite = unittest.TestSuite() + for test_dir in ['ryu/tests/integrated/bgp']: + if not pattern: + suite.addTests(loader.discover(test_dir, + top_level_dir=base_path)) + else: + suite.addTests(loader.discover(test_dir, pattern=pattern, + top_level_dir=base_path)) + return suite + + +if __name__ == '__main__': + log.early_init_log(logging.DEBUG) + log.init_log() + LOG = logging.getLogger(__name__) + pattern = None + if len(sys.argv) == 2: + pattern = sys.argv[1] + loader = unittest.defaultTestLoader + suite = load_tests(loader, None, pattern) + unittest.TextTestRunner(verbosity=2).run(suite) -- 1.9.1 ------------------------------------------------------------------------------ Check out the vibrant tech community on one of the world's most engaging tech sites, SlashDot.org! http://sdm.link/slashdot _______________________________________________ Ryu-devel mailing list [email protected] https://lists.sourceforge.net/lists/listinfo/ryu-devel
