github.com/osrg/gobgp@v2.0.0+incompatible/test/lib/yabgp.py (about) 1 # Copyright (C) 2017 Nippon Telegraph and Telephone Corporation. 2 # 3 # Licensed under the Apache License, Version 2.0 (the "License"); 4 # you may not use this file except in compliance with the License. 5 # You may obtain a copy of the License at 6 # 7 # http://www.apache.org/licenses/LICENSE-2.0 8 # 9 # Unless required by applicable law or agreed to in writing, software 10 # distributed under the License is distributed on an "AS IS" BASIS, 11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 # implied. 13 # See the License for the specific language governing permissions and 14 # limitations under the License. 15 16 from __future__ import absolute_import 17 from __future__ import print_function 18 19 import json 20 import os 21 22 from fabric import colors 23 from fabric.api import local 24 from fabric.utils import indent 25 26 from lib.base import ( 27 FLOWSPEC_NAME_TO_TYPE, 28 BGPContainer, 29 CmdBuffer, 30 try_several_times, 31 wait_for_completion, 32 ) 33 34 35 class YABGPContainer(BGPContainer): 36 37 WAIT_FOR_BOOT = 1 38 SHARED_VOLUME = '/etc/yabgp' 39 40 def __init__(self, name, asn, router_id, 41 ctn_image_name='osrg/yabgp:v0.4.0'): 42 super(YABGPContainer, self).__init__(name, asn, router_id, 43 ctn_image_name) 44 self.shared_volumes.append((self.config_dir, self.SHARED_VOLUME)) 45 46 def _copy_helper_app(self): 47 import lib 48 mod_dir = os.path.dirname(lib.__file__) 49 local('docker cp {0}/yabgp_helper.py' 50 ' {1}:/root/'.format(mod_dir, self.name)) 51 52 def _start_yabgp(self): 53 self.local( 54 'python /root/yabgp_helper.py' 55 ' --config-file {0}/yabgp.ini'.format(self.SHARED_VOLUME), 56 detach=True) 57 58 def _wait_for_boot(self): 59 return wait_for_completion(self._curl_is_running) 60 61 def run(self): 62 super(YABGPContainer, self).run() 63 # self.create_config() is called in super class 64 self._copy_helper_app() 65 # To start YABGP, it is required to configure neighbor settings, so 66 # here does not start YABGP yet. 67 # self._start_yabgp() 68 # self._wait_for_boot() 69 return self.WAIT_FOR_BOOT 70 71 def create_config(self): 72 # Currently, supports only single peer 73 c = CmdBuffer('\n') 74 c << '[DEFAULT]' 75 c << 'log_dir = {0}'.format(self.SHARED_VOLUME) 76 c << 'use_stderr = False' 77 c << '[message]' 78 c << 'write_disk = True' 79 c << 'write_dir = {0}/data/bgp/'.format(self.SHARED_VOLUME) 80 c << 'format = json' 81 82 if self.peers: 83 info = next(iter(self.peers.values())) 84 remote_as = info['remote_as'] 85 neigh_addr = info['neigh_addr'].split('/')[0] 86 local_as = info['local_as'] or self.asn 87 local_addr = info['local_addr'].split('/')[0] 88 c << '[bgp]' 89 c << 'afi_safi = ipv4, ipv6, vpnv4, vpnv6, flowspec, evpn' 90 c << 'remote_as = {0}'.format(remote_as) 91 c << 'remote_addr = {0}'.format(neigh_addr) 92 c << 'local_as = {0}'.format(local_as) 93 c << 'local_addr = {0}'.format(local_addr) 94 95 with open('{0}/yabgp.ini'.format(self.config_dir), 'w') as f: 96 print(colors.yellow('[{0}\'s new yabgp.ini]'.format(self.name))) 97 print(colors.yellow(indent(str(c)))) 98 f.writelines(str(c)) 99 100 def reload_config(self): 101 if self.peers == 0: 102 return 103 104 def _reload(): 105 def _is_running(): 106 ps = self.local('ps -ef', capture=True) 107 running = False 108 for line in ps.split('\n'): 109 if 'yabgp_helper' in line: 110 running = True 111 return running 112 113 if _is_running(): 114 self.local('/usr/bin/pkill -9 python') 115 116 self._start_yabgp() 117 self._wait_for_boot() 118 if not _is_running(): 119 raise RuntimeError() 120 121 try_several_times(_reload) 122 123 def _curl_is_running(self): 124 c = CmdBuffer(' ') 125 c << "curl -X GET" 126 c << "-u admin:admin" 127 c << "-H 'Content-Type: application/json'" 128 c << "http://localhost:8801/v1/" 129 c << "> /dev/null 2>&1; echo $?" 130 return self.local(str(c), capture=True) == '0' 131 132 def _curl_send_update(self, path, peer): 133 c = CmdBuffer(' ') 134 c << "curl -X POST" 135 c << "-u admin:admin" 136 c << "-H 'Content-Type: application/json'" 137 c << "http://localhost:8801/v1/peer/{0}/send/update".format(peer) 138 c << "-d '{0}'".format(json.dumps(path)) 139 return json.loads(self.local(str(c), capture=True)) 140 141 def _construct_ip_unicast_update(self, rf, prefix, nexthop): 142 # YABGP v0.4.0 143 # 144 # IPv4 Unicast: 145 # curl -X POST \ 146 # -u admin:admin \ 147 # -H 'Content-Type: application/json' \ 148 # http://localhost:8801/v1/peer/172.17.0.2/send/update -d '{ 149 # "attr": { 150 # "1": 0, 151 # "2": [ 152 # [ 153 # 2, 154 # [ 155 # 1, 156 # 2, 157 # 3 158 # ] 159 # ] 160 # ], 161 # "3": "192.0.2.1", 162 # "5": 500 163 # }, 164 # "nlri": [ 165 # "172.20.1.0/24", 166 # "172.20.2.0/24" 167 # ] 168 # }' 169 # 170 # IPv6 Unicast: 171 # curl -X POST \ 172 # -u admin:admin \ 173 # -H 'Content-Type: application/json' \ 174 # http://localhost:8801/v1/peer/172.17.0.2/send/update -d '{ 175 # "attr": { 176 # "1": 0, 177 # "2": [ 178 # [ 179 # 2, 180 # [ 181 # 65502 182 # ] 183 # ] 184 # ], 185 # "4": 0, 186 # "14": { 187 # "afi_safi": [ 188 # 2, 189 # 1 190 # ], 191 # "linklocal_nexthop": "fe80::c002:bff:fe7e:0", 192 # "nexthop": "2001:db8::2", 193 # "nlri": [ 194 # "::2001:db8:2:2/64", 195 # "::2001:db8:2:1/64", 196 # "::2001:db8:2:0/64" 197 # ] 198 # } 199 # } 200 # }' 201 if rf == 'ipv4': 202 return { 203 "attr": { 204 "3": nexthop, 205 }, 206 "nlri": [prefix], 207 } 208 elif rf == 'ipv6': 209 return { 210 "attr": { 211 "14": { # MP_REACH_NLRI 212 "afi_safi": [2, 1], 213 "nexthop": nexthop, 214 "nlri": [prefix], 215 }, 216 }, 217 } 218 else: 219 raise ValueError( 220 'invalid address family for ipv4/ipv6 unicast: %s' % rf) 221 222 def _construct_ip_unicast_withdraw(self, rf, prefix): 223 # YABGP v0.4.0 224 # 225 # IPv4 Unicast: 226 # curl -X POST \ 227 # -u admin:admin \ 228 # -H 'Content-Type: application/json' \ 229 # http://localhost:8801/v1/peer/172.17.0.2/send/update -d '{ 230 # "withdraw": [ 231 # "172.20.1.0/24", 232 # "172.20.2.0/24" 233 # ] 234 # }' 235 # 236 # IPv6 Unicast: 237 # curl -X POST \ 238 # -u admin:admin \ 239 # -H 'Content-Type: application/json' \ 240 # http://localhost:8801/v1/peer/172.17.0.2/send/update -d '{ 241 # "attr": { 242 # "15": { 243 # "afi_safi": [ 244 # 2, 245 # 1 246 # ], 247 # "withdraw": [ 248 # "::2001:db8:2:2/64", 249 # "::2001:db8:2:1/64", 250 # "::2001:db8:2:0/64" 251 # ] 252 # } 253 # } 254 # }' 255 if rf == 'ipv4': 256 return { 257 "withdraw": [prefix], 258 } 259 elif rf == 'ipv6': 260 return { 261 "attr": { 262 "15": { # MP_UNREACH_NLRI 263 "afi_safi": [2, 1], 264 "withdraw": [prefix], 265 }, 266 }, 267 } 268 else: 269 raise ValueError( 270 'invalid address family for ipv4/ipv6 unicast: %s' % rf) 271 272 def _construct_flowspec_match(self, matchs): 273 assert isinstance(matchs, (tuple, list)) 274 ret = {} 275 for m in matchs: 276 # m = "source-port '!=2 !=22&!=222'" 277 # typ = "source-port" 278 # args = "'!=2 !=22&!=222'" 279 typ, args = m.split(' ', 1) 280 # t = 6 281 t = FLOWSPEC_NAME_TO_TYPE.get(typ, None) 282 if t is None: 283 raise ValueError('invalid flowspec match type: %s' % typ) 284 # args = "!=2|!=22&!=222" 285 args = args.strip("'").strip('"').replace(' ', '|') 286 ret[t] = args 287 return ret 288 289 def _construct_flowspec_update(self, rf, matchs, thens): 290 # YABGP v0.4.0 291 # 292 # curl -X POST \ 293 # -u admin:admin \ 294 # -H 'Content-Type: application/json' \ 295 # http://localhost:8801/v1/peer/172.17.0.2/send/update -d '{ 296 # "attr": { 297 # "1": 0, 298 # "14": { 299 # "afi_safi": [ 300 # 1, 301 # 133 302 # ], 303 # "nexthop": "", 304 # "nlri": [ 305 # { 306 # "1": "10.0.0.0/24" 307 # } 308 # ] 309 # }, 310 # "16": [ 311 # "traffic-rate:0:0" 312 # ], 313 # "2": [], 314 # "5": 100 315 # } 316 # }' 317 # 318 # Format of "thens": 319 # "traffic-rate:<AS>:<rate>" 320 # "traffic-marking-dscp:<int value>" 321 # "redirect-nexthop:<int value>" 322 # "redirect-vrf:<RT>" 323 thens = thens or [] 324 if rf == 'ipv4-flowspec': 325 afi_safi = [1, 133] 326 else: 327 raise ValueError('invalid address family for flowspec: %s' % rf) 328 329 return { 330 "attr": { 331 "14": { # MP_REACH_NLRI 332 "afi_safi": afi_safi, 333 "nexthop": "", 334 "nlri": [self._construct_flowspec_match(matchs)] 335 }, 336 "16": thens, # EXTENDED COMMUNITIES 337 }, 338 } 339 340 def _construct_flowspec_withdraw(self, rf, matchs): 341 # curl -X POST \ 342 # -u admin:admin \ 343 # -H 'Content-Type: application/json' \ 344 # http://localhost:8801/v1/peer/172.17.0.2/send/update -d '{ 345 # "attr": { 346 # "15": { 347 # "afi_safi": [ 348 # 1, 349 # 133 350 # ], 351 # "withdraw": [ 352 # { 353 # "1": "192.88.2.3/24", 354 # "2": "192.89.1.3/24" 355 # }, 356 # { 357 # "1": "192.88.4.3/24", 358 # "2": "192.89.2.3/24" 359 # } 360 # ] 361 # } 362 # } 363 # }' 364 if rf == 'ipv4-flowspec': 365 afi_safi = [1, 133] 366 else: 367 raise ValueError('invalid address family for flowspec: %s' % rf) 368 369 return { 370 "attr": { 371 "15": { # MP_UNREACH_NLRI 372 "afi_safi": afi_safi, 373 "withdraw": [self._construct_flowspec_match(matchs)], 374 }, 375 }, 376 } 377 378 def _update_path_attributes(self, path, aspath=None, med=None, 379 local_pref=None): 380 # ORIGIN: Currently support only IGP(0) 381 path['attr']['1'] = 0 382 # AS_PATH: Currently support only AS_SEQUENCE(2) 383 if aspath is None: 384 path['attr']['2'] = [] 385 else: 386 path['attr']['2'] = [[2, aspath]] 387 # MED 388 if med is not None: 389 path['attr']['4'] = med 390 # LOCAL_PREF 391 if local_pref is not None: 392 path['attr']['5'] = local_pref 393 # TODO: 394 # Support COMMUNITY and EXTENDED COMMUNITIES 395 396 return path 397 398 def add_route(self, route, rf='ipv4', attribute=None, aspath=None, 399 community=None, med=None, extendedcommunity=None, 400 nexthop=None, matchs=None, thens=None, 401 local_pref=None, identifier=None, reload_config=True): 402 self.routes.setdefault(route, []) 403 404 for info in self.peers.values(): 405 peer = info['neigh_addr'].split('/')[0] 406 407 if rf in ['ipv4', 'ipv6']: 408 nexthop = nexthop or info['local_addr'].split('/')[0] 409 path = self._construct_ip_unicast_update( 410 rf, route, nexthop) 411 # TODO: 412 # Support "evpn" address family 413 elif rf in ['ipv4-flowspec', 'ipv6-flowspec']: 414 path = self._construct_flowspec_update( 415 rf, matchs, thens) 416 else: 417 raise ValueError('unsupported address family: %s' % rf) 418 419 self._update_path_attributes( 420 path, aspath=aspath, med=med, local_pref=local_pref) 421 422 self._curl_send_update(path, peer) 423 424 self.routes[route].append({ 425 'prefix': route, 426 'rf': rf, 427 'attr': attribute, 428 'next-hop': nexthop, 429 'as-path': aspath, 430 'community': community, 431 'med': med, 432 'local-pref': local_pref, 433 'extended-community': extendedcommunity, 434 'identifier': identifier, 435 'matchs': matchs, 436 'thens': thens, 437 }) 438 439 def del_route(self, route, identifier=None, reload_config=True): 440 new_paths = [] 441 withdraw = None 442 for p in self.routes.get(route, []): 443 if p['identifier'] != identifier: 444 new_paths.append(p) 445 else: 446 withdraw = p 447 448 if not withdraw: 449 return 450 rf = withdraw['rf'] 451 452 for info in self.peers.values(): 453 peer = info['neigh_addr'].split('/')[0] 454 455 if rf in ['ipv4', 'ipv6']: 456 r = self._construct_ip_unicast_withdraw(rf, route) 457 elif rf == 'ipv4-flowspec': 458 # NOTE: "ipv6-flowspec" does not seem to be supported with 459 # YABGP v0.4.0 460 matchs = withdraw['matchs'] 461 r = self._construct_flowspec_withdraw(rf, matchs) 462 else: 463 raise ValueError('unsupported address family: %s' % rf) 464 465 self._curl_send_update(r, peer) 466 467 self.routes[route] = new_paths 468 469 def _get_adj_rib(self, peer, in_out='in'): 470 peer_addr = self.peer_name(peer) 471 c = CmdBuffer(' ') 472 c << "curl -X GET" 473 c << "-u admin:admin" 474 c << "-H 'Content-Type: application/json'" 475 c << "http://localhost:8801/v1-ext/peer/{0}/adj-rib-{1}".format( 476 peer_addr, in_out) 477 return json.loads(self.local(str(c), capture=True)) 478 479 def get_adj_rib_in(self, peer, rf='ipv4'): 480 # "rf" should be either of; 481 # ipv4, ipv6, vpnv4, vpnv6, flowspec, evpn 482 # The same as supported "afi_safi" in yabgp.ini 483 ribs = self._get_adj_rib(peer, 'in') 484 return ribs.get(rf, {}) 485 486 def get_adj_rib_out(self, peer, rf='ipv4'): 487 # "rf" should be either of; 488 # ipv4, ipv6, vpnv4, vpnv6, flowspec, evpn 489 # The same as supported "afi_safi" in yabgp.ini 490 ribs = self._get_adj_rib(peer, 'out') 491 return ribs.get(rf, {})