diff options
Diffstat (limited to 'test/lib/yabgp.py')
-rw-r--r-- | test/lib/yabgp.py | 476 |
1 files changed, 476 insertions, 0 deletions
diff --git a/test/lib/yabgp.py b/test/lib/yabgp.py new file mode 100644 index 00000000..c3e5e0ac --- /dev/null +++ b/test/lib/yabgp.py @@ -0,0 +1,476 @@ +# Copyright (C) 2017 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 +from __future__ import print_function + +import json +import os +import time + +from fabric import colors +from fabric.api import local +from fabric.utils import indent + +from lib.base import ( + FLOWSPEC_NAME_TO_TYPE, + BGPContainer, + CmdBuffer, + try_several_times, +) + + +class YABGPContainer(BGPContainer): + + WAIT_FOR_BOOT = 1 + SHARED_VOLUME = '/etc/yabgp' + + def __init__(self, name, asn, router_id, + ctn_image_name='osrg/yabgp:v0.4.0'): + super(YABGPContainer, self).__init__(name, asn, router_id, + ctn_image_name) + self.shared_volumes.append((self.config_dir, self.SHARED_VOLUME)) + + def _copy_helper_app(self): + import lib + mod_dir = os.path.dirname(lib.__file__) + local('docker cp {0}/yabgp_helper.py' + ' {1}:/root/'.format(mod_dir, self.name)) + + def _start_yabgp(self): + self.local( + 'python /root/yabgp_helper.py' + ' --config-file {0}/yabgp.ini'.format(self.SHARED_VOLUME), + detach=True) + + def run(self): + super(YABGPContainer, self).run() + # self.create_config() is called in super class + self._copy_helper_app() + self._start_yabgp() + return self.WAIT_FOR_BOOT + + def create_config(self): + # Currently, supports only single peer + c = CmdBuffer('\n') + c << '[DEFAULT]' + c << 'log_dir = {0}'.format(self.SHARED_VOLUME) + c << 'use_stderr = False' + c << '[message]' + c << 'write_disk = True' + c << 'write_dir = {0}/data/bgp/'.format(self.SHARED_VOLUME) + c << 'format = json' + + if self.peers: + info = next(iter(self.peers.values())) + remote_as = info['remote_as'] + neigh_addr = info['neigh_addr'].split('/')[0] + local_as = info['local_as'] or self.asn + local_addr = info['local_addr'].split('/')[0] + c << '[bgp]' + c << 'afi_safi = ipv4, ipv6, vpnv4, vpnv6, flowspec, evpn' + c << 'remote_as = {0}'.format(remote_as) + c << 'remote_addr = {0}'.format(neigh_addr) + c << 'local_as = {0}'.format(local_as) + c << 'local_addr = {0}'.format(local_addr) + + with open('{0}/yabgp.ini'.format(self.config_dir), 'w') as f: + print(colors.yellow('[{0}\'s new yabgp.ini]'.format(self.name))) + print(colors.yellow(indent(str(c)))) + f.writelines(str(c)) + + def reload_config(self): + if self.peers == 0: + return + + def _reload(): + def _is_running(): + ps = self.local('ps -ef', capture=True) + running = False + for line in ps.split('\n'): + if 'yabgp_helper' in line: + running = True + return running + + if _is_running(): + self.local('/usr/bin/pkill -9 python') + + self._start_yabgp() + time.sleep(self.WAIT_FOR_BOOT) + if not _is_running(): + raise RuntimeError() + + try_several_times(_reload) + + def _curl_send_update(self, path, peer): + c = CmdBuffer(' ') + c << "curl -X POST" + c << "-u admin:admin" + c << "-H 'Content-Type: application/json'" + c << "http://localhost:8801/v1/peer/{0}/send/update".format(peer) + c << "-d '{0}'".format(json.dumps(path)) + return json.loads(self.local(str(c), capture=True)) + + def _construct_ip_unicast_update(self, rf, prefix, nexthop): + # YABGP v0.4.0 + # + # IPv4 Unicast: + # curl -X POST \ + # -u admin:admin \ + # -H 'Content-Type: application/json' \ + # http://localhost:8801/v1/peer/172.17.0.2/send/update -d '{ + # "attr": { + # "1": 0, + # "2": [ + # [ + # 2, + # [ + # 1, + # 2, + # 3 + # ] + # ] + # ], + # "3": "192.0.2.1", + # "5": 500 + # }, + # "nlri": [ + # "172.20.1.0/24", + # "172.20.2.0/24" + # ] + # }' + # + # IPv6 Unicast: + # curl -X POST \ + # -u admin:admin \ + # -H 'Content-Type: application/json' \ + # http://localhost:8801/v1/peer/172.17.0.2/send/update -d '{ + # "attr": { + # "1": 0, + # "2": [ + # [ + # 2, + # [ + # 65502 + # ] + # ] + # ], + # "4": 0, + # "14": { + # "afi_safi": [ + # 2, + # 1 + # ], + # "linklocal_nexthop": "fe80::c002:bff:fe7e:0", + # "nexthop": "2001:db8::2", + # "nlri": [ + # "::2001:db8:2:2/64", + # "::2001:db8:2:1/64", + # "::2001:db8:2:0/64" + # ] + # } + # } + # }' + if rf == 'ipv4': + return { + "attr": { + "3": nexthop, + }, + "nlri": [prefix], + } + elif rf == 'ipv6': + return { + "attr": { + "14": { # MP_REACH_NLRI + "afi_safi": [2, 1], + "nexthop": nexthop, + "nlri": [prefix], + }, + }, + } + else: + raise ValueError( + 'invalid address family for ipv4/ipv6 unicast: %s' % rf) + + def _construct_ip_unicast_withdraw(self, rf, prefix): + # YABGP v0.4.0 + # + # IPv4 Unicast: + # curl -X POST \ + # -u admin:admin \ + # -H 'Content-Type: application/json' \ + # http://localhost:8801/v1/peer/172.17.0.2/send/update -d '{ + # "withdraw": [ + # "172.20.1.0/24", + # "172.20.2.0/24" + # ] + # }' + # + # IPv6 Unicast: + # curl -X POST \ + # -u admin:admin \ + # -H 'Content-Type: application/json' \ + # http://localhost:8801/v1/peer/172.17.0.2/send/update -d '{ + # "attr": { + # "15": { + # "afi_safi": [ + # 2, + # 1 + # ], + # "withdraw": [ + # "::2001:db8:2:2/64", + # "::2001:db8:2:1/64", + # "::2001:db8:2:0/64" + # ] + # } + # } + # }' + if rf == 'ipv4': + return { + "withdraw": [prefix], + } + elif rf == 'ipv6': + return { + "attr": { + "15": { # MP_UNREACH_NLRI + "afi_safi": [2, 1], + "withdraw": [prefix], + }, + }, + } + else: + raise ValueError( + 'invalid address family for ipv4/ipv6 unicast: %s' % rf) + + def _construct_flowspec_match(self, matchs): + assert isinstance(matchs, (tuple, list)) + ret = {} + for m in matchs: + # m = "source-port '!=2 !=22&!=222'" + # typ = "source-port" + # args = "'!=2 !=22&!=222'" + typ, args = m.split(' ', 1) + # t = 6 + t = FLOWSPEC_NAME_TO_TYPE.get(typ, None) + if t is None: + raise ValueError('invalid flowspec match type: %s' % typ) + # args = "!=2|!=22&!=222" + args = args.strip("'").strip('"').replace(' ', '|') + ret[t] = args + return ret + + def _construct_flowspec_update(self, rf, matchs, thens): + # YABGP v0.4.0 + # + # curl -X POST \ + # -u admin:admin \ + # -H 'Content-Type: application/json' \ + # http://localhost:8801/v1/peer/172.17.0.2/send/update -d '{ + # "attr": { + # "1": 0, + # "14": { + # "afi_safi": [ + # 1, + # 133 + # ], + # "nexthop": "", + # "nlri": [ + # { + # "1": "10.0.0.0/24" + # } + # ] + # }, + # "16": [ + # "traffic-rate:0:0" + # ], + # "2": [], + # "5": 100 + # } + # }' + # + # Format of "thens": + # "traffic-rate:<AS>:<rate>" + # "traffic-marking-dscp:<int value>" + # "redirect-nexthop:<int value>" + # "redirect-vrf:<RT>" + thens = thens or [] + if rf == 'ipv4-flowspec': + afi_safi = [1, 133] + else: + raise ValueError('invalid address family for flowspec: %s' % rf) + + return { + "attr": { + "14": { # MP_REACH_NLRI + "afi_safi": afi_safi, + "nexthop": "", + "nlri": [self._construct_flowspec_match(matchs)] + }, + "16": thens, # EXTENDED COMMUNITIES + }, + } + + def _construct_flowspec_withdraw(self, rf, matchs): + # curl -X POST \ + # -u admin:admin \ + # -H 'Content-Type: application/json' \ + # http://localhost:8801/v1/peer/172.17.0.2/send/update -d '{ + # "attr": { + # "15": { + # "afi_safi": [ + # 1, + # 133 + # ], + # "withdraw": [ + # { + # "1": "192.88.2.3/24", + # "2": "192.89.1.3/24" + # }, + # { + # "1": "192.88.4.3/24", + # "2": "192.89.2.3/24" + # } + # ] + # } + # } + # }' + if rf == 'ipv4-flowspec': + afi_safi = [1, 133] + else: + raise ValueError('invalid address family for flowspec: %s' % rf) + + return { + "attr": { + "15": { # MP_UNREACH_NLRI + "afi_safi": afi_safi, + "withdraw": [self._construct_flowspec_match(matchs)], + }, + }, + } + + def _update_path_attributes(self, path, aspath=None, med=None, + local_pref=None): + # ORIGIN: Currently support only IGP(0) + path['attr']['1'] = 0 + # AS_PATH: Currently support only AS_SEQUENCE(2) + if aspath is None: + path['attr']['2'] = [] + else: + path['attr']['2'] = [[2, aspath]] + # MED + if med is not None: + path['attr']['4'] = med + # LOCAL_PREF + if local_pref is not None: + path['attr']['5'] = local_pref + # TODO: + # Support COMMUNITY and EXTENDED COMMUNITIES + + return path + + 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, identifier=None, reload_config=True): + self.routes.setdefault(route, []) + + for info in self.peers.values(): + peer = info['neigh_addr'].split('/')[0] + + if rf in ['ipv4', 'ipv6']: + nexthop = nexthop or info['local_addr'].split('/')[0] + path = self._construct_ip_unicast_update( + rf, route, nexthop) + # TODO: + # Support "evpn" address family + elif rf in ['ipv4-flowspec', 'ipv6-flowspec']: + path = self._construct_flowspec_update( + rf, matchs, thens) + else: + raise ValueError('unsupported address family: %s' % rf) + + self._update_path_attributes( + path, aspath=aspath, med=med, local_pref=local_pref) + + self._curl_send_update(path, peer) + + self.routes[route].append({ + 'prefix': route, + 'rf': rf, + 'attr': attribute, + 'next-hop': nexthop, + 'as-path': aspath, + 'community': community, + 'med': med, + 'local-pref': local_pref, + 'extended-community': extendedcommunity, + 'identifier': identifier, + 'matchs': matchs, + 'thens': thens, + }) + + def del_route(self, route, identifier=None, reload_config=True): + new_paths = [] + withdraw = None + for p in self.routes.get(route, []): + if p['identifier'] != identifier: + new_paths.append(p) + else: + withdraw = p + + if not withdraw: + return + rf = withdraw['rf'] + + for info in self.peers.values(): + peer = info['neigh_addr'].split('/')[0] + + if rf in ['ipv4', 'ipv6']: + r = self._construct_ip_unicast_withdraw(rf, route) + elif rf == 'ipv4-flowspec': + # NOTE: "ipv6-flowspec" does not seem to be supported with + # YABGP v0.4.0 + matchs = withdraw['matchs'] + r = self._construct_flowspec_withdraw(rf, matchs) + else: + raise ValueError('unsupported address family: %s' % rf) + + self._curl_send_update(r, peer) + + self.routes[route] = new_paths + + def _get_adj_rib(self, peer, in_out='in'): + peer_addr = self.peer_name(peer) + c = CmdBuffer(' ') + c << "curl -X GET" + c << "-u admin:admin" + c << "-H 'Content-Type: application/json'" + c << "http://localhost:8801/v1-ext/peer/{0}/adj-rib-{1}".format( + peer_addr, in_out) + return json.loads(self.local(str(c), capture=True)) + + def get_adj_rib_in(self, peer, rf='ipv4'): + # "rf" should be either of; + # ipv4, ipv6, vpnv4, vpnv6, flowspec, evpn + # The same as supported "afi_safi" in yabgp.ini + ribs = self._get_adj_rib(peer, 'in') + return ribs.get(rf, {}) + + def get_adj_rib_out(self, peer, rf='ipv4'): + # "rf" should be either of; + # ipv4, ipv6, vpnv4, vpnv6, flowspec, evpn + # The same as supported "afi_safi" in yabgp.ini + ribs = self._get_adj_rib(peer, 'out') + return ribs.get(rf, {}) |