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