diff options
-rw-r--r-- | test/scenario_test/bgp_router_test.py | 453 | ||||
-rw-r--r-- | test/scenario_test/lib/__init__.py | 0 | ||||
-rw-r--r-- | test/scenario_test/lib/bagpipe.py | 73 | ||||
-rw-r--r-- | test/scenario_test/lib/base.py | 266 | ||||
-rw-r--r-- | test/scenario_test/lib/exabgp.py | 99 | ||||
-rw-r--r-- | test/scenario_test/lib/gobgp.py | 175 | ||||
-rw-r--r-- | test/scenario_test/lib/quagga.py | 193 | ||||
-rw-r--r-- | test/scenario_test/noseplugin.py | 5 | ||||
-rw-r--r-- | test/scenario_test/route_server_ipv4_v6_test.py | 2 | ||||
-rw-r--r-- | test/scenario_test/route_server_malformed_test.py | 2 | ||||
-rw-r--r-- | test/scenario_test/route_server_policy_test.py | 2 | ||||
-rw-r--r-- | test/scenario_test/route_server_test.py | 2 |
12 files changed, 1015 insertions, 257 deletions
diff --git a/test/scenario_test/bgp_router_test.py b/test/scenario_test/bgp_router_test.py index f965d224..a424d625 100644 --- a/test/scenario_test/bgp_router_test.py +++ b/test/scenario_test/bgp_router_test.py @@ -1,4 +1,4 @@ -# Copyright (C) 2014 Nippon Telegraph and Telephone Corporation. +# Copyright (C) 2015 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. @@ -13,273 +13,224 @@ # See the License for the specific language governing permissions and # limitations under the License. -import time +import unittest +from fabric.api import local +from lib.gobgp import * +from lib.quagga import * import sys +import os +import time import nose -import quagga_access as qaccess -import docker_control as fab -from gobgp_test import GoBGPTestBase -from gobgp_test import ADJ_RIB_OUT, GLOBAL_RIB -from gobgp_test import NEIGHBOR -from noseplugin import OptionParser -from noseplugin import parser_option - -class GoBGPTest(GoBGPTestBase): - quagga_num = 3 - append_quagga = 10 - remove_quagga = 10 - append_quagga_best = 20 - - def __init__(self, *args, **kwargs): - super(GoBGPTest, self).__init__(*args, **kwargs) +from noseplugin import OptionParser, parser_option + + +class GoBGPTestBase(unittest.TestCase): + + wait_per_retry = 5 + retry_limit = 15 + + @classmethod + def setUpClass(cls): + gobgp_ctn_image_name = 'osrg/gobgp' + if parser_option.use_local: + make_gobgp_ctn() + gobgp_ctn_image_name = 'gobgp' + + g1 = GoBGPContainer(name='g1', asn=65000, router_id='192.168.0.1', + ctn_image_name=gobgp_ctn_image_name, + log_level=parser_option.gobgp_log_level) + q1 = QuaggaBGPContainer(name='q1', asn=65001, router_id='192.168.0.2') + q2 = QuaggaBGPContainer(name='q2', asn=65002, router_id='192.168.0.3') + q3 = QuaggaBGPContainer(name='q3', asn=65003, router_id='192.168.0.4') + + qs = [q1, q2, q3] + ctns = [g1, q1, q2, q3] + + # advertise a route from q1, q2, q3 + for idx, q in enumerate(qs): + route = '10.0.{0}.0/24'.format(idx+1) + q.add_route(route) + + initial_wait_time = max(ctn.run() for ctn in ctns) + + time.sleep(initial_wait_time) + + br01 = Bridge(name='br01', subnet='192.168.10.0/24') + [br01.addif(ctn) for ctn in ctns] + + for q in qs: + g1.add_peer(q) + q.add_peer(g1) + + cls.gobgp = g1 + cls.quaggas = {'q1': q1, 'q2': q2, 'q3': q3} + cls.bridges = {'br01': br01} # test each neighbor state is turned establish def test_01_neighbor_established(self): - print "test_neighbor_established" - - use_local = parser_option.use_local - go_path = parser_option.go_path - log_debug = parser_option.gobgp_log_debug - fab.init_test_env_executor(self.quagga_num, use_local, go_path, log_debug, is_route_server=False) - - print "please wait " + str(self.initial_wait_time) + " second" - time.sleep(self.initial_wait_time) - if self.check_load_config() is False: - return - - addresses = self.get_neighbor_address(self.gobgp_config) - self.retry_routine_for_state(addresses, "BGP_FSM_ESTABLISHED") - - for address in addresses: - # get neighbor state and remote ip from gobgp connections - print "check of [ " + address + " ]" - neighbor = self.ask_gobgp(NEIGHBOR, address) - state = neighbor['info']['bgp_state'] - remote_ip = neighbor['conf']['remote_ip'] - self.assertEqual(address, remote_ip) - self.assertEqual(state, "BGP_FSM_ESTABLISHED") - - # Test of advertised route gobgp from each quagga - - - def test_02_received_route(self): - print "test_received_route" - if self.check_load_config() is False: - return - - self.assert_global_rib() - - # Test of advertising route to each quagga form gobgp - def test_03_advertising_route(self): - print "test_advertising_route" - if self.check_load_config() is False: - return - - for address in self.get_neighbor_address(self.gobgp_config): - print "check of [ " + address + " ]" - rib = self.ask_gobgp(ADJ_RIB_OUT, address) - print rib - self.assert_quagga_rib(address) - - # check if quagga that is appended can establish connection with gobgp - def test_04_established_with_appended_quagga(self): - print "test_established_with_appended_quagga" - if self.check_load_config() is False: - return - - go_path = parser_option.go_path - # append new quagga container - fab.docker_container_quagga_append_executor(self.append_quagga, go_path, is_route_server=False) - print "please wait " + str(self.initial_wait_time) + " second" - time.sleep(self.initial_wait_time) - append_quagga_address = "10.0.0." + str(self.append_quagga) - self.retry_routine_for_state([append_quagga_address], "BGP_FSM_ESTABLISHED") - - # get neighbor state and remote ip of new quagga - print "check of [" + append_quagga_address + " ]" - neighbor = self.ask_gobgp(NEIGHBOR, append_quagga_address) - state = neighbor['info']['bgp_state'] - remote_ip = neighbor['conf']['remote_ip'] - self.assertEqual(append_quagga_address, remote_ip) - self.assertEqual(state, "BGP_FSM_ESTABLISHED") - - # Test of advertised route gobgp from each quagga when append quagga container - def test_05_received_route_when_appended_quagga(self): - print "test_received_route_by_appended_quagga" - if self.check_load_config() is False: - return - - self.assert_global_rib() - - # Test of advertising route to each quagga form gobgp when append quagga container - def test_06_advertising_route_when_appended_quagga(self): - print "test_advertising_route_to_appended_quagga" - if self.check_load_config() is False: - return - - for address in self.get_neighbor_address(self.gobgp_config): - print "check of [ " + address + " ]" - self.assert_quagga_rib(address) - - def test_07_active_when_quagga_removed(self): - print "test_active_when_removed_quagga" - if self.check_load_config() is False: - return - - # remove quagga container - fab.docker_container_quagga_removed_executor(self.remove_quagga) - print "please wait " + str(self.initial_wait_time) + " second" - time.sleep(self.initial_wait_time) - removed_quagga_address = "10.0.0." + str(self.remove_quagga) - self.retry_routine_for_state([removed_quagga_address], "BGP_FSM_ACTIVE") - - # get neighbor state and remote ip of removed quagga - print "check of [" + removed_quagga_address + " ]" - neighbor = self.ask_gobgp(NEIGHBOR, removed_quagga_address) - state = neighbor['info']['bgp_state'] - remote_ip = neighbor['conf']['remote_ip'] - self.assertEqual(removed_quagga_address, remote_ip) - self.assertEqual(state, "BGP_FSM_ACTIVE") - - def test_08_received_route_when_quagga_removed(self): - print "test_received_route_when_removed_quagga" - if self.check_load_config() is False: - return - - retry_count = 0 - still_exists = False - while retry_count < self.dest_check_limit: - - rib = self.ask_gobgp(GLOBAL_RIB) - - removed_prefix = "10.0.0.%d/24" % self.remove_quagga - still_exists = False - for dst in rib: - for path in dst['paths']: - if path['nlri']['prefix'] == removed_prefix: - still_exists = True - - if not still_exists: - print "compare OK" - break - else: - retry_count += 1 - print "compare NG -> retry ( %d / %d )" % (retry_count, self.dest_check_limit) - time.sleep(self.wait_per_retry) - - self.assertEqual(still_exists, False) - - def test_09_advertising_route_when_quagga_removed(self): - print "test_advertising_route_when_removed_quagga" - if self.check_load_config() is False: - return - - remove_quagga_address = "10.0.0." + str(self.remove_quagga) - removed_prefix = "10.0.0.%d/24" % self.remove_quagga - for address in self.get_neighbor_address(self.gobgp_config): - if remove_quagga_address == address: + for q in self.quaggas.itervalues(): + self.gobgp.wait_for(expected_state=BGP_FSM_ESTABLISHED, peer=q) + + def test_02_check_gobgp_global_rib(self): + for q in self.quaggas.itervalues(): + # paths expected to exist in gobgp's global rib + routes = q.routes.keys() + # gobgp's global rib + global_rib = [p['prefix'] for p in self.gobgp.get_global_rib()] + + for p in global_rib: + if p in routes: + routes.remove(p) + + if len(routes) == 0: continue - print "check of [ " + address + " ]" + gobgp = '/go/bin/gobgp' + cmd = 'docker exec {0} {1}' \ + ' monitor global rib -j'.format(self.gobgp.name, gobgp) + + process = subprocess.Popen(cmd, shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + + poll = select.epoll() + poll.register(process.stdout, select.POLLIN) - retry_count = 0 - cmp_result = False - while retry_count < self.dest_check_limit: + timeout = 10.0 - tn = qaccess.login(address) - q_rib = qaccess.show_rib(tn) - still_exists = False - for q_path in q_rib: - if q_path['Network'] == removed_prefix: - still_exists = True - #self.assertEqual(still_exists, False) + while True: + result = poll.poll(timeout) + if result: + line = process.stdout.readline() + path = json.loads(line)['nlri']['prefix'] + if path in routes: + routes.remove(path) - cmp_result = self.compare_route_with_quagga_configs(address, q_rib, route_server=False) + if len(routes) == 0: + return + continue + raise Exception('timeout') - if cmp_result and not still_exists: - print "compare OK" + + # check routes are properly advertised to all BGP speaker + def test_03_check_quagga_global_rib(self): + for q in self.quaggas.itervalues(): + done = False + for _ in range(self.retry_limit): + if done: break - else: - retry_count += 1 - print "compare NG -> retry ( %d / %d )" % (retry_count, self.dest_check_limit) + global_rib = q.get_global_rib() + global_rib = [p['prefix'] for p in global_rib] + if len(global_rib) < len(self.quaggas): time.sleep(self.wait_per_retry) + continue + + self.assertTrue(len(global_rib) == len(self.quaggas)) + + for c in self.quaggas.itervalues(): + for r in c.routes: + self.assertTrue(r in global_rib) + done = True + if done: + continue + # should not reach here + self.assertTrue(False) + + def test_04_add_quagga(self): + q4 = QuaggaBGPContainer(name='q4', asn=65004, router_id='192.168.0.5') + self.quaggas['q4'] = q4 + + q4.add_route('10.0.4.0/24') + + initial_wait_time = q4.run() + time.sleep(initial_wait_time) + self.bridges['br01'].addif(q4) + self.gobgp.add_peer(q4) + q4.add_peer(self.gobgp) + + self.gobgp.wait_for(expected_state=BGP_FSM_ESTABLISHED, peer=q4) + + def test_05_check_global_rib(self): + self.test_02_check_gobgp_global_rib() + self.test_03_check_quagga_global_rib() + + def test_06_stop_one_quagga(self): + q4 = self.quaggas['q4'] + q4.stop() + self.gobgp.wait_for(expected_state=BGP_FSM_ACTIVE, peer=q4) + del self.quaggas['q4'] + + # check gobgp properly send withdrawal message with q4's route + def test_07_check_global_rib(self): + self.test_02_check_gobgp_global_rib() + self.test_03_check_quagga_global_rib() + + def test_08_add_distant_relative(self): + q1 = self.quaggas['q1'] + q2 = self.quaggas['q2'] + q3 = self.quaggas['q3'] + q5 = QuaggaBGPContainer(name='q5', asn=65005, router_id='192.168.0.6') + + initial_wait_time = q5.run() + time.sleep(initial_wait_time) + + br02 = Bridge(name='br02', subnet='192.168.20.0/24') + br02.addif(q5) + br02.addif(q2) + + br03 = Bridge(name='br03', subnet='192.168.30.0/24') + br03.addif(q5) + br03.addif(q3) + + for q in [q2, q3]: + q5.add_peer(q) + q.add_peer(q5) + + med200 = {'name': 'med200', + 'type': 'permit', + 'match': '0.0.0.0/0', + 'direction': 'out', + 'med': 200} + q2.add_policy(med200, self.gobgp) + med100 = {'name': 'med100', + 'type': 'permit', + 'match': '0.0.0.0/0', + 'direction': 'out', + 'med': 100} + q3.add_policy(med100, self.gobgp) + + q5.add_route('10.0.6.0/24') + + self.gobgp.wait_for(expected_state=BGP_FSM_ESTABLISHED, peer=q2) + self.gobgp.wait_for(expected_state=BGP_FSM_ESTABLISHED, peer=q3) + q2.wait_for(expected_state=BGP_FSM_ESTABLISHED, peer=q5) + q3.wait_for(expected_state=BGP_FSM_ESTABLISHED, peer=q5) + + done = False + for _ in range(self.retry_limit): + paths = q1.get_global_rib('10.0.6.0/24') + if len(paths) > 0: + path = paths[0] + print "{0}'s nexthop is {1}".format(path['prefix'], + path['nexthop']) + n_addrs = [i[1].split('/')[0] for i in self.gobgp.ip_addrs] + if path['nexthop'] in n_addrs: + done = True + break + time.sleep(self.wait_per_retry) + + if not done: + self.assertTrue(False) - self.assertEqual(still_exists, False) - self.assertEqual(cmp_result, True) - - def test_10_bestpath_selection_of_received_route(self): - print "test_bestpath_selection_of_received_route" - if self.check_load_config() is False: - return - - go_path = parser_option.go_path - fab.docker_container_make_bestpath_env_executor(self.append_quagga_best, go_path, is_route_server=False) - print "please wait " + str(self.initial_wait_time) + " second" - time.sleep(self.initial_wait_time) - - print "add neighbor setting" - tn = qaccess.login("11.0.0.20") - qaccess.add_neighbor(tn, "65020", "11.0.0.2", "65002") - qaccess.add_neighbor(tn, "65020", "12.0.0.3", "65003") - - tn = qaccess.login("11.0.0.2") - tn = qaccess.add_metric(tn, "200", "192.168.20.0") - qaccess.add_neighbor(tn, "65002", "11.0.0.20", "65020") - qaccess.add_neighbor_metric(tn, "65002", "10.0.255.1", "200") - - tn = qaccess.login("10.0.0.3") - tn = qaccess.add_metric(tn, "100", "192.168.20.0") - qaccess.add_neighbor(tn, "65003", "12.0.0.20", "65020") - qaccess.add_neighbor_metric(tn, "65003", "10.0.255.1", "100") - - print "please wait " + str(self.initial_wait_time) + " second" - time.sleep(self.initial_wait_time) - - target_network = "192.168.20.0/24" - ans_nexthop = "10.0.0.3" - - print "check whether target network %s 's nexthop is %s" % (target_network, ans_nexthop) - self.retry_routine_for_bestpath("", target_network, ans_nexthop) - - def assert_quagga_rib(self, address): - retry_count = 0 - cmp_result = False - while retry_count < self.dest_check_limit: - tn = qaccess.login(address) - q_rib = qaccess.show_rib(tn) - cmp_result = self.compare_route_with_quagga_configs(address, q_rib, route_server=False) - - if cmp_result: - print "compare OK" - break - else: - retry_count += 1 - print "compare NG -> retry ( %d / %d )" % (retry_count, self.dest_check_limit) - time.sleep(self.wait_per_retry) - self.assertTrue(cmp_result) - - def assert_global_rib(self): - retry_count = 0 - cmp_result = False - while retry_count < self.dest_check_limit: - rib = self.ask_gobgp(GLOBAL_RIB) - cmp_result = self.compare_global_rib_with_quagga_configs(rib) - - if cmp_result: - print "compare OK" - break - else: - retry_count += 1 - print "compare NG -> retry ( %d / %d )" % (retry_count, self.dest_check_limit) - time.sleep(self.wait_per_retry) - self.assertTrue(cmp_result) if __name__ == '__main__': - if fab.test_user_check() is False: + if os.geteuid() is not 0: print "you are not root." sys.exit(1) - if fab.docker_pkg_check() is False: - print "not install docker package." + output = local("which docker 2>&1 > /dev/null ; echo $?", capture=True) + if int(output) is not 0: + print "docker not found" sys.exit(1) - nose.main(argv=sys.argv, addplugins=[OptionParser()], defaultTest=sys.argv[0]) + nose.main(argv=sys.argv, addplugins=[OptionParser()], + defaultTest=sys.argv[0]) diff --git a/test/scenario_test/lib/__init__.py b/test/scenario_test/lib/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/test/scenario_test/lib/__init__.py diff --git a/test/scenario_test/lib/bagpipe.py b/test/scenario_test/lib/bagpipe.py new file mode 100644 index 00000000..c73b6472 --- /dev/null +++ b/test/scenario_test/lib/bagpipe.py @@ -0,0 +1,73 @@ +# Copyright (C) 2015 Nippon Telegraph and Telephone Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from base import * + + +class BagpipeContainer(BGPContainer): + + SHARED_VOLUME = '/root/shared_volume' + + def __init__(self, name, asn, router_id, + ctn_image_name='yoshima/bagpipe-bgp'): + super(BagpipeContainer, self).__init__(name, asn, router_id, + ctn_image_name) + self.shared_volumes.append((self.config_dir, self.SHARED_VOLUME)) + + def run(self): + super(BagpipeContainer, self).run() + cmd = CmdBuffer(' ') + cmd << 'docker exec' + cmd << '{0} cp {1}/bgp.conf'.format(self.name, self.SHARED_VOLUME) + cmd << '/etc/bagpipe-bgp/' + local(str(cmd), capture=True) + cmd = 'docker exec {0} service bagpipe-bgp start'.format(self.name) + local(cmd, capture=True) + + def create_config(self): + c = CmdBuffer() + c << '[BGP]' + if len(self.ip_addrs) > 0: + c << 'local_address={0}'.format(self.ip_addrs[0][1].split('/')[0]) + for peer, info in self.peers.iteritems(): + c << 'peers={0}'.format(info['neigh_addr'].split('/')[0]) + c << 'my_as={0}'.format(self.asn) + c << 'enable_rtc=True' + c << '[API]' + c << 'api_host=localhost' + c << 'api_port=8082' + c << '[DATAPLANE_DRIVER_IPVPN]' + c << 'dataplane_driver = DummyDataplaneDriver' + c << '[DATAPLANE_DRIVER_EVPN]' + c << 'dataplane_driver = DummyDataplaneDriver' + + with open('{0}/bgp.conf'.format(self.config_dir), 'w') as f: + print colors.yellow(str(c)) + f.writelines(str(c)) + + def reload_config(self): + cmd = CmdBuffer(' ') + cmd << 'docker exec' + cmd << '{0} cp {1}/bgp.conf'.format(self.name, self.SHARED_VOLUME) + cmd << '/etc/bagpipe-bgp/' + local(str(cmd), capture=True) + cmd = 'docker exec {0} service bagpipe-bgp restart'.format(self.name) + local(cmd, capture=True) + + def pipework(self, bridge, ip_addr, intf_name=""): + super(BagpipeContainer, self).pipework(bridge, ip_addr, intf_name) + self.create_config() + if self.is_running: + self.reload_config() diff --git a/test/scenario_test/lib/base.py b/test/scenario_test/lib/base.py new file mode 100644 index 00000000..35686973 --- /dev/null +++ b/test/scenario_test/lib/base.py @@ -0,0 +1,266 @@ +# Copyright (C) 2015 Nippon Telegraph and Telephone Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from fabric.api import local, lcd +from fabric import colors +from fabric.utils import indent + +import netaddr +import os +import time +import itertools + +DEFAULT_TEST_BASE_DIR = '/tmp/gobgp' +TEST_BASE_DIR = DEFAULT_TEST_BASE_DIR + +BGP_FSM_IDLE = 'BGP_FSM_IDLE' +BGP_FSM_ACTIVE = 'BGP_FSM_ACTIVE' +BGP_FSM_ESTABLISHED = 'BGP_FSM_ESTABLISHED' + + +def get_bridges(): + return local("brctl show | awk 'NR > 1{print $1}'", + capture=True).split('\n') + + +def get_containers(): + return local("docker ps -a | awk 'NR > 1 {print $NF}'", + capture=True).split('\n') + + +class CmdBuffer(list): + def __init__(self, delim='\n'): + super(CmdBuffer, self).__init__() + self.delim = delim + + def __lshift__(self, value): + self.append(value) + + def __str__(self): + return self.delim.join(self) + + +def make_gobgp_ctn(tag='gobgp', local_gobgp_path=''): + if local_gobgp_path == '': + local_gobgp_path = os.getcwd() + + c = CmdBuffer() + c << 'FROM osrg/gobgp' + c << 'COPY gobgp /go/src/github.com/osrg/gobgp/' + c << 'RUN go get github.com/osrg/gobgp/gobgpd' + c << 'RUN go install -a github.com/osrg/gobgp/gobgpd' + c << 'RUN go get github.com/osrg/gobgp/gobgp' + c << 'RUN go install -a github.com/osrg/gobgp/gobgp' + + rindex = local_gobgp_path.rindex('gobgp') + if rindex < 0: + raise Exception('{0} seems not gobgp dir'.format(local_gobgp_path)) + + workdir = local_gobgp_path[:rindex] + with lcd(workdir): + local('echo \'{0}\' > Dockerfile'.format(str(c))) + local('docker build -t {0} .'.format(tag)) + local('rm Dockerfile') + + +class Bridge(object): + def __init__(self, name, subnet='', with_ip=True): + self.name = name + self.with_ip = with_ip + if with_ip: + self.subnet = netaddr.IPNetwork(subnet) + + def f(): + for host in self.subnet: + yield host + self._ip_generator = f() + # throw away first network address + self.next_ip_address() + + if self.name in get_bridges(): + self.delete() + + local("ip link add {0} type bridge".format(self.name), capture=True) + local("ip link set up dev {0}".format(self.name), capture=True) + + if with_ip: + self.ip_addr = self.next_ip_address() + local("ip addr add {0} dev {1}".format(self.ip_addr, self.name), + capture=True) + + self.ctns = [] + + def next_ip_address(self): + return "{0}/{1}".format(self._ip_generator.next(), + self.subnet.prefixlen) + + def addif(self, ctn, name=''): + if name == '': + name = self.name + self.ctns.append(ctn) + if self.with_ip: + ctn.pipework(self, self.next_ip_address(), name) + else: + ctn.pipework(self, '0/0', name) + + def delete(self): + local("ip link set down dev {0}".format(self.name), capture=True) + local("ip link delete {0} type bridge".format(self.name), capture=True) + + +class Container(object): + def __init__(self, name, image): + self.name = name + self.image = image + self.shared_volumes = [] + self.ip_addrs = [] + self.is_running = False + + if self.name in get_containers(): + self.stop() + + def run(self): + c = CmdBuffer(' ') + c << "docker run --privileged=true" + for sv in self.shared_volumes: + c << "-v {0}:{1}".format(sv[0], sv[1]) + c << "--name {0} -id {1}".format(self.name, self.image) + self.id = local(str(c), capture=True) + self.is_running = True + self.local("ip li set up dev lo") + return 0 + + def stop(self): + ret = local("docker rm -f " + self.name, capture=True) + self.is_running = False + return ret + + def pipework(self, bridge, ip_addr, intf_name=""): + if not self.is_running: + print colors.yellow('call run() before pipeworking') + return + c = CmdBuffer(' ') + c << "pipework {0}".format(bridge.name) + + if intf_name != "": + c << "-i {0}".format(intf_name) + else: + intf_name = "eth1" + c << "{0} {1}".format(self.name, ip_addr) + self.ip_addrs.append((intf_name, ip_addr, bridge)) + return local(str(c), capture=True) + + def local(self, cmd): + return local("docker exec -it {0} {1}".format(self.name, cmd)) + + +class BGPContainer(Container): + + WAIT_FOR_BOOT = 0 + RETRY_INTERVAL = 5 + + def __init__(self, name, asn, router_id, ctn_image_name): + self.config_dir = "{0}/{1}".format(TEST_BASE_DIR, name) + local('if [ -e {0} ]; then rm -r {0}; fi'.format(self.config_dir)) + local('mkdir -p {0}'.format(self.config_dir)) + self.asn = asn + self.router_id = router_id + self.peers = {} + self.routes = {} + self.policies = {} + super(BGPContainer, self).__init__(name, ctn_image_name) + + def run(self): + self.create_config() + super(BGPContainer, self).run() + return self.WAIT_FOR_BOOT + + def add_peer(self, peer, passwd='', evpn=False, is_rs_client=False, + policies=None, passive=False, + is_rr_client=False, cluster_id=''): + neigh_addr = '' + for me, you in itertools.product(self.ip_addrs, peer.ip_addrs): + if me[2] == you[2]: + neigh_addr = you[1] + + if neigh_addr == '': + raise Exception('peer {0} seems not ip reachable'.format(peer)) + + if not policies: + policies = [] + + self.peers[peer] = {'neigh_addr': neigh_addr, + 'passwd': passwd, + 'evpn': evpn, + 'is_rs_client': is_rs_client, + 'is_rr_client': is_rr_client, + 'cluster_id': cluster_id, + 'policies': policies, + 'passive': passive} + if self.is_running: + self.create_config() + self.reload_config() + + def del_peer(self, peer): + del self.peers[peer] + if self.is_running: + self.create_config() + self.reload_config() + + def add_route(self, route, attribute=''): + self.routes[route] = attribute + if self.is_running: + self.create_config() + self.reload_config() + + def add_policy(self, policy, peer=None): + self.policies[policy['name']] = policy + if peer in self.peers: + self.peers[peer]['policies'].append(policy) + if self.is_running: + self.create_config() + self.reload_config() + + def get_local_rib(self, peer, rf): + raise Exception('implement get_local_rib() method') + + def get_global_rib(self, rf): + raise Exception('implement get_global_rib() method') + + def get_neighbor_state(self, peer_id): + raise Exception('implement get_neighbor() method') + + def wait_for(self, expected_state, peer, timeout=10): + interval = 1 + count = 0 + while True: + state = self.get_neighbor_state(peer) + y = colors.yellow + print y("{0}'s peer {1} state: {2}".format(self.router_id, + peer.router_id, + state)) + if state == expected_state: + return + + time.sleep(interval) + count += interval + if count >= timeout: + raise Exception('timeout') + + def create_config(self): + raise Exception('implement create_config() method') + + def reload_config(self): + raise Exception('implement reload_config() method') diff --git a/test/scenario_test/lib/exabgp.py b/test/scenario_test/lib/exabgp.py new file mode 100644 index 00000000..91919101 --- /dev/null +++ b/test/scenario_test/lib/exabgp.py @@ -0,0 +1,99 @@ +# Copyright (C) 2015 Nippon Telegraph and Telephone Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from base import * + + +class ExaBGPContainer(BGPContainer): + + SHARED_VOLUME = '/root/shared_volume' + + def __init__(self, name, asn, router_id, ctn_image_name='osrg/exabgp', + exabgp_path=''): + super(ExaBGPContainer, self).__init__(name, asn, router_id, + ctn_image_name) + self.shared_volumes.append((self.config_dir, self.SHARED_VOLUME)) + self.exabgp_path = exabgp_path + + def _start_exabgp(self): + cmd = CmdBuffer(' ') + cmd << 'docker exec -d {0}'.format(self.name) + cmd << 'env exabgp.log.destination={0}/exabgpd.log'.format(self.SHARED_VOLUME) + cmd << './exabgp/sbin/exabgp {0}/exabgpd.conf'.format(self.SHARED_VOLUME) + local(str(cmd), capture=True) + + def _update_exabgp(self): + c = CmdBuffer() + c << '#!/bin/bash' + + remotepath = '/root/exabgp' + localpath = self.exabgp_path + if localpath != '': + local('cp -r {0} {1}'.format(localpath, self.config_dir)) + c << 'cp {0}/etc/exabgp/exabgp.env {1}'.format(remotepath, self.SHARED_VOLUME) + c << 'sed -i -e \'s/all = false/all = true/g\' {0}/exabgp.env'.format(self.SHARED_VOLUME) + c << 'cp -r {0}/exabgp {1}'.format(self.SHARED_VOLUME, + remotepath[:-1*len('exabgp')]) + c << 'cp {0}/exabgp.env {1}/etc/exabgp/'.format(self.SHARED_VOLUME, remotepath) + cmd = 'echo "{0:s}" > {1}/update.sh'.format(c, self.config_dir) + local(cmd, capture=True) + cmd = 'chmod 755 {0}/update.sh'.format(self.config_dir) + local(cmd, capture=True) + cmd = 'docker exec {0} {1}/update.sh'.format(self.name, + self.SHARED_VOLUME) + local(cmd, capture=True) + + def run(self): + super(ExaBGPContainer, self).run() + self._update_exabgp() + self._start_exabgp() + return self.WAIT_FOR_BOOT + + def create_config(self): + cmd = CmdBuffer() + for peer, info in self.peers.iteritems(): + cmd << 'neighbor {0} {{'.format(info['neigh_addr'].split('/')[0]) + cmd << ' router-id {0};'.format(self.router_id) + + local_addr = '' + for me, you in itertools.product(self.ip_addrs, peer.ip_addrs): + if me[2] == you[2]: + local_addr = me[1] + if local_addr == '': + raise Exception('local_addr not found') + local_addr = local_addr.split('/')[0] + cmd << ' local-address {0};'.format(local_addr) + cmd << ' local-as {0};'.format(self.asn) + cmd << ' peer-as {0};'.format(peer.asn) + + cmd << ' static {' + for route, attr in self.routes.iteritems(): + if attr == '': + cmd << ' route {0} next-hop {1};'.format(route, local_addr) + else: + cmd << ' route {0} next-hop {1} attribute {2};'.format(route, local_addr, attr) + cmd << ' }' + cmd << '}' + + with open('{0}/exabgpd.conf'.format(self.config_dir), 'w') as f: + print colors.yellow(str(cmd)) + f.write(str(cmd)) + + def reload_config(self): + cmd = 'docker exec {0} /usr/bin/pkill exabgp -SIGALRM'.format(self.name) + try: + local(cmd, capture=True) + except: + self._start_exabgp() diff --git a/test/scenario_test/lib/gobgp.py b/test/scenario_test/lib/gobgp.py new file mode 100644 index 00000000..d9115f1b --- /dev/null +++ b/test/scenario_test/lib/gobgp.py @@ -0,0 +1,175 @@ +# Copyright (C) 2015 Nippon Telegraph and Telephone Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from base import * +import json +import toml +import subprocess +import select + + +class GoBGPContainer(BGPContainer): + + PEER_TYPE_INTERNAL = 0 + PEER_TYPE_EXTERNAL = 1 + SHARED_VOLUME = '/root/shared_volume' + + def __init__(self, name, asn, router_id, ctn_image_name='gobgp', + log_level='debug'): + super(GoBGPContainer, self).__init__(name, asn, router_id, + ctn_image_name) + self.shared_volumes.append((self.config_dir, self.SHARED_VOLUME)) + self.log_level = log_level + + def _start_gobgp(self): + c = CmdBuffer() + c << '#!/bin/bash' + c << '/go/bin/gobgpd -f {0}/gobgpd.conf -l {1} -p > ' \ + '{0}/gobgpd.log 2>&1'.format(self.SHARED_VOLUME, self.log_level) + + cmd = 'echo "{0:s}" > {1}/start.sh'.format(c, self.config_dir) + local(cmd, capture=True) + cmd = "chmod 755 {0}/start.sh".format(self.config_dir) + local(cmd, capture=True) + cmd = 'docker exec -d {0} {1}/start.sh'.format(self.name, + self.SHARED_VOLUME) + local(cmd, capture=True) + + def run(self): + super(GoBGPContainer, self).run() + self._start_gobgp() + return self.WAIT_FOR_BOOT + + def get_local_rib(self, peer, rf='ipv4'): + if peer not in self.peers: + raise Exception('not found peer {0}'.format(peer.router_id)) + peer_addr = self.peers[peer]['neigh_addr'].split('/')[0] + gobgp = '/go/bin/gobgp' + cmd = CmdBuffer(' ') + cmd << "docker exec {0} {1}".format(self.name, gobgp) + cmd << "-j neighbor {0} local -a {1}".format(peer_addr, rf) + output = local(str(cmd), capture=True) + n = json.loads(output) + return n + + def get_global_rib(self, prefix='', rf='ipv4'): + gobgp = '/go/bin/gobgp' + cmd = 'docker exec {0} {1} -j global rib {2} -a {3}'.format(self.name, + gobgp, + prefix, + rf) + + output = local(cmd, capture=True) + n = json.loads(output) + return n + + def get_neighbor_state(self, peer): + if peer not in self.peers: + raise Exception('not found peer {0}'.format(peer.router_id)) + peer_addr = self.peers[peer]['neigh_addr'].split('/')[0] + gobgp = '/go/bin/gobgp' + cmd = 'docker exec {0} {1} -j neighbor {2}'.format(self.name, + gobgp, + peer_addr) + output = local(cmd, capture=True) + return json.loads(output)['info']['bgp_state'] + + def wait_for(self, expected_state, peer, timeout=20): + state = self.get_neighbor_state(peer) + y = colors.yellow + print y("{0}'s peer {1} state: {2}".format(self.router_id, + peer.router_id, + state)) + if state == expected_state: + return + + peer_addr = self.peers[peer]['neigh_addr'].split('/')[0] + gobgp = '/go/bin/gobgp' + cmd = 'docker exec {0} {1} monitor neighbor {2} -j'.format(self.name, + gobgp, + peer_addr) + process = subprocess.Popen(cmd, shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + + poll = select.epoll() + poll.register(process.stdout, select.POLLIN) + + while True: + result = poll.poll(float(timeout)) + if result: + line = process.stdout.readline() + info = json.loads(line)['info'] + print y("{0}'s peer {1} state: {2}".format(self.router_id, + peer.router_id, + info['bgp_state'])) + if info['bgp_state'] == expected_state: + return + continue + raise Exception('timeout') + + + def create_config(self): + config = {'Global': {'As': self.asn, 'RouterId': self.router_id}} + for peer, info in self.peers.iteritems(): + if self.asn == peer.asn: + peer_type = self.PEER_TYPE_INTERNAL + else: + peer_type = self.PEER_TYPE_EXTERNAL + + afi_safi_list = [] + version = netaddr.IPNetwork(info['neigh_addr']).version + if version == 4: + afi_safi_list.append({'AfiSafiName': 'ipv4-unicast'}) + elif version == 6: + afi_safi_list.append({'AfiSafiName': 'ipv6-unicast'}) + else: + Exception('invalid ip address version. {0}'.format(version)) + + if info['evpn']: + afi_safi_list.append({'AfiSafiName': 'l2vpn-evpn'}) + afi_safi_list.append({'AfiSafiName': 'encap'}) + afi_safi_list.append({'AfiSafiName': 'rtc'}) + + n = {'NeighborAddress': info['neigh_addr'].split('/')[0], + 'PeerAs': peer.asn, + 'AuthPassword': info['passwd'], + 'PeerType': peer_type, + 'AfiSafiList': afi_safi_list} + + if info['passive']: + n['TransportOptions'] = {'PassiveMode': True} + + if info['is_rs_client']: + n['RouteServer'] = {'RouteServerClient': True} + + if info['is_rr_client']: + clusterId = info['cluster_id'] + n['RouteReflector'] = {'RouteReflectorClient': True, + 'RouteReflectorClusterId': clusterId} + + if 'NeighborList' not in config: + config['NeighborList'] = [] + + config['NeighborList'].append(n) + + with open('{0}/gobgpd.conf'.format(self.config_dir), 'w') as f: + print colors.yellow('[{0}\'s new config]'.format(self.name)) + print colors.yellow(indent(toml.dumps(config))) + f.write(toml.dumps(config)) + + def reload_config(self): + cmd = 'docker exec {0} /usr/bin/pkill gobgpd -SIGHUP'.format(self.name) + local(cmd, capture=True) diff --git a/test/scenario_test/lib/quagga.py b/test/scenario_test/lib/quagga.py new file mode 100644 index 00000000..75c2f61c --- /dev/null +++ b/test/scenario_test/lib/quagga.py @@ -0,0 +1,193 @@ +# Copyright (C) 2015 Nippon Telegraph and Telephone Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from base import * +import telnetlib + + +class QuaggaTelnetDaemon(object): + TELNET_PASSWORD = "zebra" + TELNET_PORT = 2605 + + def __init__(self, ctn): + ip_addr = None + for _, ip_addr, br in ctn.ip_addrs: + if br.ip_addr: + break + + if not ip_addr: + Exception('quagga telnet daemon {0}' + ' is not ip reachable'.format(self.name)) + + self.host = ip_addr.split('/')[0] + + def __enter__(self): + self.tn = telnetlib.Telnet(self.host, self.TELNET_PORT) + self.tn.read_until("Password: ") + self.tn.write(self.TELNET_PASSWORD + "\n") + self.tn.write("enable\n") + self.tn.read_until('bgpd#') + return self.tn + + def __exit__(self, type, value, traceback): + self.tn.close() + + +class QuaggaBGPContainer(BGPContainer): + + WAIT_FOR_BOOT = 1 + SHARED_VOLUME = '/etc/quagga' + + def __init__(self, name, asn, router_id, ctn_image_name='osrg/quagga'): + super(QuaggaBGPContainer, self).__init__(name, asn, router_id, + ctn_image_name) + self.shared_volumes.append((self.config_dir, self.SHARED_VOLUME)) + + def run(self): + super(QuaggaBGPContainer, self).run() + return self.WAIT_FOR_BOOT + + def get_global_rib(self, prefix='', rf='ipv4'): + rib = [] + if prefix != '': + return self.get_global_rib_with_prefix(prefix, rf) + with QuaggaTelnetDaemon(self) as tn: + tn.write('show bgp {0} unicast\n'.format(rf)) + tn.read_until(' Network Next Hop Metric ' + 'LocPrf Weight Path') + for line in tn.read_until('bgpd#').split('\n'): + if line[0] == '*': + elems = line.split() + rib.append({'prefix': elems[1], 'nexthop': elems[2]}) + + return rib + + def get_global_rib_with_prefix(self, prefix, rf): + rib = [] + with QuaggaTelnetDaemon(self) as tn: + tn.write('show bgp {0} unicast {1}\n'.format(rf, prefix)) + lines = [line.strip() for line in tn.read_until('bgpd#').split('\n')] + lines.pop(0) # throw away first line contains 'show bgp...' + if lines[0] == '% Network not in table': + return rib + + lines = lines[2:] + + if lines[0].startswith('Not advertised'): + lines.pop(0) # another useless line + elif lines[0].startswith('Advertised to non peer-group peers:'): + lines = lines[2:] # other useless lines + else: + raise Exception('unknown output format {0}'.format(lines)) + nexthop = lines[1].split()[0].strip() + aspath = [int(asn) for asn in lines[0].split()] + rib.append({'prefix': prefix, 'nexthop': nexthop, + 'aspath': aspath}) + return rib + + def get_neighbor_state(self, peer): + if peer not in self.peers: + raise Exception('not found peer {0}'.format(peer.router_id)) + + neigh_addr = self.peers[peer]['neigh_addr'].split('/')[0] + + with QuaggaTelnetDaemon(self) as tn: + tn.write('show bgp neighbors\n') + neighbor_info = [] + curr_info = [] + for line in tn.read_until('bgpd#').split('\n'): + line = line.strip() + if line.startswith('BGP neighbor is'): + neighbor_info.append(curr_info) + curr_info = [] + curr_info.append(line) + neighbor_info.append(curr_info) + + for info in neighbor_info: + if not info[0].startswith('BGP neighbor is'): + continue + idx1 = info[0].index('BGP neighbor is ') + idx2 = info[0].index(',') + n_addr = info[0][idx1+len('BGP neighbor is '):idx2] + if n_addr == neigh_addr: + idx1 = info[2].index('= ') + state = info[2][idx1+len('= '):] + if state.startswith('Idle'): + return BGP_FSM_IDLE + elif state.startswith('Active'): + return BGP_FSM_ACTIVE + elif state.startswith('Established'): + return BGP_FSM_ESTABLISHED + else: + return state + + raise Exception('not found peer {0}'.format(peer.router_id)) + + def create_config(self): + c = CmdBuffer() + c << 'hostname bgpd' + c << 'password zebra' + c << 'router bgp {0}'.format(self.asn) + c << 'bgp router-id {0}'.format(self.router_id) + + for peer, info in self.peers.iteritems(): + version = netaddr.IPNetwork(info['neigh_addr']).version + n_addr = info['neigh_addr'].split('/')[0] + if version == 6: + c << 'no bgp default ipv4-unicast' + + c << 'neighbor {0} remote-as {1}'.format(n_addr, peer.asn) + for policy in info['policies']: + name = policy['name'] + direction = policy['direction'] + c << 'neighbor {0} route-map {1} {2}'.format(n_addr, name, + direction) + if info['passwd'] != '': + c << 'neighbor {0} password {1}'.format(n_addr, info['passwd']) + if version == 6: + c << 'address-family ipv6 unicast' + c << 'neighbor {0} activate'.format(n_addr) + c << 'exit-address-family' + + for route in self.routes.iterkeys(): + version = netaddr.IPNetwork(route).version + if version == 4: + c << 'network {0}'.format(route) + elif version == 6: + c << 'address-family ipv6 unicast' + c << 'network {0}'.format(route) + c << 'exit-address-family' + + for name, policy in self.policies.iteritems(): + c << 'access-list {0} {1} {2}'.format(name, policy['type'], + policy['match']) + c << 'route-map {0} permit 10'.format(name) + c << 'match ip address {0}'.format(name) + c << 'set metric {0}'.format(policy['med']) + + c << 'debug bgp as4' + c << 'debug bgp fsm' + c << 'debug bgp updates' + c << 'debug bgp events' + c << 'log file {0}/bgpd.log'.format(self.SHARED_VOLUME) + + with open('{0}/bgpd.conf'.format(self.config_dir), 'w') as f: + print colors.yellow('[{0}\'s new config]'.format(self.name)) + print colors.yellow(indent(str(c))) + f.writelines(str(c)) + + def reload_config(self): + cmd = 'docker exec {0} /usr/bin/pkill bgpd -SIGHUP'.format(self.name) + local(cmd, capture=True) diff --git a/test/scenario_test/noseplugin.py b/test/scenario_test/noseplugin.py index a0cedb80..1eae95bb 100644 --- a/test/scenario_test/noseplugin.py +++ b/test/scenario_test/noseplugin.py @@ -11,7 +11,8 @@ class OptionParser(Plugin): parser.add_option('--use-local', action="store_true", dest="use_local", default=False) parser.add_option('--exabgp-path', action="store", dest="exabgp_path", default="") parser.add_option('--go-path', action="store", dest="go_path", default="") - parser.add_option('--gobgp-log-debug', action="store_true", dest="gobgp_log_debug", default=False) + parser.add_option('--gobgp-log-level', action="store", + dest="gobgp_log_level", default="info") def configure(self, options, conf): super(OptionParser, self).configure(options, conf) @@ -22,4 +23,4 @@ class OptionParser(Plugin): return def finalize(self, result): - pass
\ No newline at end of file + pass diff --git a/test/scenario_test/route_server_ipv4_v6_test.py b/test/scenario_test/route_server_ipv4_v6_test.py index bf47c516..bc9e234d 100644 --- a/test/scenario_test/route_server_ipv4_v6_test.py +++ b/test/scenario_test/route_server_ipv4_v6_test.py @@ -39,7 +39,7 @@ class GoBGPIPv6Test(GoBGPTestBase): use_local = parser_option.use_local go_path = parser_option.go_path - log_debug = parser_option.gobgp_log_debug + log_debug = True if parser_option.gobgp_log_level == 'debug' else False fab.init_ipv6_test_env_executor(self.quagga_num, use_local, go_path, log_debug) print "please wait (" + str(self.initial_wait_time) + " second)" time.sleep(self.initial_wait_time) diff --git a/test/scenario_test/route_server_malformed_test.py b/test/scenario_test/route_server_malformed_test.py index ac3d1186..75c641c2 100644 --- a/test/scenario_test/route_server_malformed_test.py +++ b/test/scenario_test/route_server_malformed_test.py @@ -73,7 +73,7 @@ def test_malformed_packet(): sys.exit(1) use_local = parser_option.use_local - log_debug = parser_option.gobgp_log_debug + log_debug = True if parser_option.gobgp_log_level == 'debug' else False go_path = parser_option.go_path exabgp_path = parser_option.exabgp_path diff --git a/test/scenario_test/route_server_policy_test.py b/test/scenario_test/route_server_policy_test.py index 737c9a05..cfbdddd3 100644 --- a/test/scenario_test/route_server_policy_test.py +++ b/test/scenario_test/route_server_policy_test.py @@ -65,7 +65,7 @@ class GoBGPTest(GoBGPTestBase): print 'prepare gobgp' cls.go_path = parser_option.go_path cls.use_local = parser_option.use_local - cls.log_debug = parser_option.gobgp_log_debug + cls.log_debug = True if parser_option.gobgp_log_level == 'debug' else False fab.prepare_gobgp(cls.log_debug, cls.use_local) fab.build_config_tools(cls.go_path) diff --git a/test/scenario_test/route_server_test.py b/test/scenario_test/route_server_test.py index 61c94eb0..63063d50 100644 --- a/test/scenario_test/route_server_test.py +++ b/test/scenario_test/route_server_test.py @@ -39,7 +39,7 @@ class GoBGPTest(GoBGPTestBase): use_local = parser_option.use_local go_path = parser_option.go_path - log_debug = parser_option.gobgp_log_debug + log_debug = True if parser_option.gobgp_log_level == 'debug' else False fab.init_test_env_executor(self.quagga_num, use_local, go_path, log_debug) print "please wait " + str(self.initial_wait_time) + " second" |