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>
This commit is contained in:
fumihiko kakuma 2016-10-29 06:27:10 +09:00 committed by FUJITA Tomonori
parent 649d31afbe
commit 0a60252208
4 changed files with 1350 additions and 0 deletions

View File

View File

@ -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')

View File

@ -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)

View File

@ -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)