diff options
author | ISHIDA Wataru <ishida.wataru@lab.ntt.co.jp> | 2015-11-08 01:04:04 +0900 |
---|---|---|
committer | ISHIDA Wataru <ishida.wataru@lab.ntt.co.jp> | 2015-11-11 00:28:42 +0900 |
commit | 15812686282c61120e07c7880be6d347c1a53059 (patch) | |
tree | 01e87d3438f11ab205768793131366bfc0112ef2 /test/lib | |
parent | 6dacfb38bf6e5c32abe39cfdfe825d3702316855 (diff) |
move lib to parent directory
Signed-off-by: ISHIDA Wataru <ishida.wataru@lab.ntt.co.jp>
Diffstat (limited to 'test/lib')
-rw-r--r-- | test/lib/__init__.py | 0 | ||||
-rw-r--r-- | test/lib/bagpipe.py | 73 | ||||
-rw-r--r-- | test/lib/base.py | 382 | ||||
-rw-r--r-- | test/lib/exabgp.py | 154 | ||||
-rw-r--r-- | test/lib/gobgp.py | 331 | ||||
-rw-r--r-- | test/lib/quagga.py | 260 |
6 files changed, 1200 insertions, 0 deletions
diff --git a/test/lib/__init__.py b/test/lib/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/test/lib/__init__.py diff --git a/test/lib/bagpipe.py b/test/lib/bagpipe.py new file mode 100644 index 00000000..c73b6472 --- /dev/null +++ b/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/lib/base.py b/test/lib/base.py new file mode 100644 index 00000000..139cecc4 --- /dev/null +++ b/test/lib/base.py @@ -0,0 +1,382 @@ +# 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 +from fabric.state import env + +import netaddr +import os +import time +import itertools + +DEFAULT_TEST_PREFIX = '' +DEFAULT_TEST_BASE_DIR = '/tmp/gobgp' +TEST_PREFIX = DEFAULT_TEST_PREFIX +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' + +BGP_ATTR_TYPE_ORIGIN = 1 +BGP_ATTR_TYPE_AS_PATH = 2 +BGP_ATTR_TYPE_NEXT_HOP = 3 +BGP_ATTR_TYPE_MULTI_EXIT_DISC = 4 +BGP_ATTR_TYPE_LOCAL_PREF = 5 +BGP_ATTR_TYPE_COMMUNITIES = 8 +BGP_ATTR_TYPE_MP_REACH_NLRI = 14 +BGP_ATTR_TYPE_EXTENDED_COMMUNITIES = 16 + +env.abort_exception = RuntimeError + +def try_several_times(f, t=3, s=1): + e = None + for i in range(t): + try: + r = f() + except RuntimeError as e: + time.sleep(s) + else: + return r + raise e + + +def get_bridges(): + return try_several_times(lambda : local("brctl show | awk 'NR > 1{print $1}'", capture=True)).split('\n') + + +def get_containers(): + return try_several_times(lambda : 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='', from_image='osrg/quagga'): + if local_gobgp_path == '': + local_gobgp_path = os.getcwd() + + c = CmdBuffer() + c << 'FROM {0}'.format(from_image) + c << 'ADD gobgp /go/src/github.com/osrg/gobgp/' + c << 'RUN go get github.com/osrg/gobgp/gobgpd' + c << 'RUN go install github.com/osrg/gobgp/gobgpd' + c << 'RUN go get github.com/osrg/gobgp/gobgp' + c << 'RUN go install 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_ip=False): + self.name = '{0}_{1}'.format(TEST_PREFIX, 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() + + def f(): + if self.name in get_bridges(): + self.delete() + local("ip link add {0} type bridge".format(self.name)) + try_several_times(f) + try_several_times(lambda : local("ip link set up dev {0}".format(self.name))) + + self.self_ip = self_ip + if self_ip: + self.ip_addr = self.next_ip_address() + try_several_times(lambda :local("ip addr add {0} dev {1}".format(self.ip_addr, self.name))) + self.ctns = [] + + def next_ip_address(self): + return "{0}/{1}".format(self._ip_generator.next(), + self.subnet.prefixlen) + + def addif(self, ctn): + name = ctn.next_if_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): + try_several_times(lambda : local("ip link set down dev {0}".format(self.name))) + try_several_times(lambda : local("ip link delete {0} type bridge".format(self.name))) + + +class Container(object): + def __init__(self, name, image): + self.name = name + self.image = image + self.shared_volumes = [] + self.ip_addrs = [] + self.is_running = False + self.eths = [] + + if self.docker_name() in get_containers(): + self.remove() + + def docker_name(self): + if TEST_PREFIX == DEFAULT_TEST_PREFIX: + return self.name + return '{0}_{1}'.format(TEST_PREFIX, self.name) + + def next_if_name(self): + name = 'eth{0}'.format(len(self.eths)+1) + self.eths.append(name) + return name + + 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.docker_name(), self.image) + self.id = try_several_times(lambda : local(str(c), capture=True)) + self.is_running = True + self.local("ip li set up dev lo") + for line in self.local("ip a show dev eth0", capture=True).split('\n'): + if line.strip().startswith("inet "): + elems = [e.strip() for e in line.strip().split(' ')] + self.ip_addrs.append(('eth0', elems[1], 'docker0')) + return 0 + + def stop(self): + ret = try_several_times(lambda : local("docker stop -t 0 " + self.docker_name(), capture=True)) + self.is_running = False + return ret + + def remove(self): + ret = try_several_times(lambda : local("docker rm -f " + self.docker_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.docker_name(), ip_addr) + self.ip_addrs.append((intf_name, ip_addr, bridge.name)) + try_several_times(lambda :local(str(c))) + + def local(self, cmd, capture=False, flag=''): + return local("docker exec {0} {1} {2}".format(flag, + self.docker_name(), + cmd), capture) + + def get_pid(self): + if self.is_running: + cmd = "docker inspect -f '{{.State.Pid}}' " + self.docker_name() + return int(local(cmd, capture=True)) + return -1 + + +class BGPContainer(Container): + + WAIT_FOR_BOOT = 1 + RETRY_INTERVAL = 5 + + def __init__(self, name, asn, router_id, ctn_image_name): + self.config_dir = '/'.join((TEST_BASE_DIR, TEST_PREFIX, name)) + local('if [ -e {0} ]; then rm -r {0}; fi'.format(self.config_dir)) + local('mkdir -p {0}'.format(self.config_dir)) + local('chmod 777 {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 __repr__(self): + return str({'name':self.name, 'asn':self.asn, 'router_id':self.router_id}) + + def run(self): + self.create_config() + super(BGPContainer, self).run() + return self.WAIT_FOR_BOOT + + def add_peer(self, peer, passwd=None, evpn=False, is_rs_client=False, + policies=None, passive=False, + is_rr_client=False, cluster_id=None, + flowspec=False, bridge='', reload_config=True): + neigh_addr = '' + local_addr = '' + for me, you in itertools.product(self.ip_addrs, peer.ip_addrs): + if bridge != '' and bridge != me[2]: + continue + if me[2] == you[2]: + neigh_addr = you[1] + local_addr = me[1] + break + + 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, + 'flowspec': flowspec, + 'is_rs_client': is_rs_client, + 'is_rr_client': is_rr_client, + 'cluster_id': cluster_id, + 'policies': policies, + 'passive': passive, + 'local_addr': local_addr} + if self.is_running and reload_config: + self.create_config() + self.reload_config() + + def del_peer(self, peer, reload_config=True): + del self.peers[peer] + if self.is_running and reload_config: + self.create_config() + self.reload_config() + + def disable_peer(self, peer): + raise Exception('implement disable_peer() method') + + def enable_peer(self, peer): + raise Exception('implement enable_peer() method') + + def log(self): + return local('cat {0}/*.log'.format(self.config_dir), capture=True) + + def add_route(self, route, rf='ipv4', attribute=None, aspath=None, + community=None, med=None, extendedcommunity=None, + nexthop=None, matchs=None, thens=None, + local_pref=None, reload_config=True): + self.routes[route] = {'prefix': route, + 'rf': rf, + 'attr': attribute, + 'next-hop': nexthop, + 'as-path': aspath, + 'community': community, + 'med': med, + 'local-pref': local_pref, + 'extended-community': extendedcommunity, + 'matchs': matchs, + 'thens' : thens} + if self.is_running and reload_config: + self.create_config() + self.reload_config() + + def add_policy(self, policy, peer=None, reload_config=True): + self.policies[policy['name']] = policy + if peer in self.peers: + self.peers[peer]['policies'][policy['name']] = policy + if self.is_running and reload_config: + 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 get_reachablily(self, prefix, timeout=20): + version = netaddr.IPNetwork(prefix).version + addr = prefix.split('/')[0] + if version == 4: + ping_cmd = 'ping' + elif version == 6: + ping_cmd = 'ping6' + else: + raise Exception('unsupported route family: {0}'.format(version)) + cmd = '/bin/bash -c "/bin/{0} -c 1 -w 1 {1} | xargs echo"'.format(ping_cmd, addr) + interval = 1 + count = 0 + while True: + res = self.local(cmd, capture=True) + print colors.yellow(res) + if '1 packets received' in res and '0% packet loss': + break + time.sleep(interval) + count += interval + if count >= timeout: + raise Exception('timeout') + return True + + def wait_for(self, expected_state, peer, timeout=120): + 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 add_static_route(self, network, next_hop): + cmd = '/sbin/ip route add {0} via {1}'.format(network, next_hop) + self.local(cmd) + + def set_ipv6_forward(self): + cmd = 'sysctl -w net.ipv6.conf.all.forwarding=1' + self.local(cmd) + + def create_config(self): + raise Exception('implement create_config() method') + + def reload_config(self): + raise Exception('implement reload_config() method') diff --git a/test/lib/exabgp.py b/test/lib/exabgp.py new file mode 100644 index 00000000..39298c4c --- /dev/null +++ b/test/lib/exabgp.py @@ -0,0 +1,154 @@ +# 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 * +from itertools import chain + +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 << 'env exabgp.log.destination={0}/exabgpd.log'.format(self.SHARED_VOLUME) + cmd << './exabgp/sbin/exabgp {0}/exabgpd.conf'.format(self.SHARED_VOLUME) + self.local(str(cmd), flag='-d') + + def _update_exabgp(self): + if self.exabgp_path == '': + return + c = CmdBuffer() + c << '#!/bin/bash' + + remotepath = '/root/exabgp' + localpath = self.exabgp_path + 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 = '{0}/update.sh'.format(self.SHARED_VOLUME) + self.local(cmd) + + 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) + cmd << ' local-address {0};'.format(info['local_addr'].split('/')[0]) + cmd << ' local-as {0};'.format(self.asn) + cmd << ' peer-as {0};'.format(peer.asn) + + routes = [r for r in self.routes.values() if r['rf'] == 'ipv4' or r['rf'] == 'ipv6'] + + if len(routes) > 0: + cmd << ' static {' + for route in routes: + r = CmdBuffer(' ') + nexthop = info['local_addr'].split('/')[0] + if route['next-hop']: + nexthop = route['next-hop'] + r << ' route {0} next-hop {1}'.format(route['prefix'], nexthop) + if route['as-path']: + r << 'as-path [{0}]'.format(' '.join(str(i) for i in route['as-path'])) + if route['community']: + r << 'community [{0}]'.format(' '.join(c for c in route['community'])) + if route['med']: + r << 'med {0}'.format(route['med']) + if route['local-pref']: + r << 'local-preference {0}'.format(route['local-pref']) + if route['extended-community']: + r << 'extended-community [{0}]'.format(route['extended-community']) + if route['attr']: + r << 'attribute [ {0} ]'.format(route['attr']) + + cmd << '{0};'.format(str(r)) + cmd << ' }' + + routes = [r for r in self.routes.itervalues() if 'flowspec' in r['rf']] + if len(routes) > 0: + cmd << ' flow {' + for route in routes: + cmd << ' route {0}{{'.format(route['prefix']) + cmd << ' match {' + for match in route['matchs']: + cmd << ' {0};'.format(match) +# cmd << ' source {0};'.format(route['prefix']) +# cmd << ' destination 192.168.0.1/32;' +# cmd << ' destination-port =3128 >8080&<8088;' +# cmd << ' source-port >1024;' +# cmd << ' port =14 =15 >10&<156;' +# cmd << ' protocol udp;' # how to specify multiple ip protocols +# cmd << ' packet-length >1000&<2000;' +# cmd << ' tcp-flags !syn;' + cmd << ' }' + cmd << ' then {' + for then in route['thens']: + cmd << ' {0};'.format(then) +# cmd << ' accept;' +# cmd << ' discard;' +# cmd << ' rate-limit 9600;' +# cmd << ' redirect 1.2.3.4:100;' +# cmd << ' redirect 100:100;' +# cmd << ' mark 10;' +# cmd << ' action sample-terminal;' + cmd << ' }' + cmd << ' }' + cmd << ' }' + cmd << '}' + + with open('{0}/exabgpd.conf'.format(self.config_dir), 'w') as f: + print colors.yellow('[{0}\'s new config]'.format(self.name)) + print colors.yellow(str(cmd)) + f.write(str(cmd)) + + def reload_config(self): + if len(self.peers) == 0: + return + + def _reload(): + def _is_running(): + ps = self.local('ps', capture=True) + running = False + for line in ps.split('\n')[1:]: + if 'python' in line: + running = True + return running + if _is_running(): + self.local('/usr/bin/pkill python -SIGUSR1') + else: + self._start_exabgp() + time.sleep(1) + if not _is_running(): + raise RuntimeError() + try_several_times(_reload) diff --git a/test/lib/gobgp.py b/test/lib/gobgp.py new file mode 100644 index 00000000..042d6696 --- /dev/null +++ b/test/lib/gobgp.py @@ -0,0 +1,331 @@ +# 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 +from itertools import chain + +def extract_path_attribute(path, typ): + for a in path['attrs']: + if a['type'] == typ: + return a + return None + +class GoBGPContainer(BGPContainer): + + SHARED_VOLUME = '/root/shared_volume' + QUAGGA_VOLUME = '/etc/quagga' + + def __init__(self, name, asn, router_id, ctn_image_name='gobgp', + log_level='debug', zebra=False): + 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 + self.prefix_set = None + self.neighbor_set = None + self.bgp_set = None + self.default_policy = None + self.zebra = zebra + + def _start_gobgp(self): + zebra_op = '' + c = CmdBuffer() + c << '#!/bin/bash' + c << '/go/bin/gobgpd -f {0}/gobgpd.conf -l {1} -p {2} > ' \ + '{0}/gobgpd.log 2>&1'.format(self.SHARED_VOLUME, self.log_level, zebra_op) + + 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) + self.local("{0}/start.sh".format(self.SHARED_VOLUME), flag='-d') + + def _start_zebra(self): + cmd = 'cp {0}/zebra.conf {1}/'.format(self.SHARED_VOLUME, self.QUAGGA_VOLUME) + self.local(cmd) + cmd = '/usr/lib/quagga/zebra -f {0}/zebra.conf'.format(self.QUAGGA_VOLUME) + self.local(cmd, flag='-d') + + def run(self): + super(GoBGPContainer, self).run() + if self.zebra: + self._start_zebra() + self._start_gobgp() + return self.WAIT_FOR_BOOT + + def _get_as_path(self, path): + asps = (p['as_paths'] for p in path['attrs'] if + p['type'] == BGP_ATTR_TYPE_AS_PATH and 'as_paths' in p + and p['as_paths'] != None) + asps = chain.from_iterable(asps) + asns = (asp['asns'] for asp in asps) + return list(chain.from_iterable(asns)) + + def _get_nexthop(self, path): + for p in path['attrs']: + if p['type'] == BGP_ATTR_TYPE_NEXT_HOP or p['type'] == BGP_ATTR_TYPE_MP_REACH_NLRI: + return p['nexthop'] + + def _trigger_peer_cmd(self, cmd, 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] + cmd = 'gobgp neighbor {0} {1}'.format(peer_addr, cmd) + self.local(cmd) + + def disable_peer(self, peer): + self._trigger_peer_cmd('disable', peer) + + def enable_peer(self, peer): + self._trigger_peer_cmd('enable', peer) + + def reset(self, peer): + self._trigger_peer_cmd('reset', peer) + + def softreset(self, peer, rf='ipv4', type='in'): + self._trigger_peer_cmd('softreset{0} -a {1}'.format(type, rf), peer) + + def get_local_rib(self, peer, prefix='', 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] + cmd = 'gobgp -j neighbor {0} local {1} -a {2}'.format(peer_addr, prefix, rf) + output = self.local(cmd, capture=True) + ret = json.loads(output) + for d in ret: + for p in d["paths"]: + p["nexthop"] = self._get_nexthop(p) + p["aspath"] = self._get_as_path(p) + return ret + + def get_global_rib(self, prefix='', rf='ipv4'): + cmd = 'gobgp -j global rib {0} -a {1}'.format(prefix, rf) + output = self.local(cmd, capture=True) + ret = json.loads(output) + for d in ret: + for p in d["paths"]: + p["nexthop"] = self._get_nexthop(p) + p["aspath"] = self._get_as_path(p) + return ret + + def _get_adj_rib(self, adj_type, peer, prefix='', 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] + cmd = 'gobgp neighbor {0} adj-{1} {2} -a {3} -j'.format(peer_addr, + adj_type, + prefix, rf) + output = self.local(cmd, capture=True) + ret = [p["paths"][0] for p in json.loads(output)] + for p in ret: + p["nexthop"] = self._get_nexthop(p) + p["aspath"] = self._get_as_path(p) + return ret + + def get_adj_rib_in(self, peer, prefix='', rf='ipv4'): + return self._get_adj_rib('in', peer, prefix, rf) + + def get_adj_rib_out(self, peer, prefix='', rf='ipv4'): + return self._get_adj_rib('out', peer, prefix, rf) + + 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] + cmd = 'gobgp -j neighbor {0}'.format(peer_addr) + output = self.local(cmd, capture=True) + return json.loads(output)['info']['bgp_state'] + + def clear_policy(self): + self.policies = {} + for info in self.peers.itervalues(): + info['policies'] = {} + self.prefix_set = [] + self.neighbor_set = [] + self.statements = [] + + def set_prefix_set(self, ps): + self.prefix_set = ps + + def set_neighbor_set(self, ns): + self.neighbor_set = ns + + def set_bgp_defined_set(self, bs): + self.bgp_set = bs + + def create_config(self): + self._create_config_bgp() + if self.zebra: + self._create_config_zebra() + + def _create_config_bgp(self): + config = {'Global': {'GlobalConfig': {'As': self.asn, 'RouterId': self.router_id}}} + for peer, info in self.peers.iteritems(): + 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'}) + + if info['flowspec']: + afi_safi_list.append({'AfiSafiName': 'ipv4-flowspec'}) + afi_safi_list.append({'AfiSafiName': 'l3vpn-ipv4-flowspec'}) + afi_safi_list.append({'AfiSafiName': 'ipv6-flowspec'}) + afi_safi_list.append({'AfiSafiName': 'l3vpn-ipv6-flowspec'}) + + n = {'NeighborConfig': + {'NeighborAddress': info['neigh_addr'].split('/')[0], + 'PeerAs': peer.asn, + 'AuthPassword': info['passwd'], + }, + 'AfiSafis': {'AfiSafiList': afi_safi_list} + } + + if info['passive']: + n['Transport'] = {'TransportConfig': {'PassiveMode': True}} + + if info['is_rs_client']: + n['RouteServer'] = {'RouteServerConfig': {'RouteServerClient': True}} + + if info['is_rr_client']: + clusterId = self.router_id + if 'cluster_id' in info and info['cluster_id'] is not None: + clusterId = info['cluster_id'] + n['RouteReflector'] = {'RouteReflectorConfig' : {'RouteReflectorClient': True, + 'RouteReflectorClusterId': clusterId}} + + f = lambda typ: [p for p in info['policies'].itervalues() if p['type'] == typ] + import_policies = f('import') + export_policies = f('export') + in_policies = f('in') + f = lambda typ: [p['default'] for p in info['policies'].itervalues() if p['type'] == typ and 'default' in p] + default_import_policy = f('import') + default_export_policy = f('export') + default_in_policy = f('in') + + if len(import_policies) + len(export_policies) + len(in_policies) + len(default_import_policy) \ + + len(default_export_policy) + len(default_in_policy) > 0: + n['ApplyPolicy'] = {'ApplyPolicyConfig': {}} + + if len(import_policies) > 0: + n['ApplyPolicy']['ApplyPolicyConfig']['ImportPolicy'] = [p['name'] for p in import_policies] + + if len(export_policies) > 0: + n['ApplyPolicy']['ApplyPolicyConfig']['ExportPolicy'] = [p['name'] for p in export_policies] + + if len(in_policies) > 0: + n['ApplyPolicy']['ApplyPolicyConfig']['InPolicy'] = [p['name'] for p in in_policies] + + def f(v): + if v == 'reject': + return 1 + elif v == 'accept': + return 0 + raise Exception('invalid default policy type {0}'.format(v)) + + if len(default_import_policy) > 0: + n['ApplyPolicy']['ApplyPolicyConfig']['DefaultImportPolicy'] = f(default_import_policy[0]) + + if len(default_export_policy) > 0: + n['ApplyPolicy']['ApplyPolicyConfig']['DefaultExportPolicy'] = f(default_export_policy[0]) + + if len(default_in_policy) > 0: + n['ApplyPolicy']['ApplyPolicyConfig']['DefaultInPolicy'] = f(default_in_policy[0]) + + if 'Neighbors' not in config: + config['Neighbors'] = {'NeighborList': []} + + config['Neighbors']['NeighborList'].append(n) + + config['DefinedSets'] = {} + if self.prefix_set: + config['DefinedSets']['PrefixSets'] = {'PrefixSetList': [self.prefix_set]} + + if self.neighbor_set: + config['DefinedSets']['NeighborSets'] = {'NeighborSetList': [self.neighbor_set]} + + if self.bgp_set: + config['DefinedSets']['BgpDefinedSets'] = self.bgp_set + + policy_list = [] + for p in self.policies.itervalues(): + policy = {'Name': p['name'], + 'Statements':{'StatementList': p['statements']}} + policy_list.append(policy) + + if len(policy_list) > 0: + config['PolicyDefinitions'] = {'PolicyDefinitionList': policy_list} + + if self.zebra: + config['Global']['Zebra'] = {'Enabled': True, + 'RedistributeRouteTypeList':[{'RouteType': 'connect'}],} + + 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 _create_config_zebra(self): + c = CmdBuffer() + c << 'hostname zebra' + c << 'password zebra' + c << 'log file {0}/zebra.log'.format(self.QUAGGA_VOLUME) + c << 'debug zebra packet' + c << 'debug zebra kernel' + c << 'debug zebra rib' + c << '' + + with open('{0}/zebra.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): + daemon = [] + daemon.append('gobgpd') + if self.zebra: + daemon.append('zebra') + for d in daemon: + cmd = '/usr/bin/pkill {0} -SIGHUP'.format(d) + self.local(cmd) + for v in self.routes.itervalues(): + if v['rf'] == 'ipv4' or v['rf'] == 'ipv6': + r = CmdBuffer(' ') + r << 'gobgp global -a {0}'.format(v['rf']) + r << 'rib add {0}'.format(v['prefix']) + if v['next-hop']: + r << 'nexthop {0}'.format(v['next-hop']) + if v['local-pref']: + r << 'local-pref {0}'.format(v['local-pref']) + if v['med']: + r << 'med {0}'.format(v['med']) + cmd = str(r) + elif v['rf'] == 'ipv4-flowspec' or v['rf'] == 'ipv6-flowspec': + cmd = 'gobgp global '\ + 'rib add match {0} then {1} -a {2}'.format(' '.join(v['matchs']), ' '.join(v['thens']), v['rf']) + else: + raise Exception('unsupported route faily: {0}'.format(rf)) + self.local(cmd) diff --git a/test/lib/quagga.py b/test/lib/quagga.py new file mode 100644 index 00000000..17f855a9 --- /dev/null +++ b/test/lib/quagga.py @@ -0,0 +1,260 @@ +# 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 +from nsenter import Namespace + + +class QuaggaTelnetDaemon(object): + TELNET_PASSWORD = "zebra" + TELNET_PORT = 2605 + + def __init__(self, ctn): + self.ns = Namespace(ctn.get_pid(), 'net') + + def __enter__(self): + self.ns.__enter__() + self.tn = telnetlib.Telnet('127.0.0.1', 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() + self.ns.__exit__(type, value, traceback) + + +class QuaggaBGPContainer(BGPContainer): + + WAIT_FOR_BOOT = 1 + SHARED_VOLUME = '/etc/quagga' + + def __init__(self, name, asn, router_id, ctn_image_name='osrg/quagga', zebra=False): + super(QuaggaBGPContainer, self).__init__(name, asn, router_id, + ctn_image_name) + self.shared_volumes.append((self.config_dir, self.SHARED_VOLUME)) + self.zebra = zebra + + 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') + read_next = False + for line in tn.read_until('bgpd#').split('\n'): + if line[:2] == '*>': + line = line[2:] + ibgp = False + if line[0] == 'i': + line = line[1:] + ibgp = True + elif not read_next: + continue + + elems = line.split() + + if len(elems) == 1: + read_next = True + prefix = elems[0] + continue + elif read_next: + nexthop = elems[0] + else: + prefix = elems[0] + nexthop = elems[1] + read_next = False + + rib.append({'prefix': prefix, 'nexthop': nexthop, + 'ibgp': ibgp}) + + 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)) + aspath = [int(asn) for asn in lines[0].split()] + nexthop = lines[1].split()[0].strip() + info = [s.strip(',') for s in lines[2].split()] + attrs = [] + if 'metric' in info: + med = info[info.index('metric') + 1] + attrs.append({'type': BGP_ATTR_TYPE_MULTI_EXIT_DISC, 'metric': int(med)}) + if 'localpref' in info: + localpref = info[info.index('localpref') + 1] + attrs.append({'type': BGP_ATTR_TYPE_LOCAL_PREF, 'value': int(localpref)}) + + rib.append({'prefix': prefix, 'nexthop': nexthop, + 'aspath': aspath, 'attrs': attrs}) + 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 send_route_refresh(self): + with QuaggaTelnetDaemon(self) as tn: + tn.write('clear ip bgp * soft\n') + #tn.read_until('bgpd#') + + def create_config(self): + self._create_config_bgp() + if self.zebra: + self._create_config_zebra() + + def _create_config_bgp(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) + + version = 4 + 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 name, policy in info['policies'].iteritems(): + 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.itervalues(): + if route['rf'] == 'ipv4': + c << 'network {0}'.format(route['prefix']) + elif route['rf'] == 'ipv6': + c << 'address-family ipv6 unicast' + c << 'network {0}'.format(route['prefix']) + c << 'exit-address-family' + else: + raise Exception('unsupported route faily: {0}'.format(route['rf'])) + + if self.zebra: + if version == 6: + c << 'address-family ipv6 unicast' + c << 'redistribute connected' + c << 'exit-address-family' + else: + c << 'redistribute connected' + + 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 _create_config_zebra(self): + c = CmdBuffer() + c << 'hostname zebra' + c << 'password zebra' + c << 'log file {0}/zebra.log'.format(self.SHARED_VOLUME) + c << 'debug zebra packet' + c << 'debug zebra kernel' + c << 'debug zebra rib' + c << '' + + with open('{0}/zebra.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): + daemon = [] + daemon.append('bgpd') + if self.zebra: + daemon.append('zebra') + for d in daemon: + cmd = '/usr/bin/pkill {0} -SIGHUP'.format(d) + self.local(cmd) + |