summaryrefslogtreecommitdiffhomepage
path: root/test/lib
diff options
context:
space:
mode:
authorISHIDA Wataru <ishida.wataru@lab.ntt.co.jp>2015-11-08 01:04:04 +0900
committerISHIDA Wataru <ishida.wataru@lab.ntt.co.jp>2015-11-11 00:28:42 +0900
commit15812686282c61120e07c7880be6d347c1a53059 (patch)
tree01e87d3438f11ab205768793131366bfc0112ef2 /test/lib
parent6dacfb38bf6e5c32abe39cfdfe825d3702316855 (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__.py0
-rw-r--r--test/lib/bagpipe.py73
-rw-r--r--test/lib/base.py382
-rw-r--r--test/lib/exabgp.py154
-rw-r--r--test/lib/gobgp.py331
-rw-r--r--test/lib/quagga.py260
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)
+