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