diff options
author | fumihiko kakuma <kakuma@valinux.co.jp> | 2016-10-29 06:27:10 +0900 |
---|---|---|
committer | FUJITA Tomonori <fujita.tomonori@lab.ntt.co.jp> | 2016-10-29 06:27:10 +0900 |
commit | 0a602522086d26ce40afcab4eab6ff081927a48d (patch) | |
tree | 88c6dba71c5161c0a103201c99d8e56fe45cb396 | |
parent | 649d31afbe5ffbe909ec390770ba8bc8942478ce (diff) |
Add bgp scenario tests tool
This provides an environment which test a peer between ryu and quagga.
I also consider that these modules are used from openstack or other
projects. So there may be some functions that are not used by test
for ryu.
This has the following functions.
- build docker image and run ryu and quagga on that container.
- configure ryu and quagga.
- have some operations for ryu, quagga and docker.
Signed-off-by: Fumihiko Kakuma <kakuma@valinux.co.jp>
Signed-off-by: FUJITA Tomonori <fujita.tomonori@lab.ntt.co.jp>
-rw-r--r-- | ryu/tests/integrated/common/__init__.py | 0 | ||||
-rw-r--r-- | ryu/tests/integrated/common/docker_base.py | 808 | ||||
-rw-r--r-- | ryu/tests/integrated/common/quagga.py | 331 | ||||
-rw-r--r-- | ryu/tests/integrated/common/ryubgp.py | 211 |
4 files changed, 1350 insertions, 0 deletions
diff --git a/ryu/tests/integrated/common/__init__.py b/ryu/tests/integrated/common/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/ryu/tests/integrated/common/__init__.py diff --git a/ryu/tests/integrated/common/docker_base.py b/ryu/tests/integrated/common/docker_base.py new file mode 100644 index 00000000..13fe223f --- /dev/null +++ b/ryu/tests/integrated/common/docker_base.py @@ -0,0 +1,808 @@ +# Copyright (C) 2015 Nippon Telegraph and Telephone Corporation. +# +# This is based on the following +# https://github.com/osrg/gobgp/test/lib/base.py +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import absolute_import + +import itertools +import logging +import subprocess +import time + +from docker import Client +import netaddr +import six + +LOG = logging.getLogger(__name__) + +DEFAULT_TEST_PREFIX = '' +DEFAULT_TEST_BASE_DIR = '/tmp/ctn_docker/bgp' +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_ORIGINATOR_ID = 9 +BGP_ATTR_TYPE_CLUSTER_LIST = 10 +BGP_ATTR_TYPE_MP_REACH_NLRI = 14 +BGP_ATTR_TYPE_EXTENDED_COMMUNITIES = 16 + +BRIDGE_TYPE_DOCKER = 'docker' +BRIDGE_TYPE_BRCTL = 'brctl' +BRIDGE_TYPE_OVS = 'ovs' + + +class CommandError(Exception): + def __init__(self, out): + super(CommandError, self).__init__() + self.out = out + + +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 + + +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) + + +class CommandOut(str): + pass + + +class Command(object): + + def _execute(self, cmd, capture=False, executable=None): + """Execute a command using subprocess.Popen() + :Parameters: + - out: stdout from subprocess.Popen() + out has some attributes. + out.returncode: returncode of subprocess.Popen() + out.stderr: stderr from subprocess.Popen() + """ + if capture: + p_stdout = subprocess.PIPE + p_stderr = subprocess.PIPE + else: + p_stdout = None + p_stderr = None + pop = subprocess.Popen(cmd, shell=True, executable=executable, + stdout=p_stdout, + stderr=p_stderr) + __stdout, __stderr = pop.communicate() + try: + if six.PY3 and isinstance(__stdout, six.binary_type): + _stdout = __stdout.decode('ascii') + else: + _stdout = __stdout + if six.PY3 and isinstance(__stderr, six.binary_type): + _stderr = __stderr.decode('ascii') + else: + _stderr = __stderr + except UnicodeError: + _stdout = __stdout + _stderr = __stderr + out = CommandOut(_stdout if _stdout else "") + out.stderr = _stderr if _stderr else "" + out.command = cmd + out.returncode = pop.returncode + return out + + def execute(self, cmd, capture=True, try_times=1, interval=1): + for i in range(try_times): + out = self._execute(cmd, capture=capture) + LOG.info(out.command) + if out.returncode == 0: + return out + LOG.error(out.stderr) + if try_times + 1 >= try_times: + break + time.sleep(interval) + raise CommandError(out) + + def sudo(self, cmd, capture=True, try_times=1, interval=1): + cmd = 'sudo ' + cmd + return self.execute(cmd, capture=capture, + try_times=try_times, interval=interval) + + +class DockerImage(object): + def __init__(self, baseimage='ubuntu:14.04.5'): + self.baseimage = baseimage + self.cmd = Command() + + def get_images(self): + out = self.cmd.sudo('sudo docker images') + images = [] + for line in out.splitlines()[1:]: + images.append(line.split()[0]) + return images + + def exist(self, name): + if name in self.get_images(): + return True + else: + return False + + def build(self, tagname, dockerfile_dir): + self.cmd.sudo( + "docker build -t {0} {1}".format(tagname, dockerfile_dir), + try_times=3) + + def remove(self, tagname, check_exist=False): + if check_exist and not self.exist(tagname): + return tagname + self.cmd.sudo("docker rmi -f " + tagname, try_times=3) + + def create_quagga(self, tagname='quagga', image=None, check_exist=False): + if check_exist and self.exist(tagname): + return tagname + workdir = TEST_BASE_DIR + '/' + tagname + pkges = 'telnet tcpdump quagga' + if image: + use_image = image + else: + use_image = self.baseimage + c = CmdBuffer() + c << 'FROM ' + use_image + c << 'RUN apt-get update' + c << 'RUN apt-get install -qy --no-install-recommends ' + pkges + c << 'CMD /usr/lib/quagga/bgpd' + + self.cmd.sudo('rm -rf ' + workdir) + self.cmd.execute('mkdir -p ' + workdir) + self.cmd.execute("echo '%s' > %s/Dockerfile" % (str(c), workdir)) + self.build(tagname, workdir) + return tagname + + def create_ryu(self, tagname='ryu', image=None, check_exist=False): + if check_exist and self.exist(tagname): + return tagname + workdir = TEST_BASE_DIR + '/' + tagname + workdir_ctn = '/root/osrg/ryu' + pkges = 'telnet tcpdump iproute2 ' + pkges += 'gcc python-pip python-dev libffi-dev libssl-dev' + if image: + use_image = image + else: + use_image = self.baseimage + c = CmdBuffer() + c << 'FROM ' + use_image + c << 'RUN apt-get update' + c << 'RUN apt-get install -qy --no-install-recommends ' + pkges + c << 'RUN pip install -U six paramiko msgpack-rpc-python' + c << 'ADD ryu ' + workdir_ctn + install = 'RUN cd %s && pip install -r tools/pip-requires ' % workdir_ctn + install += '&& python setup.py install' + c << install + + self.cmd.sudo('rm -rf ' + workdir) + self.cmd.execute('mkdir -p ' + workdir) + self.cmd.execute("echo '%s' > %s/Dockerfile" % (str(c), workdir)) + self.cmd.execute('cp -r ../ryu %s/' % workdir) + self.build(tagname, workdir) + return tagname + + +class Bridge(object): + def __init__(self, name, subnet='', start_ip=None, end_ip=None, + with_ip=True, self_ip=False, + fixed_ip=None, reuse=False, + br_type='docker'): + """Manage a bridge + :Parameters: + - name: bridge name + - subnet: network cider to be used in this bridge + - start_ip: start address of an ip to be used in the subnet + - end_ip: end address of an ip to be used in the subnet + - with_ip: specify if assign automatically an ip address + - self_ip: specify if assign an ip address for the bridge + - fixed_ip: an ip address to be assigned to the bridge + - reuse: specify if use an existing bridge + - br_type: One either in a 'docker', 'brctl' or 'ovs' + """ + self.cmd = Command() + self.name = name + if br_type not in (BRIDGE_TYPE_DOCKER, BRIDGE_TYPE_BRCTL, + BRIDGE_TYPE_OVS): + raise Exception("argument error br_type: %s" % self.br_type) + self.br_type = br_type + if self.br_type == BRIDGE_TYPE_DOCKER: + self.docker_nw = True + else: + self.docker_nw = False + if TEST_PREFIX != '': + self.name = '{0}_{1}'.format(TEST_PREFIX, name) + self.with_ip = with_ip + if with_ip: + self.subnet = netaddr.IPNetwork(subnet) + if start_ip: + self.start_ip = start_ip + else: + self.start_ip = netaddr.IPAddress(self.subnet.first) + if end_ip: + self.end_ip = end_ip + else: + self.end_ip = netaddr.IPAddress(self.subnet.last) + + def f(): + for host in netaddr.IPRange(self.start_ip, self.end_ip): + yield host + self._ip_generator = f() + # throw away first network address + self.next_ip_address() + + self.self_ip = self_ip + if fixed_ip: + self.ip_addr = fixed_ip + else: + self.ip_addr = self.next_ip_address() + if not reuse: + def f(): + if self.br_type == BRIDGE_TYPE_DOCKER: + gw = "--gateway %s" % self.ip_addr.split('/')[0] + v6 = '' + if self.subnet.version == 6: + v6 = '--ipv6' + cmd = "docker network create --driver bridge %s " % v6 + cmd += "%s --subnet %s %s" % (gw, subnet, self.name) + elif self.br_type == BRIDGE_TYPE_BRCTL: + cmd = "ip link add {0} type bridge".format(self.name) + elif self.br_type == BRIDGE_TYPE_OVS: + cmd = "ovs-vsctl add-br {0}".format(self.name) + self.delete() + self.execute(cmd, sudo=True, retry=True) + try_several_times(f) + if not self.docker_nw: + self.execute("ip link set up dev {0}".format(self.name), + sudo=True, retry=True) + + if not self.docker_nw and self_ip: + ips = self.check_br_addr(self.name) + for key, ip in ips.items(): + if self.subnet.version == key: + self.execute( + "ip addr del {0} dev {1}".format(ip, self.name), + sudo=True, retry=True) + self.execute( + "ip addr add {0} dev {1}".format(self.ip_addr, self.name), + sudo=True, retry=True) + self.ctns = [] + + def get_bridges_dc(self): + out = self.execute('docker network ls', sudo=True, retry=True) + bridges = [] + for line in out.splitlines()[1:]: + bridges.append(line.split()[1]) + return bridges + + def get_bridges_brctl(self): + out = self.execute('brctl show', retry=True) + bridges = [] + for line in out.splitlines()[1:]: + bridges.append(line.split()[0]) + return bridges + + def get_bridges_ovs(self): + out = self.execute('ovs-vsctl list-br', sudo=True, retry=True) + return out.splitlines() + + def get_bridges(self): + if self.br_type == BRIDGE_TYPE_DOCKER: + return self.get_bridges_dc() + elif self.br_type == BRIDGE_TYPE_BRCTL: + return self.get_bridges_brctl() + elif self.br_type == BRIDGE_TYPE_OVS: + return self.get_bridges_ovs() + + def exist(self): + if self.name in self.get_bridges(): + return True + else: + return False + + def execute(self, cmd, capture=True, sudo=False, retry=False): + if sudo: + m = self.cmd.sudo + else: + m = self.cmd.execute + if retry: + return m(cmd, capture=capture, try_times=3, interval=1) + else: + return m(cmd, capture=capture) + + def check_br_addr(self, br): + ips = {} + cmd = "ip a show dev %s" % br + for line in self.execute(cmd, sudo=True).split('\n'): + if line.strip().startswith("inet "): + elems = [e.strip() for e in line.strip().split(' ')] + ips[4] = elems[1] + elif line.strip().startswith("inet6 "): + elems = [e.strip() for e in line.strip().split(' ')] + ips[6] = elems[1] + return ips + + def next_ip_address(self): + return "{0}/{1}".format(next(self._ip_generator), + self.subnet.prefixlen) + + def addif(self, ctn): + name = ctn.next_if_name() + self.ctns.append(ctn) + ip_address = None + if self.docker_nw: + ipv4 = None + ipv6 = None + ip_address = self.next_ip_address() + version = 4 + if netaddr.IPNetwork(ip_address).version == 6: + version = 6 + opt_ip = "--ip %s" % ip_address + if version == 4: + ipv4 = ip_address + else: + opt_ip = "--ip6 %s" % ip_address + ipv6 = ip_address + cmd = "docker network connect %s " % opt_ip + cmd += "%s %s" % (self.name, ctn.docker_name()) + self.execute(cmd, sudo=True) + ctn.set_addr_info(bridge=self.name, ipv4=ipv4, ipv6=ipv6, + ifname=name) + else: + if self.with_ip: + ip_address = self.next_ip_address() + version = 4 + if netaddr.IPNetwork(ip_address).version == 6: + version = 6 + ctn.pipework(self, ip_address, name, version=version) + else: + ctn.pipework(self, '0/0', name) + return ip_address + + def delete(self, check_exist=True): + if check_exist: + if not self.exist(): + return + if self.br_type == BRIDGE_TYPE_DOCKER: + self.execute("docker network rm %s" % self.name, + sudo=True, retry=True) + elif self.br_type == BRIDGE_TYPE_BRCTL: + self.execute("ip link set down dev %s" % self.name, + sudo=True, retry=True) + self.execute( + "ip link delete %s type bridge" % self.name, + sudo=True, retry=True) + elif self.br_type == BRIDGE_TYPE_OVS: + self.execute( + "ovs-vsctl del-br %s" % self.name, + sudo=True, retry=True) + + +class Container(object): + def __init__(self, name, image=None): + self.name = name + self.image = image + self.shared_volumes = [] + self.ip_addrs = [] + self.ip6_addrs = [] + self.is_running = False + self.eths = [] + self.id = None + + self.cmd = Command() + self.remove() + + def docker_name(self): + if TEST_PREFIX == DEFAULT_TEST_PREFIX: + return self.name + return '{0}_{1}'.format(TEST_PREFIX, self.name) + + def get_docker_id(self): + if self.id: + return self.id + else: + return self.docker_name() + + def next_if_name(self): + name = 'eth{0}'.format(len(self.eths) + 1) + self.eths.append(name) + return name + + def set_addr_info(self, bridge, ipv4=None, ipv6=None, ifname='eth0'): + if ipv4: + self.ip_addrs.append((ifname, ipv4, bridge)) + if ipv6: + self.ip6_addrs.append((ifname, ipv6, bridge)) + + def get_addr_info(self, bridge, ipv=4): + addrinfo = {} + if ipv == 4: + ip_addrs = self.ip_addrs + elif ipv == 6: + ip_addrs = self.ip6_addrs + else: + return None + for addr in ip_addrs: + if addr[2] == bridge: + addrinfo[addr[1]] = addr[0] + return addrinfo + + def execute(self, cmd, capture=True, sudo=False, retry=False): + if sudo: + m = self.cmd.sudo + else: + m = self.cmd.execute + if retry: + return m(cmd, capture=capture, try_times=3, interval=1) + else: + return m(cmd, capture=capture) + + def dcexec(self, cmd, capture=True, retry=False): + if retry: + return self.cmd.sudo(cmd, capture=capture, try_times=3, interval=1) + else: + return self.cmd.sudo(cmd, capture=capture) + + def exec_on_ctn(self, cmd, capture=True, stream=False, detach=False): + name = self.docker_name() + if stream: + # This needs root permission. + dcli = Client(timeout=120, version='auto') + i = dcli.exec_create(container=name, cmd=cmd) + return dcli.exec_start(i['Id'], tty=True, + stream=stream, detach=detach) + else: + flag = '-d' if detach else '' + return self.dcexec('docker exec {0} {1} {2}'.format( + flag, name, cmd), capture=capture) + + def get_containers(self, allctn=False): + cmd = 'docker ps --no-trunc=true' + if allctn: + cmd += ' --all=true' + out = self.dcexec(cmd, retry=True) + containers = [] + for line in out.splitlines()[1:]: + containers.append(line.split()[-1]) + return containers + + def exist(self, allctn=False): + if self.docker_name() in self.get_containers(allctn=allctn): + return True + else: + return False + + 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} --hostname {0} -id {1}".format(self.docker_name(), + self.image) + self.id = self.dcexec(str(c), retry=True) + self.is_running = True + self.exec_on_ctn("ip li set up dev lo") + ipv4 = None + ipv6 = None + for line in self.exec_on_ctn("ip a show dev eth0").split('\n'): + if line.strip().startswith("inet "): + elems = [e.strip() for e in line.strip().split(' ')] + ipv4 = elems[1] + elif line.strip().startswith("inet6 "): + elems = [e.strip() for e in line.strip().split(' ')] + ipv6 = elems[1] + self.set_addr_info(bridge='docker0', ipv4=ipv4, ipv6=ipv6, + ifname='eth0') + return 0 + + def stop(self, check_exist=True): + if check_exist: + if not self.exist(allctn=False): + return + ctn_id = self.get_docker_id() + out = self.dcexec('docker stop -t 0 ' + ctn_id, retry=True) + self.is_running = False + return out + + def remove(self, check_exist=True): + if check_exist: + if not self.exist(allctn=True): + return + ctn_id = self.get_docker_id() + out = self.dcexec('docker rm -f ' + ctn_id, retry=True) + self.is_running = False + return out + + def pipework(self, bridge, ip_addr, intf_name="", version=4): + if not self.is_running: + LOG.warning('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" + ipv4 = None + ipv6 = None + if version == 4: + ipv4 = ip_addr + else: + c << '-a 6' + ipv6 = ip_addr + c << "{0} {1}".format(self.docker_name(), ip_addr) + self.set_addr_info(bridge=bridge.name, ipv4=ipv4, ipv6=ipv6, + ifname=intf_name) + self.execute(str(c), sudo=True, retry=True) + + def get_pid(self): + if self.is_running: + cmd = "docker inspect -f '{{.State.Pid}}' " + self.docker_name() + return int(self.dcexec(cmd)) + return -1 + + def start_tcpdump(self, interface=None, filename=None): + if not interface: + interface = "eth0" + if not filename: + filename = "{0}/{1}.dump".format( + self.shared_volumes[0][1], interface) + self.exec_on_ctn( + "tcpdump -i {0} -w {1}".format(interface, filename), + detach=True) + + +class BGPContainer(Container): + + WAIT_FOR_BOOT = 1 + RETRY_INTERVAL = 5 + DEFAULT_PEER_ARG = {'neigh_addr': '', + 'passwd': None, + 'vpn': False, + 'flowspec': False, + 'is_rs_client': False, + 'is_rr_client': False, + 'cluster_id': None, + 'policies': None, + 'passive': False, + 'local_addr': '', + 'as2': False, + 'graceful_restart': None, + 'local_as': None, + 'prefix_limit': None} + default_peer_keys = sorted(DEFAULT_PEER_ARG.keys()) + DEFAULT_ROUTE_ARG = {'prefix': None, + 'rf': 'ipv4', + 'attr': None, + 'next-hop': None, + 'as-path': None, + 'community': None, + 'med': None, + 'local-pref': None, + 'extended-community': None, + 'matchs': None, + 'thens': None} + default_route_keys = sorted(DEFAULT_ROUTE_ARG.keys()) + + def __init__(self, name, asn, router_id, ctn_image_name=None): + self.config_dir = TEST_BASE_DIR + if TEST_PREFIX: + self.config_dir += '/' + TEST_PREFIX + self.config_dir += '/' + name + self.asn = asn + self.router_id = router_id + self.peers = {} + self.routes = {} + self.policies = {} + super(BGPContainer, self).__init__(name, ctn_image_name) + self.execute( + 'rm -rf {0}'.format(self.config_dir), sudo=True) + self.execute('mkdir -p {0}'.format(self.config_dir)) + self.execute('chmod 777 {0}'.format(self.config_dir)) + + def __repr__(self): + return str({'name': self.name, 'asn': self.asn, + 'router_id': self.router_id}) + + def run(self, wait=False, w_time=WAIT_FOR_BOOT): + self.create_config() + super(BGPContainer, self).run() + if wait: + time.sleep(w_time) + return w_time + + def add_peer(self, peer, bridge='', reload_config=True, v6=False, + peer_info={}): + self.peers[peer] = self.DEFAULT_PEER_ARG.copy() + self.peers[peer].update(peer_info) + peer_keys = sorted(self.peers[peer].keys()) + if peer_keys != self.default_peer_keys: + raise Exception("argument error peer_info: %s" % peer_info) + + neigh_addr = '' + local_addr = '' + it = itertools.product(self.ip_addrs, peer.ip_addrs) + if v6: + it = itertools.product(self.ip6_addrs, peer.ip6_addrs) + + for me, you in it: + if bridge != '' and bridge != me[2]: + continue + if me[2] == you[2]: + neigh_addr = you[1] + local_addr = me[1] + if v6: + addr, mask = local_addr.split('/') + local_addr = "{0}%{1}/{2}".format(addr, me[0], mask) + break + + if neigh_addr == '': + raise Exception('peer {0} seems not ip reachable'.format(peer)) + + if not self.peers[peer]['policies']: + self.peers[peer]['policies'] = {} + + self.peers[peer]['neigh_addr'] = neigh_addr + self.peers[peer]['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 self.execute('cat {0}/*.log'.format(self.config_dir)) + + def add_route(self, route, reload_config=True, route_info={}): + self.routes[route] = self.DEFAULT_ROUTE_ARG.copy() + self.routes[route].update(route_info) + route_keys = sorted(self.routes[route].keys()) + if route_keys != self.default_route_keys: + raise Exception("argument error route_info: %s" % route_info) + self.routes[route]['prefix'] = route + if self.is_running and reload_config: + self.create_config() + self.reload_config() + + def add_policy(self, policy, peer, typ, default='accept', + reload_config=True): + self.set_default_policy(peer, typ, default) + self.define_policy(policy) + self.assign_policy(peer, policy, typ) + if self.is_running and reload_config: + self.create_config() + self.reload_config() + + def set_default_policy(self, peer, typ, default): + if (typ in ['in', 'out', 'import', 'export'] and + default in ['reject', 'accept']): + if 'default-policy' not in self.peers[peer]: + self.peers[peer]['default-policy'] = {} + self.peers[peer]['default-policy'][typ] = default + else: + raise Exception('wrong type or default') + + def define_policy(self, policy): + self.policies[policy['name']] = policy + + def assign_policy(self, peer, policy, typ): + if peer not in self.peers: + raise Exception('peer {0} not found'.format(peer.name)) + name = policy['name'] + if name not in self.policies: + raise Exception('policy {0} not found'.format(name)) + self.peers[peer]['policies'][typ] = policy + + 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.exec_on_ctn(cmd) + LOG.info(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) + LOG.info("{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.exec_on_ctn(cmd) + + def set_ipv6_forward(self): + cmd = 'sysctl -w net.ipv6.conf.all.forwarding=1' + self.exec_on_ctn(cmd) + + def create_config(self): + raise Exception('implement create_config() method') + + def reload_config(self): + raise Exception('implement reload_config() method') diff --git a/ryu/tests/integrated/common/quagga.py b/ryu/tests/integrated/common/quagga.py new file mode 100644 index 00000000..80c8cf2c --- /dev/null +++ b/ryu/tests/integrated/common/quagga.py @@ -0,0 +1,331 @@ +# Copyright (C) 2015 Nippon Telegraph and Telephone Corporation. +# +# This is based on the following +# https://github.com/osrg/gobgp/test/lib/quagga.py +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import absolute_import + +import logging + +import netaddr + +from . import docker_base as base + +LOG = logging.getLogger(__name__) + + +class QuaggaBGPContainer(base.BGPContainer): + + WAIT_FOR_BOOT = 1 + SHARED_VOLUME = '/etc/quagga' + + def __init__(self, name, asn, router_id, ctn_image_name, 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 + self._create_config_debian() + + def run(self, wait=False): + w_time = super(QuaggaBGPContainer, + self).run(wait=wait, w_time=self.WAIT_FOR_BOOT) + return w_time + + def get_global_rib(self, prefix='', rf='ipv4'): + rib = [] + if prefix != '': + return self.get_global_rib_with_prefix(prefix, rf) + + out = self.vtysh('show bgp {0} unicast'.format(rf), config=False) + if out.startswith('No BGP network exists'): + return rib + + read_next = False + + for line in out.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 = [] + + lines = [line.strip() for line in self.vtysh( + 'show bgp {0} unicast {1}'.format(rf, prefix), + config=False).split('\n')] + + 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)) + + if lines[0] == 'Local': + aspath = [] + else: + 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': base.BGP_ATTR_TYPE_MULTI_EXIT_DISC, + 'metric': int(med)}) + if 'localpref' in info: + localpref = info[info.index('localpref') + 1] + attrs.append({'type': base.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] + + info = [l.strip() for l in self.vtysh( + 'show bgp neighbors {0}'.format(neigh_addr), + config=False).split('\n')] + + if not info[0].startswith('BGP neighbor is'): + raise Exception('unknown format') + + 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 base.BGP_FSM_IDLE + elif state.startswith('Active'): + return base.BGP_FSM_ACTIVE + elif state.startswith('Established'): + return base.BGP_FSM_ESTABLISHED + else: + return state + + raise Exception('not found peer {0}'.format(peer.router_id)) + + def send_route_refresh(self): + self.vtysh('clear ip bgp * soft', config=False) + + def create_config(self): + zebra = 'no' + self._create_config_bgp() + if self.zebra: + zebra = 'yes' + self._create_config_zebra() + self._create_config_daemons(zebra) + + def _create_config_debian(self): + c = base.CmdBuffer() + c << 'vtysh_enable=yes' + c << 'zebra_options=" --daemon -A 127.0.0.1"' + c << 'bgpd_options=" --daemon -A 127.0.0.1"' + c << 'ospfd_options=" --daemon -A 127.0.0.1"' + c << 'ospf6d_options=" --daemon -A ::1"' + c << 'ripd_options=" --daemon -A 127.0.0.1"' + c << 'ripngd_options=" --daemon -A ::1"' + c << 'isisd_options=" --daemon -A 127.0.0.1"' + c << 'babeld_options=" --daemon -A 127.0.0.1"' + c << 'watchquagga_enable=yes' + c << 'watchquagga_options=(--daemon)' + with open('{0}/debian.conf'.format(self.config_dir), 'w') as f: + LOG.info('[{0}\'s new config]'.format(self.name)) + LOG.info(str(c)) + f.writelines(str(c)) + + def _create_config_daemons(self, zebra='no'): + c = base.CmdBuffer() + c << 'zebra=%s' % zebra + c << 'bgpd=yes' + c << 'ospfd=no' + c << 'ospf6d=no' + c << 'ripd=no' + c << 'ripngd=no' + c << 'isisd=no' + c << 'babeld=no' + with open('{0}/daemons'.format(self.config_dir), 'w') as f: + LOG.info('[{0}\'s new config]'.format(self.name)) + LOG.info(str(c)) + f.writelines(str(c)) + + def _create_config_bgp(self): + + c = base.CmdBuffer() + c << 'hostname bgpd' + c << 'password zebra' + c << 'router bgp {0}'.format(self.asn) + c << 'bgp router-id {0}'.format(self.router_id) + if any(info['graceful_restart'] for info in self.peers.values()): + c << 'bgp graceful-restart' + + version = 4 + for peer, info in self.peers.items(): + 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) + if info['is_rs_client']: + c << 'neighbor {0} route-server-client'.format(n_addr) + for typ, p in info['policies'].items(): + c << 'neighbor {0} route-map {1} {2}'.format(n_addr, p['name'], + typ) + if info['passwd']: + c << 'neighbor {0} password {1}'.format(n_addr, info['passwd']) + if info['passive']: + c << 'neighbor {0} passive'.format(n_addr) + if version == 6: + c << 'address-family ipv6 unicast' + c << 'neighbor {0} activate'.format(n_addr) + c << 'exit-address-family' + + for route in self.routes.values(): + 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.items(): + 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: + LOG.info('[{0}\'s new config]'.format(self.name)) + LOG.info(str(c)) + f.writelines(str(c)) + + def _create_config_zebra(self): + c = base.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: + LOG.info('[{0}\'s new config]'.format(self.name)) + LOG.info(str(c)) + f.writelines(str(c)) + + def vtysh(self, cmd, config=True): + if type(cmd) is not list: + cmd = [cmd] + cmd = ' '.join("-c '{0}'".format(c) for c in cmd) + if config: + return self.exec_on_ctn( + "vtysh -d bgpd -c 'en' -c 'conf t' -c " + "'router bgp {0}' {1}".format(self.asn, cmd), + capture=True) + else: + return self.exec_on_ctn("vtysh -d bgpd {0}".format(cmd), + capture=True) + + 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.exec_on_ctn(cmd, capture=True) + + +class RawQuaggaBGPContainer(QuaggaBGPContainer): + def __init__(self, name, config, ctn_image_name, + zebra=False): + asn = None + router_id = None + for line in config.split('\n'): + line = line.strip() + if line.startswith('router bgp'): + asn = int(line[len('router bgp'):].strip()) + if line.startswith('bgp router-id'): + router_id = line[len('bgp router-id'):].strip() + if not asn: + raise Exception('asn not in quagga config') + if not router_id: + raise Exception('router-id not in quagga config') + self.config = config + super(RawQuaggaBGPContainer, self).__init__(name, asn, router_id, + ctn_image_name, zebra) + + def create_config(self): + with open('{0}/bgpd.conf'.format(self.config_dir), 'w') as f: + LOG.info('[{0}\'s new config]'.format(self.name)) + LOG.info(self.config) + f.writelines(self.config) diff --git a/ryu/tests/integrated/common/ryubgp.py b/ryu/tests/integrated/common/ryubgp.py new file mode 100644 index 00000000..5b5114a3 --- /dev/null +++ b/ryu/tests/integrated/common/ryubgp.py @@ -0,0 +1,211 @@ +# Copyright (C) 2016 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 __future__ import absolute_import + +import logging +import time + +import netaddr + +from . import docker_base as base + +LOG = logging.getLogger(__name__) + + +class RyuBGPContainer(base.BGPContainer): + + WAIT_FOR_BOOT = 1 + SHARED_VOLUME = '/etc/ryu' + + def __init__(self, name, asn, router_id, ctn_image_name): + super(RyuBGPContainer, self).__init__(name, asn, router_id, + ctn_image_name) + self.RYU_CONF = self.config_dir + '/ryu.conf' + self.SHARED_RYU_CONF = self.SHARED_VOLUME + '/ryu.conf' + self.SHARED_BGP_CONF = self.SHARED_VOLUME + '/bgp_conf.py' + self.shared_volumes.append((self.config_dir, self.SHARED_VOLUME)) + + def _create_config_ryu(self): + c = base.CmdBuffer() + c << '[DEFAULT]' + c << 'verbose=True' + c << 'log_file=/etc/ryu/manager.log' + with open(self.RYU_CONF, 'w') as f: + LOG.info("[%s's new config]" % self.name) + LOG.info(str(c)) + f.writelines(str(c)) + + def _create_config_ryu_bgp(self): + c = base.CmdBuffer() + c << 'import os' + c << '' + c << 'BGP = {' + c << " 'local_as': %s," % str(self.asn) + c << " 'router_id': '%s'," % self.router_id + c << " 'neighbors': [" + c << " {" + for peer, info in self.peers.items(): + n_addr = info['neigh_addr'].split('/')[0] + c << " 'address': '%s'," % n_addr + c << " 'remote_as': %s," % str(peer.asn) + if netaddr.IPNetwork(n_addr).version == 4: + c << " 'enable_ipv4': True," + c << " 'enable_vpnv4': True," + c << ' },' + c << ' ],' + c << " 'routes': [" + for route in self.routes.values(): + c << " {" + c << " 'prefix': '%s'," % route['prefix'] + c << " }," + c << " ]," + c << "}" + log_conf = """LOGGING = { + + # We use python logging package for logging. + 'version': 1, + 'disable_existing_loggers': False, + + 'formatters': { + 'verbose': { + 'format': '%(levelname)s %(asctime)s %(module)s ' + + '[%(process)d %(thread)d] %(message)s' + }, + 'simple': { + 'format': '%(levelname)s %(asctime)s %(module)s %(lineno)s ' + + '%(message)s' + }, + 'stats': { + 'format': '%(message)s' + }, + }, + + 'handlers': { + # Outputs log to console. + 'console': { + 'level': 'DEBUG', + 'class': 'logging.StreamHandler', + 'formatter': 'simple' + }, + 'console_stats': { + 'level': 'DEBUG', + 'class': 'logging.StreamHandler', + 'formatter': 'stats' + }, + # Rotates log file when its size reaches 10MB. + 'log_file': { + 'level': 'DEBUG', + 'class': 'logging.handlers.RotatingFileHandler', + 'filename': os.path.join('.', 'bgpspeaker.log'), + 'maxBytes': '10000000', + 'formatter': 'verbose' + }, + 'stats_file': { + 'level': 'DEBUG', + 'class': 'logging.handlers.RotatingFileHandler', + 'filename': os.path.join('.', 'statistics_bgps.log'), + 'maxBytes': '10000000', + 'formatter': 'stats' + }, + }, + + # Fine-grained control of logging per instance. + 'loggers': { + 'bgpspeaker': { + 'handlers': ['console', 'log_file'], + 'handlers': ['console'], + 'level': 'DEBUG', + 'propagate': False, + }, + 'stats': { + 'handlers': ['stats_file', 'console_stats'], + 'level': 'INFO', + 'propagate': False, + 'formatter': 'stats', + }, + }, + + # Root loggers. + 'root': { + 'handlers': ['console', 'log_file'], + 'level': 'DEBUG', + 'propagate': True, + }, +}""" + c << log_conf + with open(self.config_dir + '/bgp_conf.py', 'w') as f: + LOG.info("[%s's new config]" % self.name) + LOG.info(str(c)) + f.writelines(str(c)) + + def create_config(self): + self._create_config_ryu() + self._create_config_ryu_bgp() + + def is_running_ryu(self): + results = self.exec_on_ctn('ps ax') + running = False + for line in results.split('\n')[1:]: + if 'ryu-manager' in line: + running = True + return running + + def start_ryubgp(self, check_running=True, retry=False): + if check_running: + if self.is_running_ryu(): + return True + result = False + if retry: + try_times = 3 + else: + try_times = 1 + cmd = "ryu-manager --verbose " + cmd += "--bgp-app-config-file %s " % self.SHARED_BGP_CONF + cmd += "ryu.services.protocols.bgp.application" + for i in range(try_times): + self.exec_on_ctn(cmd, detach=True) + if self.is_running_ryu(): + result = True + break + time.sleep(1) + return result + + def stop_ryubgp(self, check_running=True, retry=False): + if check_running: + if not self.is_running_ryu(): + return True + result = False + if retry: + try_times = 3 + else: + try_times = 1 + for i in range(try_times): + cmd = '/usr/bin/pkill ryu-manager -SIGTERM' + self.exec_on_ctn(cmd) + if not self.is_running_ryu(): + result = True + break + time.sleep(1) + return result + + def run(self, wait=False): + w_time = super(RyuBGPContainer, + self).run(wait=wait, w_time=self.WAIT_FOR_BOOT) + return w_time + + def reload_config(self): + self.stop_ryubgp(retry=True) + self.start_ryubgp(retry=True) |