github.com/osrg/gobgp/v3@v3.30.0/test/lib/base.py (about) 1 # Copyright (C) 2015 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 import os 18 import time 19 import itertools 20 21 from invoke import run 22 23 import textwrap 24 from colored import fg, attr 25 26 try: 27 from docker import Client 28 except ImportError: 29 from docker import APIClient as Client 30 import netaddr 31 32 33 DEFAULT_TEST_PREFIX = '' 34 DEFAULT_TEST_BASE_DIR = '/tmp/gobgp' 35 TEST_PREFIX = DEFAULT_TEST_PREFIX 36 TEST_BASE_DIR = DEFAULT_TEST_BASE_DIR 37 38 BGP_FSM_IDLE = 'idle' 39 BGP_FSM_ACTIVE = 'active' 40 BGP_FSM_ESTABLISHED = 'established' 41 42 BGP_ATTR_TYPE_ORIGIN = 1 43 BGP_ATTR_TYPE_AS_PATH = 2 44 BGP_ATTR_TYPE_NEXT_HOP = 3 45 BGP_ATTR_TYPE_MULTI_EXIT_DISC = 4 46 BGP_ATTR_TYPE_LOCAL_PREF = 5 47 BGP_ATTR_TYPE_COMMUNITIES = 8 48 BGP_ATTR_TYPE_ORIGINATOR_ID = 9 49 BGP_ATTR_TYPE_CLUSTER_LIST = 10 50 BGP_ATTR_TYPE_MP_REACH_NLRI = 14 51 BGP_ATTR_TYPE_EXTENDED_COMMUNITIES = 16 52 53 GRACEFUL_RESTART_TIME = 30 54 LONG_LIVED_GRACEFUL_RESTART_TIME = 30 55 56 FLOWSPEC_NAME_TO_TYPE = { 57 "destination": 1, 58 "source": 2, 59 "protocol": 3, 60 "port": 4, 61 "destination-port": 5, 62 "source-port": 6, 63 "icmp-type": 7, 64 "icmp-code": 8, 65 "tcp-flags": 9, 66 "packet-length": 10, 67 "dscp": 11, 68 "fragment": 12, 69 "label": 13, 70 "ether-type": 14, 71 "source-mac": 15, 72 "destination-mac": 16, 73 "llc-dsap": 17, 74 "llc-ssap": 18, 75 "llc-control": 19, 76 "snap": 20, 77 "vid": 21, 78 "cos": 22, 79 "inner-vid": 23, 80 "inner-cos": 24, 81 } 82 83 # with this label, we can do filtering in `docker ps` and `docker network prune` 84 TEST_CONTAINER_LABEL = 'gobgp-test' 85 TEST_NETWORK_LABEL = TEST_CONTAINER_LABEL 86 87 88 def local(s, capture=False): 89 print('[localhost] local:', s) 90 _env = {'NOSE_NOLOGCAPTURE': '1' if capture else '0'} 91 return run(s, hide=True, env=_env).stdout.strip() 92 93 94 def yellow(s): 95 return fg('yellow') + str(s) + attr('reset') 96 97 98 def indent(s): 99 return textwrap.indent(str(s), ' '*4, lambda line: True) 100 101 102 def community_str(i): 103 """ 104 Converts integer in to colon separated two bytes decimal strings like 105 BGP Community or Large Community representation. 106 107 For example, this function converts 13107300 = ((200 << 16) | 100) 108 into "200:100". 109 """ 110 values = [] 111 while i > 0: 112 values.append(str(i & 0xffff)) 113 i >>= 16 114 return ':'.join(reversed(values)) 115 116 117 def wait_for_completion(f, timeout=120): 118 interval = 1 119 count = 0 120 while True: 121 if f(): 122 return 123 124 time.sleep(interval) 125 count += interval 126 if count >= timeout: 127 raise Exception('timeout') 128 129 130 def try_several_times(f, t=3, s=1): 131 for _ in range(t): 132 try: 133 r = f() 134 except RuntimeError: 135 time.sleep(s) 136 else: 137 return r 138 raise Exception 139 140 141 def assert_several_times(f, t=30, s=1): 142 e = AssertionError 143 for _ in range(t): 144 try: 145 f() 146 except AssertionError as ae: 147 time.sleep(s) 148 e = ae 149 else: 150 return 151 raise e 152 153 154 def get_bridges(): 155 return try_several_times(lambda: local("docker network ls | awk 'NR > 1{print $2}'", capture=True)).split('\n') 156 157 158 def get_containers(): 159 return try_several_times(lambda: local("docker ps -a | awk 'NR > 1 {print $NF}'", capture=True)).split('\n') 160 161 162 class CmdBuffer(list): 163 def __init__(self, delim='\n'): 164 super(CmdBuffer, self).__init__() 165 self.delim = delim 166 167 def __lshift__(self, value): 168 self.append(value) 169 170 def __str__(self): 171 return self.delim.join(self) 172 173 174 class Bridge(object): 175 def __init__(self, name, subnet='', with_ip=True, self_ip=False): 176 self.name = name 177 if TEST_PREFIX != '': 178 self.name = '{0}_{1}'.format(TEST_PREFIX, name) 179 self.with_ip = with_ip 180 if with_ip: 181 self.subnet = netaddr.IPNetwork(subnet) 182 183 def _f(): 184 for host in self.subnet: 185 yield host 186 self._ip_generator = _f() 187 # throw away first network address 188 self.next_ip_address() 189 190 def f(): 191 if self.name in get_bridges(): 192 self.delete() 193 v6 = '' 194 if self.subnet.version == 6: 195 v6 = '--ipv6' 196 self.id = local('docker network create --driver bridge {0} --subnet {1} --label {2} {3}'.format(v6, subnet, TEST_NETWORK_LABEL, self.name), capture=True) 197 try_several_times(f) 198 199 self.self_ip = self_ip 200 if self_ip: 201 self.ip_addr = self.next_ip_address() 202 try_several_times(lambda: local("ip addr add {0} dev {1}".format(self.ip_addr, self.name))) 203 self.ctns = [] 204 205 # Note: Here removes routes from the container host to prevent traffic 206 # from going through the container host's routing table. 207 if with_ip: 208 local('ip route del {0}; echo $?'.format(subnet), 209 capture=True) 210 # When IPv6, 2 routes will be installed to the container host's 211 # routing table. 212 if self.subnet.version == 6: 213 local('ip -6 route del {0}; echo $?'.format(subnet), 214 capture=True) 215 216 def next_ip_address(self): 217 return "{0}/{1}".format(next(self._ip_generator), 218 self.subnet.prefixlen) 219 220 def addif(self, ctn, ip_addr=''): 221 _name = ctn.next_if_name() 222 self.ctns.append(ctn) 223 ip = '' 224 if not ip_addr == '': 225 ip = '--ip {0}'.format(ip_addr) 226 if self.subnet.version == 6: 227 ip = '--ip6 {0}'.format(ip_addr) 228 local("docker network connect {0} {1} {2}".format(ip, self.name, ctn.docker_name())) 229 i = [x for x in list(Client(timeout=60, version='auto').inspect_network(self.id)['Containers'].values()) if x['Name'] == ctn.docker_name()][0] 230 if self.subnet.version == 4: 231 eth = 'eth{0}'.format(len(ctn.ip_addrs)) 232 addr = i['IPv4Address'] 233 ctn.ip_addrs.append((eth, addr, self.name)) 234 else: 235 eth = 'eth{0}'.format(len(ctn.ip6_addrs)) 236 addr = i['IPv6Address'] 237 ctn.ip6_addrs.append((eth, addr, self.name)) 238 239 def delete(self): 240 try_several_times(lambda: local("docker network rm {0}".format(self.name))) 241 242 243 class Container(object): 244 def __init__(self, name, image): 245 self.name = name 246 self.image = image 247 self.shared_volumes = [] 248 self.ip_addrs = [] 249 self.ip6_addrs = [] 250 self.is_running = False 251 self.eths = [] 252 self.tcpdump_running = False 253 254 if self.docker_name() in get_containers(): 255 self.remove() 256 257 def docker_name(self): 258 if TEST_PREFIX == DEFAULT_TEST_PREFIX: 259 return '{0}'.format(self.name) 260 return '{0}_{1}'.format(TEST_PREFIX, self.name) 261 262 def next_if_name(self): 263 name = 'eth{0}'.format(len(self.eths) + 1) 264 self.eths.append(name) 265 return name 266 267 def run(self): 268 c = CmdBuffer(' ') 269 c << "docker run --privileged=true" 270 for sv in self.shared_volumes: 271 c << "-v {0}:{1}".format(sv[0], sv[1]) 272 c << "--name {0} -l {1} -id {2}".format(self.docker_name(), TEST_CONTAINER_LABEL, self.image) 273 self.id = try_several_times(lambda: local(str(c), capture=True)) 274 self.is_running = True 275 self.local("ip li set up dev lo") 276 for line in self.local("ip a show dev eth0", capture=True).split('\n'): 277 if line.strip().startswith("inet "): 278 elems = [e.strip() for e in line.strip().split(' ')] 279 self.ip_addrs.append(('eth0', elems[1], 'docker0')) 280 elif line.strip().startswith("inet6 "): 281 elems = [e.strip() for e in line.strip().split(' ')] 282 self.ip6_addrs.append(('eth0', elems[1], 'docker0')) 283 return 0 284 285 def stop(self): 286 ret = try_several_times(lambda: local("docker stop -t 0 " + self.docker_name(), capture=True)) 287 self.is_running = False 288 return ret 289 290 def remove(self): 291 ret = try_several_times(lambda: local("docker rm -f " + self.docker_name(), capture=True)) 292 self.is_running = False 293 return ret 294 295 def local(self, cmd, capture=False, stream=False, detach=False, tty=True): 296 if stream: 297 dckr = Client(timeout=120, version='auto') 298 i = dckr.exec_create(container=self.docker_name(), cmd=cmd) 299 return dckr.exec_start(i['Id'], tty=tty, stream=stream, detach=detach) 300 else: 301 flag = '-d' if detach else '' 302 return local('docker exec {0} {1} {2}'.format(flag, self.docker_name(), cmd), capture) 303 304 def get_pid(self): 305 if self.is_running: 306 cmd = "docker inspect -f '{{.State.Pid}}' " + self.docker_name() 307 return int(local(cmd, capture=True)) 308 return -1 309 310 def start_tcpdump(self, interface=None, filename=None, expr='tcp port 179'): 311 if self.tcpdump_running: 312 raise Exception('tcpdump already running') 313 self.tcpdump_running = True 314 if not interface: 315 interface = "eth0" 316 if not filename: 317 filename = '{0}.dump'.format(interface) 318 self.local("tcpdump -U -i {0} -w {1}/{2} {3}".format(interface, self.shared_volumes[0][1], filename, expr), detach=True) 319 return '{0}/{1}'.format(self.shared_volumes[0][0], filename) 320 321 def stop_tcpdump(self): 322 self.local("pkill tcpdump") 323 self.tcpdump_running = False 324 325 326 class BGPContainer(Container): 327 328 WAIT_FOR_BOOT = 1 329 RETRY_INTERVAL = 5 330 331 def __init__(self, name, asn, router_id, ctn_image_name): 332 self.config_dir = '/'.join((TEST_BASE_DIR, TEST_PREFIX, name)) 333 local('if [ -e {0} ]; then rm -rf {0}; fi'.format(self.config_dir)) 334 local('mkdir -p {0}'.format(self.config_dir)) 335 local('chmod 777 {0}'.format(self.config_dir)) 336 self.asn = asn 337 self.router_id = router_id 338 self.peers = {} 339 self.routes = {} 340 self.policies = {} 341 super(BGPContainer, self).__init__(name, ctn_image_name) 342 343 def __repr__(self): 344 return str({'name': self.name, 'asn': self.asn, 'router_id': self.router_id}) 345 346 def run(self): 347 self.create_config() 348 super(BGPContainer, self).run() 349 return self.WAIT_FOR_BOOT 350 351 def peer_name(self, peer): 352 if peer not in self.peers: 353 raise Exception('not found peer {0}'.format(peer.router_id)) 354 name = self.peers[peer]['interface'] 355 if name == '': 356 name = self.peers[peer]['neigh_addr'].split('/')[0] 357 return name 358 359 def update_peer(self, peer, **kwargs): 360 if peer not in self.peers: 361 raise Exception('peer not exists') 362 self.add_peer(peer, **kwargs) 363 364 def add_peer( 365 self, 366 peer, 367 passwd=None, 368 vpn=False, 369 is_rs_client=False, 370 policies=None, 371 passive=False, 372 is_rr_client=False, 373 cluster_id=None, 374 flowspec=False, 375 bridge="", 376 reload_config=True, 377 as2=False, 378 graceful_restart=None, 379 local_as=None, 380 prefix_limit=None, 381 v6=False, 382 llgr=None, 383 vrf="", 384 interface="", 385 allow_as_in=0, 386 remove_private_as=None, 387 replace_peer_as=False, 388 addpath=0, 389 treat_as_withdraw=False, 390 remote_as=None, 391 mup=False, 392 ): 393 neigh_addr = '' 394 local_addr = '' 395 it = itertools.product(self.ip_addrs, peer.ip_addrs) 396 if v6: 397 it = itertools.product(self.ip6_addrs, peer.ip6_addrs) 398 399 if interface == '': 400 for me, you in it: 401 if bridge != '' and bridge != me[2]: 402 continue 403 if me[2] == you[2]: 404 neigh_addr = you[1] 405 local_addr = me[1] 406 if v6: 407 addr, mask = local_addr.split('/') 408 local_addr = "{0}%{1}/{2}".format(addr, me[0], mask) 409 break 410 411 if neigh_addr == '': 412 raise Exception('peer {0} seems not ip reachable'.format(peer)) 413 414 if not policies: 415 policies = {} 416 417 self.peers[peer] = {'neigh_addr': neigh_addr, 418 'interface': interface, 419 'passwd': passwd, 420 'vpn': vpn, 421 'flowspec': flowspec, 422 'is_rs_client': is_rs_client, 423 'is_rr_client': is_rr_client, 424 'cluster_id': cluster_id, 425 'policies': policies, 426 'passive': passive, 427 'local_addr': local_addr, 428 'as2': as2, 429 'graceful_restart': graceful_restart, 430 'local_as': local_as, 431 'prefix_limit': prefix_limit, 432 'llgr': llgr, 433 'vrf': vrf, 434 'allow_as_in': allow_as_in, 435 'remove_private_as': remove_private_as, 436 'replace_peer_as': replace_peer_as, 437 'addpath': addpath, 438 'treat_as_withdraw': treat_as_withdraw, 439 'remote_as': remote_as or peer.asn, 440 'mup': mup} 441 if self.is_running and reload_config: 442 self.create_config() 443 self.reload_config() 444 445 def del_peer(self, peer, reload_config=True): 446 del self.peers[peer] 447 if self.is_running and reload_config: 448 self.create_config() 449 self.reload_config() 450 451 def disable_peer(self, peer): 452 raise Exception('implement disable_peer() method') 453 454 def enable_peer(self, peer): 455 raise Exception('implement enable_peer() method') 456 457 def log(self): 458 return local('cat {0}/*.log'.format(self.config_dir), capture=True) 459 460 def _extract_routes(self, families): 461 routes = {} 462 for prefix, paths in list(self.routes.items()): 463 if paths and paths[0]['rf'] in families: 464 routes[prefix] = paths 465 return routes 466 467 def add_route(self, route, rf='ipv4', attribute=None, aspath=None, 468 community=None, med=None, extendedcommunity=None, 469 nexthop=None, matchs=None, thens=None, 470 local_pref=None, identifier=None, reload_config=True): 471 if route not in self.routes: 472 self.routes[route] = [] 473 prefix = route 474 if 'flowspec' in rf: 475 prefix = ' '.join(['match'] + matchs) 476 self.routes[route].append({ 477 'prefix': prefix, 478 'rf': rf, 479 'attr': attribute, 480 'next-hop': nexthop, 481 'as-path': aspath, 482 'community': community, 483 'med': med, 484 'local-pref': local_pref, 485 'extended-community': extendedcommunity, 486 'identifier': identifier, 487 'matchs': matchs, 488 'thens': thens, 489 }) 490 if self.is_running and reload_config: 491 self.create_config() 492 self.reload_config() 493 494 def del_route(self, route, identifier=None, reload_config=True): 495 if route not in self.routes: 496 return 497 self.routes[route] = [p for p in self.routes[route] if p['identifier'] != identifier] 498 if self.is_running and reload_config: 499 self.create_config() 500 self.reload_config() 501 502 def add_policy(self, policy, peer, typ, default='accept', reload_config=True): 503 self.set_default_policy(peer, typ, default) 504 self.define_policy(policy) 505 self.assign_policy(peer, policy, typ) 506 if self.is_running and reload_config: 507 self.create_config() 508 self.reload_config() 509 510 def set_default_policy(self, peer, typ, default): 511 if typ in ['in', 'out', 'import', 'export'] and default in ['reject', 'accept']: 512 if 'default-policy' not in self.peers[peer]: 513 self.peers[peer]['default-policy'] = {} 514 self.peers[peer]['default-policy'][typ] = default 515 else: 516 raise Exception('wrong type or default') 517 518 def define_policy(self, policy): 519 self.policies[policy['name']] = policy 520 521 def assign_policy(self, peer, policy, typ): 522 if peer not in self.peers: 523 raise Exception('peer {0} not found'.format(peer.name)) 524 name = policy['name'] 525 if name not in self.policies: 526 raise Exception('policy {0} not found'.format(name)) 527 self.peers[peer]['policies'][typ] = policy 528 529 def get_local_rib(self, peer, rf): 530 raise Exception('implement get_local_rib() method') 531 532 def get_global_rib(self, rf): 533 raise Exception('implement get_global_rib() method') 534 535 def get_neighbor_state(self, peer_id): 536 raise Exception('implement get_neighbor() method') 537 538 def get_reachability(self, prefix, timeout=20): 539 version = netaddr.IPNetwork(prefix).version 540 addr = prefix.split('/')[0] 541 if version == 4: 542 ping_cmd = 'ping' 543 elif version == 6: 544 ping_cmd = 'ping6' 545 else: 546 raise Exception('unsupported route family: {0}'.format(version)) 547 cmd = '/bin/bash -c "/bin/{0} -c 1 -w 1 {1} | xargs echo"'.format(ping_cmd, addr) 548 interval = 1 549 count = 0 550 while True: 551 res = self.local(cmd, capture=True) 552 print(yellow(res)) 553 if ('1 packets received' in res or '1 received' in res) and '0% packet loss' in res: 554 break 555 time.sleep(interval) 556 count += interval 557 if count >= timeout: 558 raise Exception('timeout') 559 return True 560 561 def wait_for(self, expected_state, peer, timeout=120): 562 interval = 1 563 count = 0 564 while True: 565 state = self.get_neighbor_state(peer) 566 print(yellow("{0}'s peer {1} state: {2}".format(self.router_id, 567 peer.router_id, 568 state))) 569 if state == expected_state: 570 return 571 572 time.sleep(interval) 573 count += interval 574 if count >= timeout: 575 raise Exception('timeout') 576 577 def add_static_route(self, network, next_hop): 578 cmd = '/sbin/ip route add {0} via {1}'.format(network, next_hop) 579 self.local(cmd, capture=True) 580 581 def set_ipv6_forward(self): 582 cmd = 'sysctl -w net.ipv6.conf.all.forwarding=1' 583 self.local(cmd, capture=True) 584 585 def create_config(self): 586 raise Exception('implement create_config() method') 587 588 def reload_config(self): 589 raise Exception('implement reload_config() method') 590 591 592 class OSPFContainer(Container): 593 WAIT_FOR_BOOT = 1 594 595 def __init__(self, name, ctn_image_name): 596 self.config_dir = '/'.join((TEST_BASE_DIR, TEST_PREFIX, name)) 597 local('if [ -e {0} ]; then rm -rf {0}; fi'.format(self.config_dir)) 598 local('mkdir -p {0}'.format(self.config_dir)) 599 local('chmod 777 {0}'.format(self.config_dir)) 600 601 # Example: 602 # networks = { 603 # '192.168.1.0/24': '0.0.0.0', # <network>: <area> 604 # } 605 self.networks = {} 606 super(OSPFContainer, self).__init__(name, ctn_image_name) 607 608 def __repr__(self): 609 return str({'name': self.name, 'networks': self.networks}) 610 611 def run(self): 612 self.create_config() 613 super(OSPFContainer, self).run() 614 return self.WAIT_FOR_BOOT 615 616 def create_config(self): 617 raise NotImplementedError