├── .gitignore ├── docs ├── bgperf.jpg ├── bgperf_remote.jpg ├── bgperf_10K_elapsed.png ├── bgperf_10K_max_cpu.png ├── bgperf_10K_max_mem.png ├── bgperf_10K_neighbor.png ├── bgperf_10K_total_time.png ├── bgperf_10K_route_reception.png ├── benchmark_remote_target.md ├── how_bgperf_works.md └── mrt.md ├── pip-requirements.txt ├── bird.tfsm ├── graphs.py ├── nos_templates ├── eos.j2 └── junos.j2 ├── settings.py ├── new_vm.sh ├── benchmark.yaml ├── big-tests.yaml ├── filters ├── frr.conf ├── openbgp.conf ├── junos.conf ├── bird.conf └── rustybgpd.conf ├── exabgp.py ├── rustybgp.py ├── eos.py ├── tester.py ├── monitor.py ├── flock.py ├── bgpdump2.py ├── junos.py ├── srlinux.py ├── openbgp.py ├── frr_compiled.py ├── gobgp.py ├── mrt_tester.py ├── frr.py ├── bird.py ├── LICENSE ├── base.py ├── README.md └── bgperf2.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by venv; see https://docs.python.org/3/library/venv.html 2 | * 3 | -------------------------------------------------------------------------------- /docs/bgperf.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netenglabs/bgperf2/HEAD/docs/bgperf.jpg -------------------------------------------------------------------------------- /docs/bgperf_remote.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netenglabs/bgperf2/HEAD/docs/bgperf_remote.jpg -------------------------------------------------------------------------------- /docs/bgperf_10K_elapsed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netenglabs/bgperf2/HEAD/docs/bgperf_10K_elapsed.png -------------------------------------------------------------------------------- /docs/bgperf_10K_max_cpu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netenglabs/bgperf2/HEAD/docs/bgperf_10K_max_cpu.png -------------------------------------------------------------------------------- /docs/bgperf_10K_max_mem.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netenglabs/bgperf2/HEAD/docs/bgperf_10K_max_mem.png -------------------------------------------------------------------------------- /docs/bgperf_10K_neighbor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netenglabs/bgperf2/HEAD/docs/bgperf_10K_neighbor.png -------------------------------------------------------------------------------- /docs/bgperf_10K_total_time.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netenglabs/bgperf2/HEAD/docs/bgperf_10K_total_time.png -------------------------------------------------------------------------------- /docs/bgperf_10K_route_reception.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netenglabs/bgperf2/HEAD/docs/bgperf_10K_route_reception.png -------------------------------------------------------------------------------- /pip-requirements.txt: -------------------------------------------------------------------------------- 1 | docker 2 | jinja2 3 | pyyaml 4 | pyroute2 5 | nsenter 6 | netaddr 7 | mako 8 | six 9 | packaging 10 | psutil 11 | pyyaml 12 | matplotlib 13 | numpy 14 | toml 15 | textfsm 16 | 17 | -------------------------------------------------------------------------------- /bird.tfsm: -------------------------------------------------------------------------------- 1 | Value neighbor (\d+\.\d+\.\d+\.\d+) 2 | Value received (\d+) 3 | Value accepted (\d+) 4 | 5 | Start 6 | ^\s+BGP state:.*$$ -> Record 7 | ^.*Pipe -> Record 8 | ^\s+Neighbor address:\s+${neighbor} 9 | ^\s+Import updates:\s+${received}\s+\S+\s+\S+\s+\S+\s+${accepted} 10 | -------------------------------------------------------------------------------- /graphs.py: -------------------------------------------------------------------------------- 1 | # creates graphs from batch output 2 | 3 | from bgperf import create_batch_graphs 4 | from argparse import ArgumentParser 5 | 6 | from csv import reader 7 | 8 | if __name__ == '__main__': 9 | 10 | parser = ArgumentParser(description='BGP performance measuring tool') 11 | parser.add_argument('-f', '--filename') 12 | parser.add_argument('-n', '--name', default='tests.csv') 13 | 14 | args = parser.parse_args() 15 | 16 | data = [] 17 | 18 | with open(args.filename) as f: 19 | csv_data = reader(f) 20 | for line in csv_data: 21 | data.append(line) 22 | data.pop(0) # get rid of headers 23 | print(f"{len(data)} tests") 24 | create_batch_graphs(data, args.name) 25 | -------------------------------------------------------------------------------- /nos_templates/eos.j2: -------------------------------------------------------------------------------- 1 | service routing protocols model multi-agent 2 | ip routing 3 | 4 | !interface Loopback0 5 | ! ip address {{ data['router-id'] }}/16 6 | 7 | interface Management0 8 | ip address {{ data['router-id'] }}/16 9 | 10 | router bgp {{ data.asn }} 11 | router-id {{ data['router-id'] }} 12 | bgp advertise-inactive 13 | bgp log-neighbor-changes 14 | 15 | neighbor ebgp-peers peer group 16 | {% for n in data.neighbors %} 17 | neighbor {{ n['router-id'] }} peer group ebgp-peers 18 | neighbor {{ n['router-id'] }} remote-as {{n.as}} 19 | no neighbor {{ n['router-id']}} enforce-first-as 20 | neighbor {{ n['router-id'] }} maximum-routes 0 21 | {% endfor %} 22 | ! 23 | address-family ipv4 24 | {% for n in data.neighbors %} 25 | neighbor {{ n['router-id'] }} activate 26 | {% endfor %} 27 | -------------------------------------------------------------------------------- /settings.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2016 Nippon Telegraph and Telephone Corporation. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 13 | # implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | try: 18 | from docker import Client 19 | except ImportError: 20 | from docker import APIClient as Client 21 | 22 | dckr = Client(version='auto') 23 | -------------------------------------------------------------------------------- /new_vm.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This script is used to set everything up to run tests on a new VM or installation 3 | 4 | wget -q http://archive.routeviews.org/bgpdata/2021.08/RIBS/rib.20210801.0000.bz2 && bzip2 -d rib.20210801.0000.bz2 & 5 | sudo apt update 6 | sudo apt upgrade --yes 7 | sudo apt install docker.io --yes 8 | sudo apt install python3-pip --yes 9 | sudo apt install sysstat --yes 10 | sudo apt install emacs-nox --yes 11 | sudo usermod -aG docker ubuntu 12 | 13 | pip3 install -r pip-requirements.txt 14 | 15 | sudo /sbin/shutdown now -r 16 | # the user group permissions need to be applied, so easiest to log out 17 | 18 | # python3 bgperf.py update exabgp & python3 bgperf.py update gobgp & python3 bgperf.py update bird & python3 bgperf.py update frr & python3 bgperf.py update frr_c & python3 bgperf.py update rustybgp & python3 bgperf.py update openbgp & python3 bgperf.py update bgpdump2 & 19 | # -- just in case ython3 bgperf.py prepare && python3 bgperf.py update frr_c && python3 bgperf.py update bgpdump2 -------------------------------------------------------------------------------- /nos_templates/junos.j2: -------------------------------------------------------------------------------- 1 | system { 2 | root-authentication { 3 | encrypted-password bgperf; ## SECRET-DATA 4 | } 5 | license { 6 | keys { 7 | key "{{ data.license }}"; 8 | } 9 | } 10 | processes { 11 | routing { 12 | bgp { 13 | rib-sharding { 14 | number-of-shards {{ data.cores }}; 15 | } 16 | update-threading { 17 | number-of-threads {{ data.cores }}; 18 | } 19 | } 20 | } 21 | } 22 | } 23 | routing-options { 24 | router-id {{ data['router-id'] }}; 25 | autonomous-system {{ data.asn }}; 26 | } 27 | protocols { 28 | bgp { 29 | group ebgp-peers { 30 | type external; 31 | advertise-inactive; 32 | {%- for n in data.neighbors -%} 33 | neighbor {{ n['router-id'] }} { 34 | peer-as {{ n.as }}; 35 | {% if data.filter is defined %} 36 | import {{data.filter}} ; 37 | {% endif %} 38 | } 39 | {% endfor %} 40 | } 41 | } 42 | } 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /docs/benchmark_remote_target.md: -------------------------------------------------------------------------------- 1 | # Benchmark remote target 2 | 3 | ![architecture of bgperf](./bgperf_remote.jpg) 4 | 5 | To benchmark remote bgp target, make bgperf configuration file manually and 6 | add `remote: true` to the `target` configuration. 7 | 8 | ```shell 9 | $ cat scenario.yaml 10 | local_prefix: 192.168.10.0/24 11 | monitor: 12 | as: 1001 13 | check-points: [20] 14 | local-address: 192.168.10.2 15 | router-id: 10.10.0.2 16 | target: {as: 1000, local-address: 192.168.10.1, router-id: 10.10.0.1, remote: true} 17 | testers: 18 | - neighbors: 19 | 10.10.0.3: 20 | as: 1003 21 | local-address: 192.168.10.3 22 | paths: [100.0.0.0/32, 100.0.0.1/32, 100.0.0.2/32, 100.0.0.3/32, 100.0.0.4/32, 23 | 100.0.0.5/32, 100.0.0.6/32, 100.0.0.7/32, 100.0.0.8/32, 100.0.0.9/32] 24 | router-id: 10.10.0.3 25 | 10.10.0.4: 26 | as: 1004 27 | local-address: 192.168.10.4 28 | paths: [100.0.0.10/32, 100.0.0.11/32, 100.0.0.12/32, 100.0.0.13/32, 100.0.0.14/32, 29 | 100.0.0.15/32, 100.0.0.16/32, 100.0.0.17/32, 100.0.0.18/32, 100.0.0.19/32] 30 | router-id: 10.10.0.4 31 | ``` 32 | 33 | Use `-f` option to pass the configuration. 34 | 35 | ```shell 36 | $ sudo ./bgperf.py bench -f scenario.yaml 37 | ``` 38 | 39 | For remote benchmarking, bgperf.py can't collect cpu/memory stats. 40 | -------------------------------------------------------------------------------- /benchmark.yaml: -------------------------------------------------------------------------------- 1 | # a set of standard tests 2 | # to compare outputs a I make changes to bgperf 3 | tests: 4 | - 5 | name: baseline-benchmark 6 | neighbors: [10] 7 | prefixes: [800_000] 8 | filter_test: [None, ixp, transit] 9 | targets: 10 | - 11 | name: junos 12 | label: junos rs-16 13 | tester_type: bgpdump2 14 | mrt_file: /home/jpietsch/bgperf/rib.20210801.0000 15 | license_file: /home/jpietsch/bgperf/NOS/2021Nov_JNX_LICFEAT_CRPD_STANDARD-TRIAL.txt 16 | - 17 | name: eos 18 | label: eos arBGP 19 | tester_type: bgpdump2 20 | mrt_file: /home/jpietsch/bgperf/rib.20210801.0000 21 | 22 | - 23 | name: frr_c 24 | label: frr 8 25 | tester_type: bgpdump2 26 | mrt_file: /home/jpietsch/bgperf/rib.20210801.0000 27 | - 28 | name: bird 29 | tester_type: bgpdump2 30 | mrt_file: /home/jpietsch/bgperf/rib.20210801.0000 31 | - 32 | name: rustybgp 33 | tester_type: bgpdump2 34 | mrt_file: /home/jpietsch/bgperf/rib.20210801.0000 35 | 36 | # TODO: openbgp doesn't work, complains about rpki 37 | # I have no idea how I broke it 38 | # - 39 | # name: openbgp 40 | # tester_type: bgpdump2 41 | # mrt_file: /home/jpietsch/bgperf/rib.20210801.0000 42 | 43 | 44 | -------------------------------------------------------------------------------- /big-tests.yaml: -------------------------------------------------------------------------------- 1 | tests: 2 | - 3 | name: 1000p 4 | # with 384 GB RAM, can't do more than 1000n at 1000p 5 | neighbors: [1, 500, 750, 1000] 6 | prefixes: [1000] 7 | targets: 8 | - 9 | name: frr 10 | - 11 | name: bird 12 | label: bird -s 13 | single_table: True 14 | - 15 | name: frr 16 | - 17 | name: frr_c 18 | label: frr 8 19 | - 20 | name: rustybgp 21 | - # for more than 1024 neighbors must increase gcthresh3 22 | # echo 16384 | sudo tee /proc/sys/net/ipv4/neigh/default/gc_thresh3 23 | name: many_neighbors_100p 24 | neighbors: [1500, 1750, 2000, 2250, 2500, 3000] 25 | prefixes: [100] 26 | targets: 27 | - 28 | name: bird 29 | label: bird -s 30 | single_table: True 31 | - 32 | name: frr 33 | - 34 | name: frr_c 35 | label: frr 8 36 | - 37 | name: rustybgp 38 | - 39 | - # for more than 1024 neighbors must increase gcthresh3 40 | # echo 16384 | sudo tee /proc/sys/net/ipv4/neigh/default/gc_thresh3 41 | name: many_many_neighbors_10p 42 | neighbors: [3000, 4000, 5000] 43 | prefixes: [10] 44 | targets: 45 | - 46 | name: bird 47 | label: bird -s 48 | single_table: True 49 | - 50 | name: frr 51 | - 52 | name: frr_c 53 | label: frr 8 54 | - 55 | name: rustybgp -------------------------------------------------------------------------------- /filters/frr.conf: -------------------------------------------------------------------------------- 1 | # taken from https://bgpfilterguide.nlnog.net 2 | 3 | # bogon ASNs 4 | bgp as-path access-list bogon-asns deny 0 5 | bgp as-path access-list bogon-asns deny 23456 6 | bgp as-path access-list bogon-asns deny 64496-131071 7 | bgp as-path access-list bogon-asns deny 4200000000-4294967295 8 | 9 | # bogon prefixes 10 | #ip prefix-list BOGONS_v4 permit 0.0.0.0/8 le 32 11 | ip prefix-list BOGONS_v4 permit 10.0.0.0/8 le 32 12 | ip prefix-list BOGONS_v4 permit 100.64.0.0/10 le 32 13 | ip prefix-list BOGONS_v4 permit 127.0.0.0/8 le 32 14 | ip prefix-list BOGONS_v4 permit 169.254.0.0/16 le 32 15 | ip prefix-list BOGONS_v4 permit 172.16.0.0/12 le 32 16 | ip prefix-list BOGONS_v4 permit 192.0.2.0/24 le 32 17 | ip prefix-list BOGONS_v4 permit 192.88.99.0/24 le 32 18 | ip prefix-list BOGONS_v4 permit 192.168.0.0/16 le 32 19 | ip prefix-list BOGONS_v4 permit 198.18.0.0/15 le 32 20 | ip prefix-list BOGONS_v4 permit 198.51.100.0/24 le 32 21 | ip prefix-list BOGONS_v4 permit 203.0.113.0/24 le 32 22 | ip prefix-list BOGONS_v4 permit 224.0.0.0/4 le 32 23 | ip prefix-list BOGONS_v4 permit 240.0.0.0/4 le 32 24 | 25 | # small prefixes 26 | ip prefix-list SMALL_v4 permit 0.0.0.0/0 ge 25 le 32 27 | ipv6 prefix-list SMALL_v6 permit ::/0 ge 49 le 128 28 | 29 | # long as paths 30 | 31 | ## TODO: figure out what this should look like for FRR 32 | 33 | # known transit AS 34 | bgp as-path access-list peerings permit .*(174|701|1299|2914|3257|3320|3356|3491|4134|5511|6453|6461|6762|6830|7018).* 35 | #bgp as-path access-list peerings permit _(174|701|1299|2914|3257|3320|3356|3491|4134|5511|6453|6461|6762|6830|7018)_ 36 | 37 | route-map ixp deny 10 38 | match ip address prefix-list BOGONS_v4 39 | route-map ixp deny 20 40 | match as-path bogon-asns 41 | route-map ixp deny 30 42 | match as-path peerings 43 | route-map ixp deny 40 44 | match ip address prefix-list SMALL_v4 45 | route-map ixp permit 100 46 | 47 | 48 | route-map transit deny 10 49 | match ip address prefix-list BOGONS_v4 50 | route-map transit deny 20 51 | match as-path bogon-asns 52 | # route-map transit deny 40 53 | # match ip address prefix-list SMALL_v4 54 | route-map transit permit 100 55 | 56 | 57 | -------------------------------------------------------------------------------- /filters/openbgp.conf: -------------------------------------------------------------------------------- 1 | # take from https://bgpfilterguide.nlnog.net 2 | 3 | # bogon ASNs 4 | deny quick from any AS 23456 # AS_TRANS 5 | deny quick from any AS 64496 - 64511 # Reserved for use in docs and code RFC5398 6 | deny quick from any AS 64512 - 65534 # Reserved for Private Use RFC6996 7 | deny quick from any AS 65535 # Reserved RFC7300 8 | deny quick from any AS 65536 - 65551 # Reserved for use in docs and code RFC5398 9 | deny quick from any AS 65552 - 131071 # Reserved 10 | deny quick from any AS 4200000000 - 4294967294 # Reserved for Private Use RFC6996 11 | deny quick from any AS 4294967295 # Reserved RFC7300 12 | 13 | # bogon prefixes 14 | #deny quick from any prefix 0.0.0.0/8 prefixlen >= 8 # 'this' network [RFC1122] 15 | deny quick from any prefix 10.0.0.0/8 prefixlen >= 8 # private space [RFC1918] 16 | deny quick from any prefix 100.64.0.0/10 prefixlen >= 10 # CGN Shared [RFC6598] 17 | deny quick from any prefix 127.0.0.0/8 prefixlen >= 8 # localhost [RFC1122] 18 | deny quick from any prefix 169.254.0.0/16 prefixlen >= 16 # link local [RFC3927] 19 | deny quick from any prefix 172.16.0.0/12 prefixlen >= 12 # private space [RFC1918] 20 | deny quick from any prefix 192.0.2.0/24 prefixlen >= 24 # TEST-NET-1 [RFC5737] 21 | deny quick from any prefix 192.88.99.0/24 prefixlen >= 24 # 6to4 anycast relay [RFC7526] 22 | deny quick from any prefix 192.168.0.0/16 prefixlen >= 16 # private space [RFC1918] 23 | deny quick from any prefix 198.18.0.0/15 prefixlen >= 15 # benchmarking [RFC2544] 24 | deny quick from any prefix 198.51.100.0/24 prefixlen >= 24 # TEST-NET-2 [RFC5737] 25 | deny quick from any prefix 203.0.113.0/24 prefixlen >= 24 # TEST-NET-3 [RFC5737] 26 | deny quick from any prefix 224.0.0.0/4 prefixlen >= 4 # multicast 27 | deny quick from any prefix 240.0.0.0/4 prefixlen >= 4 # reserved for future use 28 | 29 | 30 | # small prefixes 31 | #deny quick from any inet prefixlen > 24 32 | deny quick from any inet6 prefixlen > 48 33 | 34 | # long as paths 35 | # deny quick from any max-as-len 100 # don't know frr equiv so not using yet 36 | 37 | # known transit AS 38 | # hardcoding this into the openbgp.py code becaues I don't know how to have different filters 39 | # deny quick from any transit-as {174,701,1299,2914,3257,3320,3356,3491,4134,5511,6453,6461,6762,6830,7018} 40 | 41 | allow from any 42 | -------------------------------------------------------------------------------- /exabgp.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2016 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 base import * 17 | 18 | class ExaBGP(Container): 19 | 20 | GUEST_DIR = '/root/config' 21 | 22 | def __init__(self, name, host_dir, conf, image='bgperf/exabgp'): 23 | super(ExaBGP, self).__init__('bgperf_exabgp_' + name, image, host_dir, self.GUEST_DIR, conf) 24 | 25 | 26 | # This Dockerfile has parts borrowed from exabgps Dockerfile 27 | @classmethod 28 | def build_image(cls, force=False, tag='bgperf/exabgp', checkout='HEAD', nocache=False): 29 | cls.dockerfile = ''' 30 | FROM python:3-buster 31 | 32 | 33 | ENV PYTHONPATH "/tmp/exabgp/src" 34 | 35 | RUN apt update \ 36 | && apt -y dist-upgrade \ 37 | && rm -rf /var/lib/apt/lists/* /var/cache/apt/* 38 | 39 | ADD . /tmp/exabgp 40 | WORKDIR /tmp/exabgp 41 | RUN ln -s src/exabgp exabgp 42 | 43 | RUN echo Building exabgp 44 | RUN pip3 install --upgrade pip setuptools wheel 45 | RUN pip3 install exabgp 46 | WORKDIR /root 47 | 48 | RUN ln -s /root/exabgp /exabgp 49 | #ENTRYPOINT ["/bin/bash"] 50 | '''.format(checkout) 51 | super(ExaBGP, cls).build_image(force, tag, nocache) 52 | 53 | 54 | class ExaBGP_MRTParse(Container): 55 | 56 | GUEST_DIR = '/root/config' 57 | 58 | def __init__(self, name, host_dir, conf, image='bgperf/exabgp_mrtparse'): 59 | super(ExaBGP_MRTParse, self).__init__('bgperf_exabgp_mrtparse_' + name, image, host_dir, self.GUEST_DIR, conf) 60 | 61 | @classmethod 62 | def build_image(cls, force=False, tag='bgperf/exabgp_mrtparse', checkout='HEAD', nocache=False): 63 | cls.dockerfile = ''' 64 | FROM python:3-slim-buster 65 | 66 | ENV PYTHONPATH "/tmp/exabgp/src" 67 | 68 | RUN apt update \ 69 | && apt -y dist-upgrade \ 70 | && rm -rf /var/lib/apt/lists/* /var/cache/apt/* 71 | 72 | ADD . /tmp/exabgp 73 | WORKDIR /tmp/exabgp 74 | RUN ln -s src/exabgp exabgp 75 | 76 | RUN echo Building exabgp 77 | RUN pip3 install --upgrade pip setuptools wheel 78 | RUN pip3 install exabgp 79 | WORKDIR /root 80 | 81 | RUN ln -s /root/exabgp /exabgp 82 | ENTRYPOINT ["/bin/bash"] 83 | '''.format(checkout) 84 | super(ExaBGP_MRTParse, cls).build_image(force, tag, nocache) 85 | -------------------------------------------------------------------------------- /rustybgp.py: -------------------------------------------------------------------------------- 1 | 2 | import toml 3 | from base import * 4 | from gobgp import GoBGPTarget 5 | 6 | 7 | class RustyBGP(Container): 8 | CONTAINER_NAME = None 9 | GUEST_DIR = '/root/config' 10 | 11 | def __init__(self, host_dir, conf, image='bgperf/rustybgp'): 12 | super(RustyBGP, self).__init__(self.CONTAINER_NAME, image, host_dir, self.GUEST_DIR, conf) 13 | 14 | @classmethod 15 | def build_image(cls, force=False, tag='bgperf/rustybgp', checkout='', nocache=False): 16 | 17 | cls.dockerfile = ''' 18 | 19 | FROM rust:1-bullseye AS rust_builder 20 | RUN rustup component add rustfmt 21 | RUN git clone https://github.com/osrg/rustybgp.git 22 | # I don't know why, but a newer version of futures is required 23 | RUN cd rustybgp && sed -i "s/0.3.16/0.3.31/g" daemon/Cargo.toml && cargo build --release && cp target/release/rustybgpd /root 24 | RUN wget https://github.com/osrg/gobgp/releases/download/v3.0.0/gobgp_3.0.0_linux_amd64.tar.gz 25 | RUN tar xzf gobgp_*.tar.gz 26 | RUN cp gobgp /root 27 | 28 | 29 | FROM debian:bullseye 30 | WORKDIR /root 31 | COPY --from=rust_builder /root/rustybgpd ./ 32 | COPY --from=rust_builder /root/gobgp ./ 33 | 34 | '''.format(checkout) 35 | super(RustyBGP, cls).build_image(force, tag, nocache) 36 | 37 | 38 | class RustyBGPTarget(RustyBGP, GoBGPTarget): 39 | # RustyBGP has the same config as GoBGP 40 | # except some things are different 41 | 42 | CONTAINER_NAME = 'bgperf_rustybgp_target' 43 | 44 | def __init__(self, host_dir, conf, image='bgperf/rustybgp'): 45 | super(GoBGPTarget, self).__init__(host_dir, conf, image=image) 46 | 47 | def write_config(self): 48 | # I don't want to figure out how to write config as TOML Instead of YAML, 49 | # but RustyBGP can only handle TOML, so I'm cheating 50 | config = super(RustyBGPTarget, self).write_config() 51 | del config['policy-definitions'] 52 | del config['defined-sets'] 53 | 54 | toml_config = toml.dumps(config) 55 | with open('{0}/{1}'.format(self.host_dir, self.CONFIG_FILE_NAME), 'w') as f: 56 | f.write(toml_config) 57 | if 'filter_test' in self.conf: 58 | f.write(self.get_filter_test_config()) 59 | 60 | def get_filter_test_config(self): 61 | file = open("filters/rustybgpd.conf", mode='r') 62 | filters = file.read() 63 | filters += "\n[global.apply-policy.config]\n" 64 | filters += f"import-policy-list = [\"{self.conf['filter_test']}\"]" 65 | file.close 66 | return filters 67 | 68 | def get_startup_cmd(self): 69 | return '\n'.join( 70 | ['#!/bin/bash', 71 | 'ulimit -n 65536', 72 | 'RUST_BACKTRACE=1 /root/rustybgpd -f {guest_dir}/{config_file_name} > {guest_dir}/rustybgp.log 2>&1'] 73 | ).format( 74 | guest_dir=self.guest_dir, 75 | config_file_name=self.CONFIG_FILE_NAME, 76 | debug_level='info') 77 | 78 | def get_version_cmd(self): 79 | return "/root/rustybgpd --version" 80 | 81 | def exec_version_cmd(self): 82 | version = self.get_version_cmd() 83 | i= dckr.exec_create(container=self.name, cmd=version, stderr=False) 84 | return dckr.exec_start(i['Id'], stream=False, detach=False).decode('utf-8').strip() 85 | -------------------------------------------------------------------------------- /docs/how_bgperf_works.md: -------------------------------------------------------------------------------- 1 | # How bgperf works 2 | 3 | ![architecture of bgperf](./bgperf.jpg) 4 | 5 | When `bench` command issued, `bgperf` boots three (or more) docker containers, 6 | `target`, `monitor` and one or more `tester` and connect them via a bridge (`bgperf-br` by default). 7 | 8 | By default, `bgperf` stores all configuration files and log files under `/tmp/bgperf`. 9 | Here is what you can see after issuing `bgperf.py bench -n 10`. 10 | 11 | ```shell 12 | $ tree /tmp/bgperf2 13 | /tmp/bgperf2 14 | ├── gobgp 15 | │   ├── gobgpd.conf 16 | │   ├── gobgpd.log 17 | │   └── start.sh 18 | ├── monitor 19 | │   ├── gobgpd.conf 20 | │   ├── gobgpd.log 21 | │   └── start.sh 22 | ├── scenario.yaml 23 | └── tester 24 | ├── 10.10.0.10.conf 25 | ├── 10.10.0.10.log 26 | ├── 10.10.0.11.conf 27 | ├── 10.10.0.11.log 28 | ├── 10.10.0.12.conf 29 | ├── 10.10.0.12.log 30 | ├── 10.10.0.3.conf 31 | ├── 10.10.0.3.log 32 | ├── 10.10.0.4.conf 33 | ├── 10.10.0.4.log 34 | ├── 10.10.0.5.conf 35 | ├── 10.10.0.5.log 36 | ├── 10.10.0.6.conf 37 | ├── 10.10.0.6.log 38 | ├── 10.10.0.7.conf 39 | ├── 10.10.0.7.log 40 | ├── 10.10.0.8.conf 41 | ├── 10.10.0.8.log 42 | ├── 10.10.0.9.conf 43 | ├── 10.10.0.9.log 44 | └── start.sh 45 | 46 | 3 directories, 28 files 47 | ``` 48 | 49 | `scenario.yaml` controls all the configuration of benchmark. You can pass your own scenario by using `-f` option. 50 | By default, `bgperf` creates it automatically and places it under `/tmp/bgperf2` like above. Let's see what's inside `scenario.yaml`. 51 | 52 | ```shell 53 | $ cat /tmp/bgperf2/scenario.yaml 54 | <% 55 | import netaddr 56 | from itertools import islice 57 | 58 | it = netaddr.iter_iprange('100.0.0.0','160.0.0.0') 59 | 60 | def gen_paths(num): 61 | return list('{0}/32'.format(ip) for ip in islice(it, num)) 62 | %> 63 | local_prefix: 10.10.0.0/24 64 | monitor: 65 | as: 1001 66 | check-points: [1000] 67 | local-address: 10.10.0.2 68 | router-id: 10.10.0.2 69 | target: {as: 1000, local-address: 10.10.0.1, router-id: 10.10.0.1} 70 | testers: 71 | - name: tester 72 | neighbors: 73 | 10.10.0.10: 74 | as: 1010 75 | filter: 76 | in: &id001 [] 77 | local-address: 10.10.0.10 78 | paths: ${gen_paths(100)} 79 | router-id: 10.10.0.10 80 | 10.10.0.100: 81 | as: 1100 82 | filter: 83 | in: *id001 84 | local-address: 10.10.0.100 85 | paths: ${gen_paths(100)} 86 | router-id: 10.10.0.100 87 | 10.10.0.101: 88 | as: 1101 89 | filter: 90 | in: *id001 91 | local-address: 10.10.0.101 92 | paths: ${gen_paths(100)} 93 | router-id: 10.10.0.101 94 | ...(snip)... 95 | ``` 96 | 97 | It describes local address, AS number and router-id of each cast. 98 | With regard to tester, it also describes the routes to advertise to the target. 99 | 100 | `check-points` field of `monitor` control when to end the benchmark. 101 | During the benchmark, `bgperf.py` continuously checks how many routes `monitor` have got. 102 | Benchmark ends when the number of received routes gets equal to check-point value. 103 | 104 | As you may notice, `scenario.yaml` is [mako](http://www.makotemplates.org/) templated. You can use mako templating to simplify 105 | your scenario. 106 | -------------------------------------------------------------------------------- /eos.py: -------------------------------------------------------------------------------- 1 | from jinja2.loaders import FileSystemLoader 2 | from base import * 3 | import json 4 | 5 | class Eos(Container): 6 | CONTAINER_NAME = None 7 | GUEST_DIR = '/mnt/flash' 8 | 9 | def __init__(self, host_dir, conf, image='ceos'): 10 | super(Eos, self).__init__(self.CONTAINER_NAME, image, host_dir, self.GUEST_DIR, conf) 11 | 12 | self.environment = { 13 | "CEOS": "1", 14 | "EOS_PLATFORM": "ceoslab", 15 | "container": "docker", 16 | "ETBA": "4", 17 | "SKIP_ZEROTOUCH_BARRIER_IN_SYSDBINIT": "1", 18 | "INTFTYPE": "eth", 19 | "MAPETH0": "1", 20 | "MGMT_INTF": "eth0", 21 | } 22 | # eos needs to have a specific command run on container creation 23 | self.command = "/sbin/init" 24 | for k,v in self.environment.items(): 25 | self.command += f" systemd.setenv={k}={v}" 26 | 27 | 28 | # don't build just download 29 | # assume that you do this by hand 30 | @classmethod 31 | def build_image(cls, force=False, tag='ceos', checkout='', nocache=False): 32 | cls.dockerfile = '' 33 | print("Can't build Eos, must download yourself") 34 | 35 | 36 | 37 | class EosTarget(Eos, Target): 38 | 39 | CONTAINER_NAME = 'bgperf_eos_target' 40 | CONFIG_FILE_NAME = 'startup-config' 41 | 42 | def __init__(self, host_dir, conf, image='ceos'): 43 | super(EosTarget, self).__init__(host_dir, conf, image=image) 44 | 45 | 46 | def write_config(self): 47 | bgp = {} 48 | bgp['neighbors'] = [] 49 | bgp['asn'] = self.conf['as'] 50 | bgp['router-id'] = self.conf['router-id'] 51 | 52 | for n in sorted(list(flatten(list(t.get('neighbors', {}).values()) for t in self.scenario_global_conf['testers'])) + 53 | [self.scenario_global_conf['monitor']], key=lambda n: n['as']): 54 | bgp['neighbors'].append(n) 55 | config = self.get_template(bgp, template_file="eos.j2") 56 | 57 | with open('{0}/{1}'.format(self.host_dir, self.CONFIG_FILE_NAME), 'w') as f: 58 | f.write(config) 59 | f.flush() 60 | 61 | 62 | 63 | def exec_startup_cmd(self, stream=False, detach=False): 64 | return None 65 | 66 | 67 | def get_version_cmd(self): 68 | return "Cli -c 'show version|json'" 69 | 70 | def exec_version_cmd(self): 71 | version = self.get_version_cmd() 72 | i= dckr.exec_create(container=self.name, cmd=version, stderr=True) 73 | results = json.loads(dckr.exec_start(i['Id'], stream=False, detach=False).decode('utf-8')) 74 | 75 | return results['version'].strip('(engineering build)') 76 | 77 | 78 | 79 | def get_neighbors_state(self): 80 | neighbors_accepted = {} 81 | neighbors_received = {} 82 | neighbor_received_output = self.local("Cli -c 'sh ip bgp summary |json'") 83 | if neighbor_received_output: 84 | neighbor_received_output = json.loads(neighbor_received_output.decode('utf-8'))["vrfs"]["default"]["peers"] 85 | 86 | 87 | for n in neighbor_received_output.keys(): 88 | rcd = neighbor_received_output[n]['prefixAccepted'] 89 | neighbors_accepted[n] = rcd 90 | return neighbors_received, neighbors_accepted 91 | 92 | 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /docs/mrt.md: -------------------------------------------------------------------------------- 1 | # MRT injection 2 | 3 | This feature requires the basic knowledge about how `bgperf2` works. 4 | Please refer to [this guide](https://github.com/osrg/bgperf/blob/master/docs/how_bgperf_works.md). 5 | 6 | `bgperf` supports injecting routes to the target implementation via MRT file. 7 | With this feature, you can inject more realistic routes rather than artifitial routes which 8 | `bgperf` automatically generates. 9 | 10 | To use the feature, you need to create your own `scenario.yaml`. 11 | 12 | Below is an example configuration to enable MRT injection feature. 13 | 14 | ```shell 15 | $ cat /tmp/bgperf2/scenario.yaml 16 | <% 17 | import netaddr 18 | from itertools import islice 19 | 20 | it = netaddr.iter_iprange('100.0.0.0','160.0.0.0') 21 | 22 | def gen_paths(num): 23 | return list('{0}/32'.format(ip) for ip in islice(it, num)) 24 | %> 25 | local_prefix: 10.10.0.0/24 26 | monitor: 27 | as: 1001 28 | check-points: [1000] 29 | local-address: 10.10.0.2 30 | router-id: 10.10.0.2 31 | target: {as: 1000, local-address: 10.10.0.1, router-id: 10.10.0.1} 32 | testers: 33 | - name: mrt-injector 34 | type: mrt 35 | neighbors: 36 | 10.10.0.200: 37 | as: 1200 38 | local-address: 10.10.0.200 39 | router-id: 10.10.0.200 40 | mrt-file: /path/to/mrt/file 41 | only-best: true # only inject best path to the tester router (recommended to set this true) 42 | count: 1000 # number of routes to inject 43 | skip: 100 # number of routers to skip in the mrt file 44 | - name: tester 45 | neighbors: 46 | 10.10.0.10: 47 | as: 1010 48 | local-address: 10.10.0.10 49 | paths: ${gen_paths(100)} 50 | router-id: 10.10.0.10 51 | 10.10.0.100: 52 | as: 1100 53 | local-address: 10.10.0.100 54 | paths: ${gen_paths(100)} 55 | router-id: 10.10.0.100 56 | ``` 57 | 58 | By adding `type: mrt`, tester will be run in mrt mode. 59 | The MRT injector can be GoBGP (default) or ExaBGP, depending on the value set on `mrt_injector` (gobgp, exabgp). 60 | The `mrt-file` can be set both at tester level and at neighbor level: the file provided within the neighbor configuration has priority over the one set at tester level: 61 | 62 | As you can see, you can mix normal tester and mrt tester to create more complicated scenario. 63 | 64 | ``` 65 | ... 66 | - name: mrt-injector-gobgp 67 | type: mrt 68 | neighbors: 69 | 10.10.0.200: 70 | as: 1200 71 | local-address: 10.10.0.200 72 | router-id: 10.10.0.200 73 | mrt-file: /path/to/mrt/file1 74 | - name: mrt-injector-exabgp 75 | type: mrt 76 | mrt_injector: exabgp 77 | mrt-file: /path/to/mrt/file2 78 | neighbors: 79 | 10.10.0.201: 80 | as: 1201 81 | local-address: 10.10.0.201 82 | router-id: 10.10.0.201 83 | 10.10.0.202: 84 | as: 1202 85 | local-address: 10.10.0.202 86 | router-id: 10.10.0.202 87 | mrt-file: /path/to/mrt/file3 88 | ``` 89 | 90 | Here, two testers are configured: 91 | - the first one uses GoBGP and injects routes from file1 92 | - the second one sets up 2 neighbors: 10.10.0.201 injects routes from file2 (configured at tester level), while 10.10.0.202 injects routes from file3. 93 | 94 | GoBGP injectors can be further configured with the following options: 95 | - `only-best`: True/False, to inject only best paths 96 | - `count` and `skip`: with this configuration, the mrt tester will inject *count* routes taken from the MRT file with *skip* offset to the target router. 97 | 98 | ExaBGP testers accept the following options: 99 | - `high-perf`: True/False, to enable [ExaBGP High Performance mode](https://github.com/Exa-Networks/exabgp/wiki/High-Performance). 100 | -------------------------------------------------------------------------------- /filters/junos.conf: -------------------------------------------------------------------------------- 1 | # take from https://bgpfilterguide.nlnog.net 2 | 3 | # bogon ASNs 4 | 5 | policy-options { 6 | as-path-group bogon-asns { 7 | /* RFC7607 */ 8 | as-path zero ".* 0 .*"; 9 | /* RFC 4893 AS_TRANS */ 10 | as-path as_trans ".* 23456 .*"; 11 | /* RFC 5398 and documentation/example ASNs */ 12 | as-path examples1 ".* [64496-64511] .*"; 13 | as-path examples2 ".* [65536-65551] .*"; 14 | /* RFC 6996 Private ASNs*/ 15 | as-path reserved1 ".* [64512-65534] .*"; 16 | as-path reserved2 ".* [4200000000-4294967294] .*"; 17 | /* RFC 6996 Last 16 and 32 bit ASNs */ 18 | as-path last16 ".* 65535 .*"; 19 | as-path last32 ".* 4294967295 .*"; 20 | /* RFC IANA reserved ASNs*/ 21 | as-path iana-reserved ".* [65552-131071] .*"; 22 | } 23 | policy-statement reject-bogon-ans { 24 | term bogon-asns { 25 | from as-path-group bogon-asns; 26 | then accept; 27 | } 28 | } 29 | 30 | # bogon prefixes 31 | policy-statement reject-bogon-prefixes { 32 | term reject-bogon-prefixes-v4 { 33 | from { 34 | route-filter 0.0.0.0/8 orlonger; 35 | route-filter 10.0.0.0/8 orlonger; 36 | route-filter 100.64.0.0/10 orlonger; 37 | route-filter 127.0.0.0/8 orlonger; 38 | route-filter 169.254.0.0/16 orlonger; 39 | route-filter 172.16.0.0/12 orlonger; 40 | route-filter 192.0.2.0/24 orlonger; 41 | route-filter 192.88.99.0/24 orlonger; 42 | route-filter 192.168.0.0/16 orlonger; 43 | route-filter 198.18.0.0/15 orlonger; 44 | route-filter 198.51.100.0/24 orlonger; 45 | route-filter 203.0.113.0/24 orlonger; 46 | route-filter 224.0.0.0/4 orlonger; 47 | route-filter 240.0.0.0/4 orlonger; 48 | } 49 | then accept; 50 | } 51 | } 52 | 53 | 54 | # small prefixes 55 | policy-statement reject_small_prefixes { 56 | term reject_small_prefixes_v4 { 57 | from { 58 | route-filter 0.0.0.0/0 prefix-length-range /25-/32; 59 | } 60 | then accept; 61 | } 62 | } 63 | 64 | 65 | # long as paths 66 | 67 | # policy-statement bgp-import-policy { 68 | # term no-long-paths { 69 | # from as-path too-many-hops; 70 | # then accept; 71 | # } 72 | # } 73 | 74 | 75 | # as-path too-many-hops ".{100,}"; 76 | 77 | # known transit AS 78 | 79 | policy-statement reject-transit-asns { 80 | term no-transit-leaks { 81 | from as-path no-transit-import-in; 82 | then accept; 83 | } 84 | } 85 | 86 | 87 | as-path no-transit-import-in ".* (174|701|1299|2914|3257|3320|3356|3491|4134|5511|6453|6461|6762|6830|7018) .*"; 88 | 89 | policy-statement transit { 90 | term bogon-asns { 91 | from policy reject-bogon-ans; 92 | then { 93 | reject; 94 | default-action accept; 95 | } 96 | } 97 | term bogon-ips { 98 | from policy reject-bogon-prefixes; 99 | then reject; 100 | } 101 | term small-prefixes { 102 | from policy reject_small_prefixes; 103 | then reject; 104 | } 105 | } 106 | 107 | policy-statement ixp { 108 | term bogon-asns { 109 | from policy reject-bogon-ans; 110 | then reject; 111 | } 112 | term bogon-ips { 113 | from policy reject-bogon-prefixes; 114 | then reject; 115 | } 116 | term small-prefixes { 117 | from policy reject_small_prefixes; 118 | then reject; 119 | } 120 | term no-transit { 121 | from policy reject-transit-asns; 122 | then reject; 123 | } 124 | } 125 | 126 | } -------------------------------------------------------------------------------- /filters/bird.conf: -------------------------------------------------------------------------------- 1 | # take from https://bgpfilterguide.nlnog.net 2 | 3 | # bogon ASNs 4 | 5 | define BOGON_ASNS = [ 6 | 0, # RFC 7607 7 | 23456, # RFC 4893 AS_TRANS 8 | 64496..64511, # RFC 5398 and documentation/example ASNs 9 | 64512..65534, # RFC 6996 Private ASNs 10 | 65535, # RFC 7300 Last 16 bit ASN 11 | 65536..65551, # RFC 5398 and documentation/example ASNs 12 | 65552..131071, # RFC IANA reserved ASNs 13 | 4200000000..4294967294, # RFC 6996 Private ASNs 14 | 4294967295 ]; # RFC 7300 Last 32 bit ASN 15 | 16 | function reject_bogon_asns() 17 | int set bogon_asns; 18 | { 19 | bogon_asns = BOGON_ASNS; 20 | 21 | if ( bgp_path ~ bogon_asns ) then { 22 | reject; 23 | } 24 | } 25 | 26 | # bogon prefixes 27 | define BOGON_PREFIXES = [ 28 | #0.0.0.0/8+, # RFC 1122 'this' network 29 | 10.0.0.0/8+, # RFC 1918 private space 30 | 100.64.0.0/10+, # RFC 6598 Carrier grade nat space 31 | 127.0.0.0/8+, # RFC 1122 localhost 32 | 169.254.0.0/16+, # RFC 3927 link local 33 | 172.16.0.0/12+, # RFC 1918 private space 34 | 192.0.2.0/24+, # RFC 5737 TEST-NET-1 35 | 192.88.99.0/24+, # RFC 7526 6to4 anycast relay 36 | 192.168.0.0/16+, # RFC 1918 private space 37 | 198.18.0.0/15+, # RFC 2544 benchmarking 38 | 198.51.100.0/24+, # RFC 5737 TEST-NET-2 39 | 203.0.113.0/24+, # RFC 5737 TEST-NET-3 40 | 224.0.0.0/4+, # multicast 41 | 240.0.0.0/4+ ]; # reserved 42 | 43 | function reject_bogon_prefixes() 44 | prefix set bogon_prefixes; 45 | { 46 | bogon_prefixes = BOGON_PREFIXES; 47 | 48 | if (net ~ bogon_prefixes) then { 49 | reject; 50 | } 51 | } 52 | 53 | 54 | # small prefixes 55 | function reject_small_prefixes() 56 | { 57 | if (net.len > 24) then { 58 | print "Reject: Too small prefix: ", net, " ", bgp_path; 59 | reject; 60 | } 61 | } 62 | 63 | # long as paths 64 | function reject_long_aspaths() 65 | { 66 | if ( bgp_path.len > 100 ) then { 67 | print "Reject: Too long AS path: ", net, " ", bgp_path; 68 | reject; 69 | } 70 | } 71 | 72 | # known transit AS 73 | 74 | define TRANSIT_ASNS = [ 174, # Cogent 75 | 701, # UUNET 76 | 1299, # Telia 77 | 2914, # NTT Ltd. 78 | 3257, # GTT Backbone 79 | 3320, # Deutsche Telekom AG (DTAG) 80 | 3356, # Level3 81 | 3491, # PCCW 82 | 4134, # Chinanet 83 | 5511, # Orange opentransit 84 | 6453, # Tata Communications 85 | 6461, # Zayo Bandwidth 86 | 6762, # Seabone / Telecom Italia 87 | 6830, # Liberty Global 88 | 7018 ]; # AT&T 89 | function reject_transit_paths() 90 | int set transit_asns; 91 | { 92 | transit_asns = TRANSIT_ASNS; 93 | if (bgp_path ~ transit_asns) then { 94 | #print "Reject: Transit ASNs found on IXP: ", net, " ", bgp_path; 95 | reject; 96 | } 97 | } 98 | 99 | 100 | filter transit { 101 | #reject_invalids(); #rpki not implemented yet for testing in bgperf 102 | reject_bogon_prefixes(); 103 | reject_bogon_asns(); 104 | #reject_long_aspaths(); # don't have an equivilent for frr 105 | #reject_small_prefixes(); 106 | 107 | accept; 108 | } 109 | 110 | filter ixp { 111 | # reject_invalids();#rpki not implemented yet for testing in bgperf 112 | reject_bogon_prefixes(); 113 | reject_bogon_asns(); 114 | #reject_long_aspaths(); # don't have an equivilent for frr 115 | reject_transit_paths(); 116 | reject_small_prefixes(); 117 | 118 | accept; 119 | } 120 | 121 | -------------------------------------------------------------------------------- /filters/rustybgpd.conf: -------------------------------------------------------------------------------- 1 | [[policy-definitions]] 2 | name = "ixp" 3 | [[policy-definitions.statements]] 4 | name = "bogon-prefix-statement" 5 | [policy-definitions.statements.conditions.match-prefix-set] 6 | prefix-set = "bogon-prefix" 7 | match-set-options = "any" 8 | [policy-definitions.statements.actions] 9 | route-disposition = "reject-route" 10 | # 11 | [[policy-definitions.statements]] 12 | name = "bogon-asn-statement" 13 | [policy-definitions.statements.conditions.bgp-conditions.match-as-path-set] 14 | as-path-set = "bogon-asn" 15 | match-set-options = "any" 16 | [policy-definitions.statements.actions] 17 | route-disposition = "reject-route" 18 | # 19 | [[policy-definitions.statements]] 20 | name = "small-prefix-statement" 21 | [policy-definitions.statements.conditions.match-prefix-set] 22 | prefix-set = "small-prefix" 23 | match-set-options = "any" 24 | [policy-definitions.statements.actions] 25 | route-disposition = "reject-route" 26 | # 27 | [[policy-definitions.statements]] 28 | name = "transit-asn-statement" 29 | [policy-definitions.statements.conditions.bgp-conditions.match-as-path-set] 30 | as-path-set = "transit-asn" 31 | match-set-options = "any" 32 | [policy-definitions.statements.actions] 33 | route-disposition = "reject-route" 34 | # 35 | [[policy-definitions.statements]] 36 | name = "long-aspath-statement" 37 | [policy-definitions.statements.conditions.bgp-conditions.as-path-length] 38 | operator = "ge" 39 | value = 100 40 | [policy-definitions.statements.actions] 41 | route-disposition = "reject-route" 42 | 43 | [[policy-definitions]] 44 | name = "transit" 45 | [[policy-definitions.statements]] 46 | name = "bogon-prefix-statement" 47 | [[policy-definitions.statements]] 48 | name = "bogon-asn-statement" 49 | # [[policy-definitions.statements]] 50 | # name = "small-prefix-statement" 51 | [[policy-definitions.statements]] 52 | name = "long-aspath-statement" 53 | 54 | 55 | [defined-sets] 56 | [[defined-sets.bgp-defined-sets.as-path-sets]] 57 | as-path-set-name = "bogon-asn" 58 | as-path-list = ["_0_", "_23456_", "_64496-64511_", "_64512-65534_", "_65535_", "_65536-65551_", "_65552-131071_", "_4200000000-4294967294_", "_4294967295_"] 59 | [[defined-sets.bgp-defined-sets.as-path-sets]] 60 | as-path-set-name = "transit-asn" 61 | as-path-list = ["_174_", "_701_", "_1299_", "_2914_", "_3257_", "_3320_", "_3356_", "_3491_", "_4134_", "_5511_", "_6453_", "_6762_", "_6830_", "_7018_"] 62 | 63 | [[defined-sets.prefix-sets]] 64 | prefix-set-name = "bogon-prefix" 65 | # [[defined-sets.prefix-sets.prefix-list]] 66 | # ip-prefix = "0.0.0.0/8" 67 | # masklength-range = "8..32" 68 | [[defined-sets.prefix-sets.prefix-list]] 69 | ip-prefix = "10.0.0.0/8" 70 | masklength-range = "8..32" 71 | [[defined-sets.prefix-sets.prefix-list]] 72 | ip-prefix = "100.64.0.0/10" 73 | masklength-range = "10..32" 74 | [[defined-sets.prefix-sets.prefix-list]] 75 | ip-prefix = "127.0.0.0/12" 76 | masklength-range = "8..32" 77 | [[defined-sets.prefix-sets.prefix-list]] 78 | ip-prefix = "169.254.0.0/16" 79 | masklength-range = "16..32" 80 | [[defined-sets.prefix-sets.prefix-list]] 81 | ip-prefix = "172.16.0.0/12" 82 | masklength-range = "12..32" 83 | [[defined-sets.prefix-sets.prefix-list]] 84 | ip-prefix = "192.0.2.0/24" 85 | masklength-range = "24..32" 86 | [[defined-sets.prefix-sets.prefix-list]] 87 | ip-prefix = "192.88.99.0/24" 88 | masklength-range = "24..32" 89 | [[defined-sets.prefix-sets.prefix-list]] 90 | ip-prefix = "192.168.0.0/16" 91 | masklength-range = "16..32" 92 | [[defined-sets.prefix-sets.prefix-list]] 93 | ip-prefix = "198.18.0.0/15" 94 | masklength-range = "15..32" 95 | [[defined-sets.prefix-sets.prefix-list]] 96 | ip-prefix = "198.51.100.0/24" 97 | masklength-range = "24..32" 98 | [[defined-sets.prefix-sets.prefix-list]] 99 | ip-prefix = "203.0.113.0/24" 100 | masklength-range = "24..32" 101 | [[defined-sets.prefix-sets.prefix-list]] 102 | ip-prefix = "224.0.0.0/4" 103 | masklength-range = "4..32" 104 | [[defined-sets.prefix-sets.prefix-list]] 105 | ip-prefix = "240.0.0.0/4" 106 | masklength-range = "4..32" 107 | 108 | [[defined-sets.prefix-sets]] 109 | prefix-set-name = "small-prefix" 110 | [[defined-sets.prefix-sets.prefix-list]] 111 | ip-prefix = "0.0.0.0/0" 112 | masklength-range = "25..32" 113 | -------------------------------------------------------------------------------- /tester.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2016 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 base import Tester 17 | from exabgp import ExaBGP 18 | from bird import BIRD 19 | from settings import dckr 20 | from subprocess import check_output, Popen, PIPE 21 | 22 | 23 | class ExaBGPTester(Tester, ExaBGP): 24 | 25 | CONTAINER_NAME_PREFIX = 'bgperf_exabgp_tester_' 26 | 27 | def __init__(self, name, host_dir, conf, image='bgperf/exabgp'): 28 | super(ExaBGPTester, self).__init__(name, host_dir, conf, image) 29 | 30 | def configure_neighbors(self, target_conf): 31 | peers = list(self.conf.get('neighbors', {}).values()) 32 | 33 | for p in peers: 34 | with open('{0}/{1}.conf'.format(self.host_dir, p['router-id']), 'w') as f: 35 | local_address = p['local-address'] 36 | config = '''neighbor {0} {{ 37 | peer-as {1}; 38 | router-id {2}; 39 | local-address {3}; 40 | local-as {4}; 41 | static {{ 42 | '''.format(target_conf['local-address'], target_conf['as'], 43 | p['router-id'], local_address, p['as']) 44 | f.write(config) 45 | for path in p['paths']: 46 | f.write(' route {0} next-hop {1};\n'.format(path, local_address)) 47 | f.write(''' } 48 | }''') 49 | 50 | def get_startup_cmd(self): 51 | startup = ['''#!/bin/bash 52 | ulimit -n 65536'''] 53 | peers = list(self.conf.get('neighbors', {}).values()) 54 | for p in peers: 55 | startup.append('''env exabgp.log.destination={0}/{1}.log \ 56 | exabgp.daemon.daemonize=true \ 57 | exabgp.daemon.user=root \ 58 | exabgp {0}/{1}.conf'''.format(self.guest_dir, p['router-id'])) 59 | return '\n'.join(startup) 60 | 61 | 62 | class BIRDTester(Tester, BIRD): 63 | 64 | CONTAINER_NAME_PREFIX = 'bgperf_bird_tester_' 65 | 66 | def __init__(self, name, host_dir, conf, image='bgperf/bird'): 67 | super(BIRDTester, self).__init__('bgperf_bird_' + name, host_dir, conf, image) 68 | 69 | def configure_neighbors(self, target_conf): 70 | peers = list(self.conf.get('neighbors', {}).values()) 71 | 72 | for p in peers: 73 | with open('{0}/{1}.conf'.format(self.host_dir, p['router-id']), 'w') as f: 74 | local_address = p['local-address'] 75 | config = '''log "{5}/{2}.log" all; 76 | #debug protocols all; 77 | debug protocols {{states}}; 78 | router id {2}; 79 | protocol device {{}} 80 | protocol bgp {{ 81 | #hold time 5; 82 | source address {3}; 83 | connect delay time 1; 84 | interface "eth0"; 85 | strict bind; 86 | ipv4 {{ import none; export all; }}; 87 | local {3} as {4}; 88 | neighbor {0} as {1}; 89 | }} 90 | protocol static {{ ipv4; 91 | '''.format(target_conf['local-address'], target_conf['as'], 92 | p['router-id'], local_address, p['as'], self.guest_dir) 93 | f.write(config) 94 | for path in p['paths']: 95 | f.write(' route {0} via {1};\n'.format(path, local_address)) 96 | f.write('}') 97 | 98 | def get_startup_cmd(self): 99 | startup = [f'''#!/bin/bash 100 | ulimit -n 65536 101 | #sleep 2 102 | #(ip link; ip addr) > {self.guest_dir}/ip-a.log 103 | '''] 104 | peers = list(self.conf.get('neighbors', {}).values()) 105 | for p in peers: 106 | startup.append('''bird -c {0}/{1}.conf -s {0}/{1}.ctl >>{0}/{1}.log 2>&1\n'''.format(self.guest_dir, p['router-id'])) 107 | return '\n'.join(startup) 108 | 109 | def find_errors(): 110 | grep1 = Popen(('grep RMT /tmp/bgperf2/tester/*.log'), shell=True, stdout=PIPE) 111 | grep2 = Popen(('grep', '-v', 'NEXT_HOP'), stdin=grep1.stdout, stdout=PIPE) 112 | errors = check_output(('wc', '-l'), stdin=grep2.stdout) 113 | grep1.wait() 114 | grep2.wait() 115 | return errors.decode('utf-8').strip() -------------------------------------------------------------------------------- /monitor.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2016 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 json.decoder import JSONDecodeError 17 | from gobgp import GoBGP 18 | import os 19 | from settings import dckr 20 | import yaml 21 | import json 22 | from threading import Thread 23 | import time 24 | import datetime 25 | 26 | def rm_line(): 27 | print('\x1b[1A\x1b[2K\x1b[1D\x1b[1A') 28 | 29 | class Monitor(GoBGP): 30 | 31 | CONTAINER_NAME = 'bgperf_monitor' 32 | 33 | def run(self, conf, dckr_net_name=''): 34 | ctn = super(GoBGP, self).run(dckr_net_name) 35 | config = {} 36 | config['global'] = { 37 | 'config': { 38 | 'as': conf['monitor']['as'], 39 | 'router-id': conf['monitor']['router-id'], 40 | }, 41 | } 42 | config ['neighbors'] = [{'config': {'neighbor-address': conf['target']['local-address'], 43 | 'peer-as': conf['target']['as']}, 44 | 'transport': {'config': {'local-address': conf['monitor']['local-address']}}, 45 | 'timers': {'config': {'connect-retry': 10}}}] 46 | with open('{0}/{1}'.format(self.host_dir, 'gobgpd.conf'), 'w') as f: 47 | f.write(yaml.dump(config)) 48 | self.config_name = 'gobgpd.conf' 49 | startup = '''#!/bin/bash 50 | ulimit -n 65536 51 | gobgpd -t yaml -f {1}/{2} -l {3} > {1}/gobgpd.log 2>&1 52 | '''.format(conf['monitor']['local-address'], self.guest_dir, self.config_name, 'info') 53 | filename = '{0}/start.sh'.format(self.host_dir) 54 | with open(filename, 'w') as f: 55 | f.write(startup) 56 | os.chmod(filename, 0o777) 57 | i = dckr.exec_create(container=self.name, cmd='{0}/start.sh'.format(self.guest_dir)) 58 | dckr.exec_start(i['Id'], detach=True, socket=True) 59 | self.config = conf 60 | return ctn 61 | 62 | def local(self, cmd, stream=False): 63 | i = dckr.exec_create(container=self.name, cmd=cmd) 64 | return dckr.exec_start(i['Id'], stream=stream) 65 | 66 | def wait_established(self, neighbor): 67 | n = 0 68 | while True: 69 | if n > 0: 70 | rm_line() 71 | print(f"Waiting {n} seconds for monitor") 72 | 73 | neighbor_data = self.local('gobgp neighbor {0} -j'.format(neighbor)).decode('utf-8') 74 | 75 | try: 76 | neigh = json.loads(neighbor_data) 77 | except JSONDecodeError: 78 | neigh = {'state': {'session_state': 'failed'}} 79 | 80 | 81 | if ((neigh['state']['session_state'] == 'established') or 82 | (neigh['state']['session_state'] == 6)): 83 | 84 | return n 85 | time.sleep(1) 86 | 87 | n = n+1 88 | 89 | def stats(self, queue): 90 | self.stop_monitoring = False 91 | def stats(): 92 | cps = self.config['monitor']['check-points'] if 'check-points' in self.config['monitor'] else [] 93 | while True: 94 | if self.stop_monitoring: 95 | return 96 | try: 97 | info = json.loads(self.local('gobgp neighbor -j').decode('utf-8'))[0] 98 | except Exception as e: 99 | print(f"Monitoring reading exception {self.monitor_for}: {e} ") 100 | continue 101 | 102 | info['who'] = self.name 103 | state = info['afi_safis'][0]['state'] 104 | if 'accepted'in state and len(cps) > 0 and int(cps[0]) <= int(state['accepted']): 105 | #cps.pop(0) 106 | info['checked'] = True 107 | else: 108 | info['checked'] = False 109 | info['time'] = datetime.datetime.now() 110 | queue.put(info) 111 | time.sleep(1) 112 | 113 | t = Thread(target=stats) 114 | t.daemon = True 115 | t.start() 116 | 117 | -------------------------------------------------------------------------------- /flock.py: -------------------------------------------------------------------------------- 1 | from base import * 2 | import json 3 | 4 | class Flock(Container): 5 | CONTAINER_NAME = None 6 | GUEST_DIR = '/root/config' 7 | 8 | def __init__(self, host_dir, conf, image='bgperf/flock'): 9 | super(Flock, self).__init__(self.CONTAINER_NAME, image, host_dir, self.GUEST_DIR, conf) 10 | 11 | @classmethod 12 | def build_image(cls, force=False, tag='bgperf/flock', checkout='', nocache=False): 13 | 14 | cls.dockerfile = ''' 15 | FROM debian:latest 16 | 17 | RUN apt update \ 18 | && apt -y dist-upgrade \ 19 | && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends tzdata \ 20 | && apt-get install -y curl systemd iputils-ping sudo psutils procps iproute2\ 21 | && rm -rf /var/lib/apt/lists/* /var/cache/apt/* 22 | 23 | RUN curl 'https://www.flocknetworks.com/?smd_process_download=1&download_id=429' --output flockd_21.1.0_amd64.deb && \ 24 | dpkg -i ./flockd_21.1.0_amd64.deb 25 | 26 | '''.format(checkout) 27 | super(Flock, cls).build_image(force, tag, nocache) 28 | 29 | 30 | class FlockTarget(Flock, Target): 31 | 32 | CONTAINER_NAME = 'bgperf_flock_target' 33 | CONFIG_FILE_NAME = 'bgpd.conf' 34 | 35 | def __init__(self, host_dir, conf, image='bgperf/flock'): 36 | super(FlockTarget, self).__init__(host_dir, conf, image=image) 37 | 38 | def write_config(self): 39 | config = {} 40 | config["system"] = {"api": {"rest": {"bind_ip_addr": "127.0.0.1"}}} 41 | config["system"]["api"]["netlink_recv"] = True 42 | 43 | config["bgp"] = {} 44 | config["bgp"]["local"] = {} 45 | config["bgp"]["local"]["id"] = self.conf['router-id'] 46 | config["bgp"]["local"]["asn"] = self.conf['as'] 47 | config["bgp"]["local"]["router_server"] = True # -- not yet 48 | # config["static"] = {"static_routes":[{ "ip_net": "10.10.0.0/16"} ]} # from scenario this is lcal_prefix but don't have access to that here 49 | # config["static"]["static_routes"][0]["next_hops"] = [{"intf_name": "eth0"}] #not sure where 10.10.0.1 comes from 50 | # config["static"]["static_routes"].append({"ip_net": "10.10.0.3/32", "next_hops": [{ "intf_name": "eth0"}]}) 51 | 52 | 53 | 54 | def gen_neighbor_config(n): 55 | config = {} 56 | config["asn"] = n['as'] 57 | config["neighbor"] = [] 58 | config["neighbor"].append({"ip": n['router-id'], "local_ip": self.conf['router-id'], 59 | "af": [{"afi": "ipv4", "safi": "unicast"}]}) 60 | return config 61 | 62 | 63 | 64 | config["bgp"]["as"] = [] 65 | 66 | for n in sorted(list(flatten(list(t.get('neighbors', {}).values()) for t in self.scenario_global_conf['testers'])) + 67 | [self.scenario_global_conf['monitor']], key=lambda n: n['as']): 68 | config["bgp"]["as"].append(gen_neighbor_config(n)) 69 | 70 | with open('{0}/{1}'.format(self.host_dir, self.CONFIG_FILE_NAME), 'w') as f: 71 | f.write(json.dumps(config)) 72 | f.flush() 73 | 74 | def get_startup_cmd(self): 75 | return '\n'.join( 76 | ['#!/bin/bash', 77 | 'ulimit -n 65536', 78 | 'cp {guest_dir}/{config_file_name} /etc/flockd/flockd.json', 79 | 'RUST_LOG="info,bgp=debug" FLOCK_LOG="info,bgp=debug" /usr/sbin/flockd > {guest_dir}/flock.log 2>&1'] 80 | ).format( 81 | guest_dir=self.guest_dir, 82 | config_file_name=self.CONFIG_FILE_NAME, 83 | debug_level='info') 84 | 85 | def get_version_cmd(self): 86 | return "/usr/bin/flockc -V" 87 | 88 | def exec_version_cmd(self): 89 | version = self.get_version_cmd() 90 | i= dckr.exec_create(container=self.name, cmd=version, stderr=True) 91 | return dckr.exec_start(i['Id'], stream=False, detach=False).decode('utf-8').strip('\n') 92 | 93 | def get_neighbors_state(self): 94 | neighbors_accepted = {} 95 | neighbor_received_output = json.loads(self.local("/usr/bin/flockc bgp --host 127.0.0.1 -J").decode('utf-8')) 96 | return neighbor_received_output['neighbor_summary']['default']['recv_converged'] 97 | 98 | def get_neighbor_received_routes(self): 99 | ## if we call this before the daemon starts we will not get output 100 | 101 | tester_count, neighbors_checked = self.get_test_counts() 102 | neighbors_accepted = self.get_neighbors_state() - 1 # have to discount the monitor 103 | i = 0 104 | for n in neighbors_checked.keys(): 105 | if i >= neighbors_accepted: 106 | break 107 | neighbors_checked[n] = True 108 | i += 1 109 | 110 | 111 | return neighbors_checked, neighbors_checked 112 | 113 | 114 | -------------------------------------------------------------------------------- /bgpdump2.py: -------------------------------------------------------------------------------- 1 | import re 2 | from subprocess import check_output, Popen, PIPE 3 | from base import * 4 | from mrt_tester import MRTTester 5 | 6 | class Bgpdump2(Container): 7 | 8 | GUEST_DIR = '/root/config' 9 | 10 | CONTAINER_NAME = 'bgperf_bgpdump2_target' 11 | 12 | def __init__(self, host_dir, conf, image='bgperf/bgpdump2'): 13 | super(Bgpdump2, self).__init__(self.CONTAINER_NAME, image, host_dir, self.GUEST_DIR, conf) 14 | 15 | 16 | @classmethod 17 | def build_image(cls, force=False, tag='bgperf/bgpdump2', checkout='HEAD', nocache=False): 18 | cls.dockerfile = ''' 19 | FROM ubuntu:20.04 20 | WORKDIR /root 21 | 22 | RUN apt update \ 23 | && apt -y dist-upgrade \ 24 | && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends tzdata \ 25 | && apt-get install -y git libarchive-dev libbz2-dev liblz-dev zlib1g-dev autoconf \ 26 | gcc wget make iputils-ping automake-1.15 \ 27 | && rm -rf /var/lib/apt/lists/* /var/cache/apt/* 28 | 29 | RUN git clone https://github.com/rtbrick/bgpdump2.git \ 30 | && cd bgpdump2 \ 31 | && ./configure \ 32 | && make \ 33 | && mv src/bgpdump2 /usr/local/sbin/ 34 | 35 | RUN touch /root/mrt_file 36 | 37 | ENTRYPOINT ["/bin/bash"] 38 | '''.format(checkout) 39 | super(Bgpdump2, cls).build_image(force, tag, nocache) 40 | 41 | 42 | 43 | class Bgpdump2Tester(Tester, Bgpdump2, MRTTester): 44 | CONTAINER_NAME_PREFIX = 'bgperf_bgpdump2_tester_' 45 | 46 | def __init__(self, name, host_dir, conf, image='bgperf/bgpdump2'): 47 | super(Bgpdump2Tester, self).__init__(name, host_dir, conf, image) 48 | 49 | def configure_neighbors(self, target_conf): 50 | # this doesn't really do anything, but we use it to find the target 51 | self.target_ip = target_conf['local-address'] 52 | return None 53 | 54 | 55 | def get_index_valid(self, prefix_count): 56 | good_indexes = [] 57 | counts = self.local(f"/usr/local/sbin/bgpdump2 -c /root/mrt_file").decode('utf-8').split('\n')[1] 58 | counts = counts.split(',') 59 | counts.pop(0) # first item is timestamp, we don't care 60 | for i, c in enumerate(counts): 61 | if int(c) >= int(prefix_count): 62 | good_indexes.append(i) 63 | if len(good_indexes) < 1: 64 | print(f"No mrt data has {prefix_count} of prefixes to send") 65 | exit(1) 66 | print(f"{len(good_indexes)} peers with more than {prefix_count} prefixes in this MRT data") 67 | return good_indexes 68 | 69 | def get_index_useful_neighbor(self, prefix_count): 70 | ''' dynamically figure out which of the indexes in the mrt file have enough data''' 71 | good_indexes = self.get_index_valid(prefix_count) 72 | 73 | if 'mrt-index' in self.conf: 74 | return good_indexes[self.conf['mrt-index'] % len(good_indexes)] 75 | else: 76 | return 3 77 | 78 | def get_index_asns(self): 79 | index_asns = {} 80 | asn = re.compile(r".*peer_table\[(\d+)\].*asn:(\d+).*") 81 | r_table = self.local(f"/usr/local/sbin/bgpdump2 -P /root/mrt_file").decode('utf-8').splitlines() 82 | for line in r_table: 83 | m_asn = asn.match(line) 84 | if m_asn: 85 | g_asn = m_asn.groups() 86 | index_asns[int(g_asn[0])] = int(g_asn[1]) 87 | 88 | return index_asns 89 | 90 | def get_local_as(self, index): 91 | index_asns = self.get_index_asns() 92 | return index_asns[index] 93 | 94 | 95 | def get_startup_cmd(self): 96 | 97 | # just get the first neighbor, we can only handle one neighbor per container 98 | neighbor = next(iter(self.conf['neighbors'].values())) 99 | prefix_count = neighbor['count'] 100 | index = self.conf['bgpdump-index'] if 'bgpdump-index' in self.conf else self.get_index_useful_neighbor(prefix_count) 101 | local_as = self.get_local_as(index) or neighbor['as'] 102 | startup = '''#!/bin/bash 103 | ulimit -n 65536 104 | /usr/local/sbin/bgpdump2 --blaster {} -p {} -a {} /root/mrt_file -T {} -S {}> {}/bgpdump2.log 2>&1 & 105 | 106 | '''.format(self.target_ip, index, 107 | local_as, prefix_count, neighbor['local-address'], self.guest_dir) 108 | return startup 109 | #> {}/bgpdump2.log 2>&1 110 | 111 | def find_errors(): 112 | grep1 = Popen(('grep -i error /tmp/bgperf2/mrt-injector*/*.log'), shell=True, stdout=PIPE) 113 | errors = check_output(('wc', '-l'), stdin=grep1.stdout) 114 | grep1.wait() 115 | return errors.decode('utf-8').strip() 116 | 117 | def find_timeouts(): 118 | grep1 = Popen(('grep -i timeout /tmp/bgperf2/mrt-injector*/*.log'), shell=True, stdout=PIPE) 119 | timeouts = check_output(('wc', '-l'), stdin=grep1.stdout) 120 | grep1.wait() 121 | return timeouts.decode('utf-8').strip() 122 | -------------------------------------------------------------------------------- /junos.py: -------------------------------------------------------------------------------- 1 | from jinja2.loaders import FileSystemLoader 2 | from base import * 3 | import json 4 | import gzip 5 | import os 6 | from shutil import copyfile 7 | 8 | 9 | class Junos(Container): 10 | CONTAINER_NAME = None 11 | GUEST_DIR = '/config' 12 | LOG_DIR = '/var/log' 13 | 14 | def __init__(self, host_dir, conf, image='crpd'): 15 | super(Junos, self).__init__(self.CONTAINER_NAME, image, host_dir, self.GUEST_DIR, conf) 16 | self.volumes = [self.guest_dir, self.LOG_DIR] 17 | self.host_log_dir = f"{host_dir}/log" 18 | if not os.path.exists(self.host_log_dir): 19 | os.makedirs(self.host_log_dir) 20 | os.chmod(self.host_log_dir, 0o777) 21 | 22 | 23 | # don't build just download 24 | # assume that you do this by hand 25 | @classmethod 26 | def build_image(cls, force=False, tag='crpd', checkout='', nocache=False): 27 | cls.dockerfile = '' 28 | print("Can't build junos, must download yourself") 29 | print("https://www.juniper.net/us/en/dm/crpd-free-trial.html") 30 | print("Must also tag image: docker tag 'crpd:21.3R1-S1.1 crpd:latest'") 31 | 32 | 33 | class JunosTarget(Junos, Target): 34 | 35 | CONTAINER_NAME = 'bgperf_junos_target' 36 | CONFIG_FILE_NAME = 'juniper.conf.gz' 37 | 38 | def __init__(self, host_dir, conf, image='crpd'): 39 | super(JunosTarget, self).__init__(host_dir, conf, image=image) 40 | if not 'license_file' in self.conf or not self.conf['license_file']: 41 | print(f"Junos requires a license file") 42 | exit(1) 43 | if not os.path.exists(self.conf['license_file']): 44 | print(f"license file {self.conf['license_file']} doesen't exist") 45 | exit(1) 46 | 47 | def write_config(self): 48 | bgp = {} 49 | bgp['neighbors'] = [] 50 | bgp['asn'] = self.conf['as'] 51 | bgp['router-id'] = self.conf['router-id'] 52 | # junper suggests areound half avaible cores 53 | bgp['cores'] = os.cpu_count() // 2 if os.cpu_count() < 63 else 31 54 | if 'filter_test' in self.conf: 55 | bgp['filter'] = self.conf['filter_test'] 56 | 57 | bgp['license'] = self.get_license_key(self.conf['license_file']) 58 | 59 | for n in sorted(list(flatten(list(t.get('neighbors', {}).values()) for t in self.scenario_global_conf['testers'])) + 60 | [self.scenario_global_conf['monitor']], key=lambda n: n['as']): 61 | bgp['neighbors'].append(n) 62 | config = self.get_template(bgp, template_file="junos.j2") 63 | 64 | # junos expects the config file to be compressed 65 | with gzip.open('{0}/{1}'.format(self.host_dir, self.CONFIG_FILE_NAME), 'w') as f: 66 | f.write(config.encode('utf8')) 67 | f.write(self.get_filter_test_config().encode('utf8')) 68 | f.flush() 69 | 70 | def get_filter_test_config(self): 71 | file = open("filters/junos.conf", mode='r') 72 | filters = file.read() 73 | file.close 74 | return filters 75 | 76 | def get_license_key(self, license_file): 77 | with open(license_file) as f: 78 | data = f.readlines()[0].strip('\n') 79 | return data 80 | 81 | def exec_startup_cmd(self, stream=False, detach=False): 82 | return None 83 | 84 | 85 | def get_version_cmd(self): 86 | return "cli show version" 87 | 88 | def exec_version_cmd(self): 89 | version = self.get_version_cmd() 90 | i= dckr.exec_create(container=self.name, cmd=version, stderr=True) 91 | return dckr.exec_start(i['Id'], stream=False, detach=False).decode('utf-8').split('\n')[3].strip('\n').split(':')[1].split(' ')[1] 92 | 93 | 94 | def get_neighbors_state(self): 95 | neighbors_accepted = {} 96 | neighbors_received = {} 97 | neighbor_received_output = json.loads(self.local("cli show bgp neighbor \| no-more \| display json").decode('utf-8')) 98 | 99 | for neighbor in neighbor_received_output['bgp-information'][0]['bgp-peer']: 100 | 101 | ip = neighbor['peer-address'][0]["data"].split('+')[0] 102 | if 'bgp-rib' in neighbor: 103 | neighbors_received[ip] = int(neighbor['bgp-rib'][0]['received-prefix-count'][0]["data"]) 104 | neighbors_accepted[ip] = int(neighbor['bgp-rib'][0]['accepted-prefix-count'][0]["data"]) 105 | else: 106 | neighbors_received[ip] = 0 107 | neighbors_accepted[ip] = 0 108 | 109 | return neighbors_received, neighbors_accepted 110 | 111 | 112 | 113 | # have to complete copy and add from parent because we need to bind an extra volume 114 | def get_host_config(self): 115 | host_config = dckr.create_host_config( 116 | binds=['{0}:{1}'.format(os.path.abspath(self.host_dir), self.guest_dir), 117 | '{0}:{1}'.format(os.path.abspath(self.host_log_dir), self.LOG_DIR) ], 118 | privileged=True, 119 | network_mode='bridge', 120 | cap_add=['NET_ADMIN'] 121 | ) 122 | return host_config 123 | 124 | 125 | -------------------------------------------------------------------------------- /srlinux.py: -------------------------------------------------------------------------------- 1 | from base import * 2 | import json 3 | 4 | 5 | class SRLinux(Container): 6 | CONTAINER_NAME = None 7 | GUEST_DIR = '/etc/opt/srlinux' 8 | 9 | def __init__(self, host_dir, conf, image='ghcr.io/nokia/srlinux'): 10 | super(SRLinux, self).__init__(self.CONTAINER_NAME, image, host_dir, self.GUEST_DIR, conf) 11 | 12 | 13 | # don't build just download from docker pull ghcr.io/nokia/srlinux 14 | # assume that you do this by hand 15 | @classmethod 16 | def build_image(cls, force=False, tag='ghcr.io/nokia/srlinux', checkout='', nocache=False): 17 | cls.dockerfile = '' 18 | print("Can't build SRLinux, must download yourself") 19 | print("docker pull ghcr.io/nokia/srlinux") 20 | 21 | 22 | class SRLinuxTarget(SRLinux, Target): 23 | 24 | CONTAINER_NAME = 'bgperf_SRLinux_target' 25 | CONFIG_FILE_NAME = 'config.json' 26 | 27 | def __init__(self, host_dir, conf, image='ghcr.io/nokia/srlinux'): 28 | super(SRLinuxTarget, self).__init__(host_dir, conf, image=image) 29 | 30 | def write_config(self): 31 | config = {} 32 | key = "network-instance" 33 | bgp = 'srl_nokia-bgp:bgp' 34 | 35 | config = ''' 36 | enter candidate 37 | set / network-instance default 38 | set / network-instance default protocols 39 | set / network-instance default protocols bgp 40 | set / network-instance default protocols bgp admin-state enable 41 | set / network-instance default protocols bgp router-id {0} 42 | set / network-instance default protocols bgp autonomous-system {1} 43 | set / network-instance default protocols bgp group neighbors 44 | set / network-instance default protocols bgp group neighbors ipv4-unicast 45 | set / network-instance default protocols bgp group neighbors ipv4-unicast admin-state enable 46 | '''.format(self.conf['router-id'], self.conf['as']) 47 | 48 | config = {} 49 | config[key] = {"default": {"protocols": {"bgp": {}}}} 50 | config[key]["default"]["protocols"]["bgp"]["admin-state"] = 'enable' 51 | config[key]["default"]["protocols"]["bgp"]["autonomous-system"] = self.conf['as'] 52 | config[key]["default"]["protocols"]["bgp"]["router-id"] = self.conf['router-id'] 53 | config[key]["default"]["protocols"]["bgp"]['group neighbors'] = {"ipv4-unicast": {"admin-state": "enable"}} 54 | 55 | 56 | 57 | def gen_neighbor_config(n): 58 | config = ''' 59 | set / network-instance default protocols bgp neighbor {0} 60 | set / network-instance default protocols bgp neighbor {0} peer-as {1} 61 | set / network-instance default protocols bgp neighbor {0} peer-group neighbors 62 | 63 | '''.format(n['router-id'], n['as']) 64 | config = {f"neighbor {n['router-id']}": {}} 65 | config[f"neighbor {n['router-id']}"]["peer-as"] = n["as"] 66 | config[f"neighbor {n['router-id']}"]["peer-group"] = "neighbors" 67 | 68 | return config 69 | 70 | 71 | def gen_prefix_configs(n): 72 | pass 73 | 74 | def gen_filter(name, match): 75 | pass 76 | 77 | def gen_prefix_filter(n, match): 78 | pass 79 | 80 | def gen_aspath_filter(n, match): 81 | pass 82 | 83 | def gen_community_filter(n, match): 84 | pass 85 | 86 | def gen_ext_community_filter(n, match): 87 | pass 88 | 89 | 90 | for n in sorted(list(flatten(list(t.get('neighbors', {}).values()) for t in self.scenario_global_conf['testers'])) + 91 | [self.scenario_global_conf['monitor']], key=lambda n: n['as']): 92 | config[key]["default"]["protocols"].update(gen_neighbor_config(n)) 93 | 94 | 95 | with open('{0}/{1}'.format(self.host_dir, self.CONFIG_FILE_NAME), 'w') as f: 96 | f.write(json.dumps(config)) 97 | f.flush() 98 | 99 | 100 | 101 | def exec_startup_cmd(self, stream=False, detach=False): 102 | return self.local('sudo bash -c /opt/srlinux/bin/sr_linux', 103 | detach=detach, 104 | stream=stream) 105 | 106 | 107 | def get_version_cmd(self): 108 | return "/usr/bin/SRLinuxc -V" 109 | 110 | def exec_version_cmd(self): 111 | version = self.get_version_cmd() 112 | i= dckr.exec_create(container=self.name, cmd=version, stderr=True) 113 | return dckr.exec_start(i['Id'], stream=False, detach=False).decode('utf-8').strip('\n') 114 | 115 | 116 | def get_neighbors_state(self): 117 | neighbors_accepted = {} 118 | neighbor_received_output = json.loads(self.local("/usr/bin/SRLinuxc bgp --host 127.0.0.1 -J").decode('utf-8')) 119 | 120 | return neighbor_received_output['neighbor_summary']['recv_converged'] 121 | 122 | def get_neighbor_received_routes(self): 123 | ## if we call this before the daemon starts we will not get output 124 | 125 | tester_count, neighbors_checked = self.get_test_counts() 126 | neighbors_accepted = self.get_neighbors_state() - 1 # have to discount the monitor 127 | 128 | i = 0 129 | for n in neighbors_checked.keys(): 130 | if i >= neighbors_accepted: 131 | break 132 | neighbors_checked[n] = True 133 | i += 1 134 | 135 | 136 | return neighbors_checked, neighbors_checked 137 | 138 | 139 | -------------------------------------------------------------------------------- /openbgp.py: -------------------------------------------------------------------------------- 1 | 2 | from base import * 3 | import json 4 | 5 | class OpenBGP(Container): 6 | CONTAINER_NAME = None 7 | GUEST_DIR = '/root/config' 8 | 9 | def __init__(self, host_dir, conf, image='bgperf/openbgp'): 10 | super(OpenBGP, self).__init__(self.CONTAINER_NAME, image, host_dir, self.GUEST_DIR, conf) 11 | 12 | @classmethod 13 | def build_image(cls, force=False, tag='bgperf/openbgp', checkout='', nocache=False): 14 | 15 | cls.dockerfile = ''' 16 | FROM openbgpd/openbgpd 17 | 18 | '''.format(checkout) 19 | super(OpenBGP, cls).build_image(force, tag, nocache) 20 | 21 | 22 | class OpenBGPTarget(OpenBGP, Target): 23 | 24 | CONTAINER_NAME = 'bgperf_openbgp_target' 25 | CONFIG_FILE_NAME = 'bgpd.conf' 26 | 27 | def __init__(self, host_dir, conf, image='bgperf/openbgp'): 28 | super(OpenBGPTarget, self).__init__(host_dir, conf, image=image) 29 | 30 | def write_config(self): 31 | 32 | config = """ASN="{0}" 33 | 34 | AS $ASN 35 | router-id {1} 36 | fib-update no 37 | """.format(self.conf['as'], self.conf['router-id']) 38 | 39 | def gen_neighbor_config(n): 40 | return ('''neighbor {0} {{ 41 | remote-as {1} 42 | enforce neighbor-as no 43 | }} 44 | '''.format(n['router-id'], n['as']) ) 45 | 46 | 47 | def gen_prefix_configs(n): 48 | pass 49 | 50 | def gen_filter(name, match): 51 | c = ['function {0}()'.format(name), '{'] 52 | for typ, name in match: 53 | c.append(' if ! {0}() then return false;'.format(name)) 54 | c.append('return true;') 55 | c.append('}') 56 | return '\n'.join(c) + '\n' 57 | 58 | def gen_prefix_filter(n, match): 59 | pass 60 | 61 | def gen_aspath_filter(n, match): 62 | pass 63 | 64 | def gen_community_filter(n, match): 65 | pass 66 | 67 | def gen_ext_community_filter(n, match): 68 | pass 69 | 70 | with open('{0}/{1}'.format(self.host_dir, self.CONFIG_FILE_NAME), 'w') as f: 71 | f.write(config) 72 | 73 | if 'policy' in self.scenario_global_conf: 74 | for k, v in self.scenario_global_conf['policy'].items(): 75 | match_info = [] 76 | for i, match in enumerate(v['match']): 77 | n = '{0}_match_{1}'.format(k, i) 78 | if match['type'] == 'prefix': 79 | f.write(gen_prefix_filter(n, match)) 80 | elif match['type'] == 'as-path': 81 | f.write(gen_aspath_filter(n, match)) 82 | elif match['type'] == 'community': 83 | f.write(gen_community_filter(n, match)) 84 | elif match['type'] == 'ext-community': 85 | f.write(gen_ext_community_filter(n, match)) 86 | match_info.append((match['type'], n)) 87 | f.write(gen_filter(k, match_info)) 88 | 89 | for n in sorted(list(flatten(list(t.get('neighbors', {}).values()) for t in self.scenario_global_conf['testers'])) + [self.scenario_global_conf['monitor']], key=lambda n: n['as']): 90 | f.write(gen_neighbor_config(n)) 91 | f.write('allow to any\n') 92 | 93 | if 'filter_test' in self.conf: 94 | f.write(self.get_filter_test_config()) 95 | if self.conf['filter_test'] == 'ixp': 96 | f.write("deny quick from any inet prefixlen > 24\n") 97 | f.write('deny quick from any transit-as {174,701,1299,2914,3257,3320,3356,3491,4134,5511,6453,6461,6762,6830,7018}\n') 98 | else: 99 | f.write('allow from any\n') 100 | 101 | f.flush() 102 | 103 | def get_startup_cmd(self): 104 | return '\n'.join( 105 | ['#!/bin/bash', 106 | 'ulimit -n 65536', 107 | '/usr/local/sbin/bgpd -f {guest_dir}/{config_file_name} -d > {guest_dir}/openbgp.log 2>&1'] 108 | ).format( 109 | guest_dir=self.guest_dir, 110 | config_file_name=self.CONFIG_FILE_NAME, 111 | debug_level='info') 112 | 113 | def get_version_cmd(self): 114 | return "/usr/local/sbin/bgpctl -V" 115 | 116 | def exec_version_cmd(self): 117 | version = self.get_version_cmd() 118 | i= dckr.exec_create(container=self.name, cmd=version, stderr=True) 119 | return dckr.exec_start(i['Id'], stream=False, detach=False).decode('utf-8').strip('\n') 120 | 121 | def get_neighbors_state(self): 122 | neighbors_accepted = {} 123 | neighbors_received_full = {} 124 | neighbor_received_output = json.loads(self.local("/usr/local/sbin/bgpctl -j show neighbor").decode('utf-8')) 125 | for neigh in neighbor_received_output['neighbors']: 126 | neighbors_accepted[neigh['remote_addr']] = neigh['stats']['prefixes']['received'] 127 | neighbors_received_full[neigh['remote_addr']] = False if neigh['stats']['update']['received']['eor'] == 0 else True 128 | 129 | 130 | return neighbors_received_full, neighbors_accepted 131 | 132 | 133 | def get_filter_test_config(self): 134 | file = open("filters/openbgp.conf", mode='r') 135 | filters = file.read() 136 | file.close 137 | return filters -------------------------------------------------------------------------------- /frr_compiled.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | from base import * 4 | from frr import FRRoutingTarget 5 | 6 | 7 | class FRRoutingCompiled(Container): 8 | CONTAINER_NAME = None 9 | GUEST_DIR = '/root/config' 10 | 11 | def __init__(self, host_dir, conf, image='bgperf/frr_c'): 12 | super(FRRoutingCompiled, self).__init__(self.CONTAINER_NAME, image, host_dir, self.GUEST_DIR, conf) 13 | 14 | @classmethod 15 | def build_image(cls, force=False, tag='bgperf/frr_c', checkout='stable/8.0', nocache=False): 16 | # copied from https://github.com/FRRouting/frr/blob/master/docker/ubuntu-ci/Dockerfile 17 | # but you have to remove any lines that include # comments 18 | cls.dockerfile = ''' 19 | ARG UBUNTU_VERSION=22.04 20 | FROM ubuntu:$UBUNTU_VERSION 21 | 22 | ARG DEBIAN_FRONTEND=noninteractive 23 | ENV APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn 24 | 25 | # Update and install build requirements. 26 | 27 | RUN apt update && apt upgrade -y && \ 28 | apt-get install -y \ 29 | autoconf \ 30 | automake \ 31 | bison \ 32 | build-essential \ 33 | flex \ 34 | git \ 35 | install-info \ 36 | libc-ares-dev \ 37 | libcap-dev \ 38 | libelf-dev \ 39 | libjson-c-dev \ 40 | libpam0g-dev \ 41 | libreadline-dev \ 42 | libsnmp-dev \ 43 | libsqlite3-dev \ 44 | lsb-release \ 45 | libtool \ 46 | lcov \ 47 | make \ 48 | perl \ 49 | pkg-config \ 50 | python3-dev \ 51 | python3-sphinx \ 52 | screen \ 53 | texinfo \ 54 | tmux \ 55 | iptables \ 56 | && \ 57 | apt-get install -y \ 58 | libprotobuf-c-dev \ 59 | protobuf-c-compiler \ 60 | && \ 61 | apt-get install -y \ 62 | cmake \ 63 | libpcre2-dev \ 64 | && \ 65 | apt-get install -y \ 66 | libgrpc-dev \ 67 | libgrpc++-dev \ 68 | protobuf-compiler-grpc \ 69 | && \ 70 | apt-get install -y \ 71 | curl \ 72 | gdb \ 73 | kmod \ 74 | iproute2 \ 75 | iputils-ping \ 76 | liblua5.3-dev \ 77 | libssl-dev \ 78 | lua5.3 \ 79 | net-tools \ 80 | python3 \ 81 | python3-pip \ 82 | snmp \ 83 | snmp-mibs-downloader \ 84 | snmpd \ 85 | ssmping \ 86 | sudo \ 87 | time \ 88 | tshark \ 89 | valgrind \ 90 | yodl \ 91 | && \ 92 | download-mibs && \ 93 | wget --tries=5 --waitretry=10 --retry-connrefused https://raw.githubusercontent.com/FRRouting/frr-mibs/main/iana/IANA-IPPM-METRICS-REGISTRY-MIB -O /usr/share/snmp/mibs/iana/IANA-IPPM-METRICS-REGISTRY-MIB && \ 94 | wget --tries=5 --waitretry=10 --retry-connrefused https://raw.githubusercontent.com/FRRouting/frr-mibs/main/ietf/SNMPv2-PDU -O /usr/share/snmp/mibs/ietf/SNMPv2-PDU && \ 95 | wget --tries=5 --waitretry=10 --retry-connrefused https://raw.githubusercontent.com/FRRouting/frr-mibs/main/ietf/IPATM-IPMC-MIB -O /usr/share/snmp/mibs/ietf/IPATM-IPMC-MIB && \ 96 | rm -f /usr/lib/python3.*/EXTERNALLY-MANAGED && \ 97 | python3 -m pip install wheel && \ 98 | bash -c "PV=($(pkg-config --modversion protobuf | tr '.' ' ')); if (( PV[0] == 3 && PV[1] < 19 )); then python3 -m pip install 'protobuf<4' grpcio grpcio-tools; else python3 -m pip install 'protobuf>=4' grpcio grpcio-tools; fi" && \ 99 | python3 -m pip install 'pytest>=6.2.4' 'pytest-xdist>=2.3.0' && \ 100 | python3 -m pip install 'scapy>=2.4.5' && \ 101 | python3 -m pip install xmltodict && \ 102 | python3 -m pip install git+https://github.com/Exa-Networks/exabgp@0659057837cd6c6351579e9f0fa47e9fb7de7311 103 | 104 | 105 | ARG UID=1010 106 | RUN groupadd -r -g 92 frr && \ 107 | groupadd -r -g 85 frrvty && \ 108 | adduser --system --ingroup frr --home /home/frr \ 109 | --gecos "FRR suite" -u $UID --shell /bin/bash frr && \ 110 | usermod -a -G frrvty frr && \ 111 | useradd -d /var/run/exabgp/ -s /bin/false exabgp && \ 112 | echo 'frr ALL = NOPASSWD: ALL' | tee /etc/sudoers.d/frr && \ 113 | mkdir -p /home/frr && chown frr.frr /home/frr 114 | 115 | # Install FRR built packages 116 | RUN mkdir -p /etc/apt/keyrings && \ 117 | curl -s -o /etc/apt/keyrings/frrouting.gpg https://deb.frrouting.org/frr/keys.gpg && \ 118 | echo deb '[signed-by=/etc/apt/keyrings/frrouting.gpg]' https://deb.frrouting.org/frr \ 119 | $(lsb_release -s -c) "frr-stable" > /etc/apt/sources.list.d/frr.list && \ 120 | apt-get update && apt-get install -y librtr-dev libyang2-dev libyang2-tools 121 | 122 | 123 | #USER frr:frr 124 | RUN cd ~/ && git clone https://github.com/FRRouting/frr.git 125 | 126 | 127 | 128 | #COPY --chown=frr:frr ./ /home/frr/frr/ 129 | 130 | RUN cd ~/frr && \ 131 | ./bootstrap.sh && \ 132 | ./configure \ 133 | --prefix=/usr \ 134 | --sysconfdir=/etc \ 135 | --localstatedir=/var \ 136 | --sbindir=/usr/lib/frr \ 137 | --enable-gcov \ 138 | --enable-rpki \ 139 | --enable-multipath=256 \ 140 | --enable-user=frr \ 141 | --enable-group=frr \ 142 | --enable-vty-group=frrvty \ 143 | --enable-snmp=agentx \ 144 | --enable-scripting \ 145 | --enable-configfile-mask=0640 \ 146 | --enable-logfile-mask=0640 \ 147 | --with-pkg-extra-version=-my-manual-build && \ 148 | make -j $(nproc) && \ 149 | sudo make install 150 | 151 | RUN cd ~/frr && make check || true 152 | 153 | RUN sudo cp ~/frr/docker/ubuntu-ci/docker-start /usr/sbin/docker-start && rm -rf ~/frr 154 | 155 | CMD ["/usr/sbin/docker-start"] 156 | 157 | RUN sudo install -m 755 -o frr -g frr -d /var/log/frr && \ 158 | sudo install -m 755 -o frr -g frr -d /var/opt/frr && \ 159 | sudo install -m 775 -o frr -g frrvty -d /etc/frr && \ 160 | sudo install -m 640 -o frr -g frr /dev/null /etc/frr/zebra.conf && \ 161 | sudo install -m 640 -o frr -g frr /dev/null /etc/frr/bgpd.conf && \ 162 | sudo install -m 640 -o frr -g frrvty /dev/null /etc/frr/vtysh.conf && \ 163 | sudo install -m 755 -o frr -g frr -d /var/lib/frr && \ 164 | sudo install -m 755 -o frr -g frr -d /var/etc/frr && \ 165 | sudo install -m 755 -o frr -g frr -d /var/run/frr 166 | 167 | 168 | #RUN sudo mkdir /etc/frr /var/lib/frr /var/run/frr /frr 169 | # sudo chown frr:frr /etc/frr /var/lib/frr /var/run/frr 170 | # sudo mkdir -p /root/config && sudo chown frr:frr /root/config 171 | 172 | '''.format(checkout) 173 | print("FRRoutingCompiled") 174 | super(FRRoutingCompiled, cls).build_image(force, tag, nocache) 175 | 176 | 177 | class FRRoutingCompiledTarget(FRRoutingCompiled, FRRoutingTarget): 178 | 179 | CONTAINER_NAME = 'bgperf_frrouting_compiled_target' 180 | 181 | def __init__(self, host_dir, conf, image='bgperf/frr_c'): 182 | super(FRRoutingTarget, self).__init__(host_dir, conf, image='bgperf/frr_c') 183 | -------------------------------------------------------------------------------- /gobgp.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2016 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 base import * 17 | import yaml 18 | import json 19 | 20 | class GoBGP(Container): 21 | 22 | CONTAINER_NAME = None 23 | GUEST_DIR = '/root/config' 24 | 25 | def __init__(self, host_dir, conf, image='bgperf/gobgp'): 26 | super(GoBGP, self).__init__(self.CONTAINER_NAME, image, host_dir, self.GUEST_DIR, conf) 27 | 28 | @classmethod 29 | def build_image(cls, force=False, tag='bgperf/gobgp', checkout='HEAD', nocache=False): 30 | cls.dockerfile = ''' 31 | FROM golang:latest 32 | WORKDIR /root 33 | RUN git clone https://github.com/osrg/gobgp.git && cd gobgp && go mod download 34 | RUN cd gobgp && go install ./cmd/gobgpd 35 | RUN cd gobgp && go install ./cmd/gobgp 36 | RUN rm -rf /root/gobgp && cp /go/bin/gobgp /root/gobgp 37 | '''.format(checkout) 38 | super(GoBGP, cls).build_image(force, tag, nocache) 39 | 40 | 41 | class GoBGPTarget(GoBGP, Target): 42 | 43 | CONTAINER_NAME = 'bgperf_gobgp_target' 44 | CONFIG_FILE_NAME = 'gobgpd.conf' 45 | DYNAMIC_NEIGHBORS = True 46 | 47 | def write_config(self): 48 | 49 | config = {} 50 | config['global'] = { 51 | 'config': { 52 | 'as': self.conf['as'], 53 | 'router-id': self.conf['router-id'] 54 | }, 55 | } 56 | if 'policy' in self.scenario_global_conf: 57 | config['policy-definitions'] = [] 58 | config['defined-sets'] = { 59 | 'prefix-sets': [], 60 | 'bgp-defined-sets': { 61 | 'as-path-sets': [], 62 | 'community-sets': [], 63 | 'ext-community-sets': [], 64 | }, 65 | } 66 | for k, v in self.scenario_global_conf['policy'].items(): 67 | conditions = { 68 | 'bgp-conditions': {}, 69 | } 70 | for i, match in enumerate(v['match']): 71 | n = '{0}_match_{1}'.format(k, i) 72 | if match['type'] == 'prefix': 73 | config['defined-sets']['prefix-sets'].append({ 74 | 'prefix-set-name': n, 75 | 'prefix-list': [{'ip-prefix': p} for p in match['value']] 76 | }) 77 | conditions['match-prefix-set'] = {'prefix-set': n} 78 | elif match['type'] == 'as-path': 79 | config['defined-sets']['bgp-defined-sets']['as-path-sets'].append({ 80 | 'as-path-set-name': n, 81 | 'as-path-list': match['value'], 82 | }) 83 | conditions['bgp-conditions']['match-as-path-set'] = {'as-path-set': n} 84 | elif match['type'] == 'community': 85 | config['defined-sets']['bgp-defined-sets']['community-sets'].append({ 86 | 'community-set-name': n, 87 | 'community-list': match['value'], 88 | }) 89 | conditions['bgp-conditions']['match-community-set'] = {'community-set': n} 90 | elif match['type'] == 'ext-community': 91 | config['defined-sets']['bgp-defined-sets']['ext-community-sets'].append({ 92 | 'ext-community-set-name': n, 93 | 'ext-community-list': match['value'], 94 | }) 95 | conditions['bgp-conditions']['match-ext-community-set'] = {'ext-community-set': n} 96 | 97 | config['policy-definitions'].append({ 98 | 'name': k, 99 | 'statements': [{'name': k, 'conditions': conditions, 'actions': {'route-disposition': True}}], 100 | }) 101 | 102 | 103 | def gen_neighbor_config(n): 104 | c = {'config': {'neighbor-address': n['local-address'], 'peer-as': n['as']}, 105 | 'transport': {'config': {'local-address': self.conf['local-address']}}, 106 | #'route-server': {'config': {'route-server-client': True}} 107 | } 108 | if 'filter' in n: 109 | a = {} 110 | if 'in' in n['filter']: 111 | a['import-policy-list'] = n['filter']['in'] 112 | a['default-import-policy'] = 'accept-route' 113 | if 'out' in n['filter']: 114 | a['export-policy-list'] = n['filter']['out'] 115 | a['default-export-policy'] = 'accept-route' 116 | c['apply-policy'] = {'config': a} 117 | return c 118 | 119 | if self.DYNAMIC_NEIGHBORS: 120 | config['peer-groups'] = [{'config': {'peer-group-name': 'everything'}, 'timers': {'config': {'hold-time': 90}}}] 121 | config['dynamic-neighbors'] = [{'config': {'prefix': '10.0.0.0/8', 'peer-group': 'everything'}}] 122 | else: 123 | config['neighbors'] = [gen_neighbor_config(n) for n in list(flatten(list(t.get('neighbors', {}).values()) for t in self.scenario_global_conf['testers'])) + [self.scenario_global_conf['monitor']]] 124 | with open('{0}/{1}'.format(self.host_dir, self.CONFIG_FILE_NAME), 'w') as f: 125 | f.write(yaml.dump(config, default_flow_style=False)) 126 | return config 127 | 128 | def get_startup_cmd(self): 129 | return '\n'.join( 130 | ['#!/bin/bash', 131 | 'ulimit -n 65536', 132 | 'gobgpd -t yaml -f {guest_dir}/{config_file_name} -l {debug_level} > {guest_dir}/gobgpd.log 2>&1'] 133 | ).format( 134 | guest_dir=self.guest_dir, 135 | config_file_name=self.CONFIG_FILE_NAME, 136 | debug_level='info') 137 | 138 | def get_version_cmd(self): 139 | return "gobgpd --version" 140 | 141 | def exec_version_cmd(self): 142 | ret = super().exec_version_cmd() 143 | return ret.split(' ')[2].strip('\n') 144 | 145 | 146 | def get_neighbors_state(self): 147 | neighbors_accepted = {} 148 | neighbors_received = {} 149 | neighbor_received_output = self.local("/root/gobgp neighbor -j") 150 | if neighbor_received_output: 151 | neighbor_received_output = json.loads(neighbor_received_output.decode('utf-8')) 152 | 153 | for neighbor in neighbor_received_output: 154 | if 'afi_safis' in neighbor and 'accepted' in neighbor['afi_safis'][0]['state']: 155 | neighbors_accepted[neighbor['state']['neighbor_address']] = neighbor['afi_safis'][0]['state']['accepted'] 156 | else: 157 | neighbors_accepted[neighbor['state']['neighbor_address']] = 0 158 | 159 | if 'afi_safis' in neighbor and 'received' in neighbor['afi_safis'][0]['state']: 160 | neighbors_received[neighbor['state']['neighbor_address']] = neighbor['afi_safis'][0]['state']['received'] 161 | else: 162 | neighbors_received[neighbor['state']['neighbor_address']] = 0 163 | 164 | return neighbors_received, neighbors_accepted 165 | 166 | ## Caveats 167 | # I don't think accepting policy is configured correctly. it 168 | # doesn't seem to be applied to the neighobr -------------------------------------------------------------------------------- /mrt_tester.py: -------------------------------------------------------------------------------- 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 tester import Tester 17 | from gobgp import GoBGP 18 | from exabgp import ExaBGP_MRTParse 19 | import os 20 | import yaml 21 | from subprocess import check_output, Popen, PIPE 22 | from settings import dckr 23 | 24 | from base import * 25 | 26 | 27 | class MRTTester(Container): 28 | 29 | # def get_mrt_file(self, conf, name): 30 | # # conf: tester or neighbor configuration 31 | # if 'mrt-file' in conf: 32 | # mrt_file_path = os.path.expanduser(conf['mrt-file']) 33 | 34 | # guest_mrt_file_path = '{guest_dir}/{filename}'.format( 35 | # guest_dir=self.guest_dir, 36 | # filename=name + '.mrt' 37 | # ) 38 | # host_mrt_file_path = '{host_dir}/{filename}'.format( 39 | # host_dir=self.host_dir, 40 | # filename=name + '.mrt' 41 | # ) 42 | # if not os.path.isfile(host_mrt_file_path): 43 | # shutil.copyfile(mrt_file_path, host_mrt_file_path) 44 | # return guest_mrt_file_path 45 | 46 | def get_mrt_file(self, conf): 47 | return conf['mrt-file'] 48 | 49 | def get_host_config(self): 50 | neighbor = next(iter(self.conf['neighbors'].values())) 51 | #create an mrt_file on guest_dir so that it can be mounted 52 | host_config = dckr.create_host_config( 53 | binds=['{0}:{1}'.format(os.path.abspath(self.host_dir), self.guest_dir), 54 | '{0}:/root/mrt_file'.format(self.get_mrt_file(neighbor))], 55 | privileged=True, 56 | network_mode='bridge', 57 | cap_add=['NET_ADMIN'] 58 | ) 59 | return host_config 60 | 61 | 62 | class ExaBGPMrtTester(Tester, ExaBGP_MRTParse, MRTTester): 63 | 64 | CONTAINER_NAME_PREFIX = 'bgperf_exabgp_mrttester_' 65 | 66 | def __init__(self, name, host_dir, conf, image='bgperf/exabgp_mrtparse'): 67 | super(ExaBGPMrtTester, self).__init__(name, host_dir, conf, image) 68 | 69 | def configure_neighbors(self, target_conf): 70 | tester_mrt_guest_file_path = self.get_mrt_file(self.conf, self.name) 71 | 72 | neighbors = list(self.conf.get('neighbors', {}).values()) 73 | 74 | for neighbor in neighbors: 75 | config = '''neighbor {0} {{ 76 | peer-as {1}; 77 | router-id {2}; 78 | local-address {3}; 79 | local-as {4}; 80 | api {{ 81 | processes [ inject_mrt ]; 82 | }} 83 | }}'''.format(target_conf['local-address'], target_conf['as'], 84 | neighbor['router-id'], neighbor['local-address'], 85 | neighbor['as']) 86 | 87 | mrt_guest_file_path = self.get_mrt_file(neighbor, 88 | neighbor['router-id']) 89 | if not mrt_guest_file_path: 90 | mrt_guest_file_path = tester_mrt_guest_file_path 91 | 92 | cmd = ['/usr/bin/python3', '/root/mrtparse/examples/mrt2exabgp.py'] 93 | cmd += ['-r {router_id}', 94 | '-l {local_as}', 95 | '-p {peer_as}', 96 | '-L {local_addr}', 97 | '-n {peer_addr}', 98 | '-G', 99 | '{mrt_file_path}'] 100 | 101 | config += '\n' 102 | config += 'process inject_mrt {\n' 103 | config += ' run {cmd};\n'.format( 104 | cmd=' '.join(cmd).format( 105 | router_id = neighbor['router-id'], 106 | local_as = neighbor['as'], 107 | peer_as = target_conf['as'], 108 | local_addr = neighbor['local-address'], 109 | peer_addr = target_conf['local-address'], 110 | mrt_file_path = mrt_guest_file_path 111 | ) 112 | ) 113 | config += ' encoder text;\n' 114 | config += '}\n' 115 | 116 | with open('{0}/{1}.conf'.format(self.host_dir, neighbor['router-id']), 'w') as f: 117 | f.write(config) 118 | 119 | def get_startup_cmd(self): 120 | peers = list(self.conf.get('neighbors', {}).values()) 121 | 122 | startup = ['#!/bin/bash', 123 | 'ulimit -n 65536'] 124 | 125 | cmd = ['env', 126 | 'exabgp.daemon.daemonize=true', 127 | 'exabgp.daemon.user=root'] 128 | 129 | # Higher performances: 130 | # exabgp -d config1 config2 131 | # https://github.com/Exa-Networks/exabgp/wiki/High-Performance 132 | # WARNING: can not log to files when running multiple configuration 133 | if self.conf.get('high-perf', False) is True: 134 | cmd += ['/exabgp/sbin/exabgp -d {} >/dev/null 2>&1 &'.format( 135 | ' '.join([ 136 | '{}/{}.conf'.format(self.guest_dir, p['router-id']) for p in peers 137 | ]) 138 | )] 139 | startup += [' '.join(cmd)] 140 | else: 141 | for p in peers: 142 | startup += [' '.join( 143 | cmd + [ 144 | 'exabgp.log.destination={0}/{1}.log'.format( 145 | self.guest_dir, p['router-id']), 146 | 'exabgp {}/{}.conf'.format( 147 | self.guest_dir, p['router-id']), 148 | '> {}/exabgp.log 2>&1'.format(self.guest_dir), 149 | '&' 150 | ]) 151 | ] 152 | 153 | return '\n'.join(startup) 154 | 155 | 156 | class GoBGPMRTTester(Tester, GoBGP, MRTTester): 157 | 158 | CONTAINER_NAME_PREFIX = 'bgperf_gobgp_mrttester_' 159 | 160 | def __init__(self, name, host_dir, conf, image='bgperf/gobgp'): 161 | super(GoBGPMRTTester, self).__init__(name, host_dir, conf, image) 162 | 163 | def configure_neighbors(self, target_conf): 164 | conf = list(self.conf.get('neighbors', {}).values())[0] 165 | 166 | config = { 167 | 'global': { 168 | 'config': { 169 | 'as': conf['as'], 170 | 'router-id': conf['router-id'], 171 | } 172 | }, 173 | 'neighbors': [ 174 | { 175 | 'config': { 176 | 'neighbor-address': target_conf['local-address'], 177 | 'peer-as': target_conf['as'] 178 | } 179 | } 180 | ] 181 | } 182 | 183 | with open('{0}/{1}.conf'.format(self.host_dir, self.name), 'w') as f: 184 | f.write(yaml.dump(config, default_flow_style=False)) 185 | self.config_name = '{0}.conf'.format(self.name) 186 | 187 | def get_startup_cmd(self): 188 | conf = list(self.conf.get('neighbors', {}).values())[0] 189 | 190 | mrtfile = '/root/mrt_file' 191 | if not mrtfile: 192 | mrtfile = self.get_mrt_file(self.conf, self.name) 193 | 194 | startup = '''#!/bin/bash 195 | ulimit -n 65536 196 | gobgpd -t yaml -f {1}/{2} -l {3} > {1}/gobgpd.log 2>&1 & 197 | '''.format(conf['local-address'], self.guest_dir, self.config_name, 'info') 198 | startup += 'sleep 1\n' # seems to need a wait betwee gobgpd starting and the client pushing the mrt file 199 | cmd = ['gobgp', 'mrt'] 200 | if conf.get('only-best', False): 201 | cmd.append('--only-best') 202 | cmd += ['inject', 'global', f"--nexthop {conf['local-address']}", "--no-ipv6", mrtfile] 203 | if 'count' in conf: 204 | cmd.append(str(conf['count'])) 205 | if 'skip' in conf: 206 | cmd.append(str(conf['skip'])) 207 | cmd += [f"> {self.guest_dir}/mrt.log 2>&1", '&'] 208 | 209 | startup += '\n' + ' '.join(cmd) 210 | 211 | #startup += '\n' + 'pkill -SIGHUP gobgpd' 212 | return startup 213 | 214 | def find_errors(): 215 | grep1 = Popen(('grep -i expired /tmp/bgperf2/mrt-injector*/*.log'), shell=True, stdout=PIPE) 216 | errors = check_output(('wc', '-l'), stdin=grep1.stdout) 217 | grep1.wait() 218 | return errors.decode('utf-8').strip() 219 | -------------------------------------------------------------------------------- /frr.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2017 Network Device Education Foundation, Inc. ("NetDEF") 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 base import * 17 | import json 18 | import re 19 | 20 | class FRRouting(Container): 21 | CONTAINER_NAME = None 22 | GUEST_DIR = '/root/config' 23 | 24 | def __init__(self, host_dir, conf, image='bgperf/frr'): 25 | super(FRRouting, self).__init__(self.CONTAINER_NAME, image, host_dir, self.GUEST_DIR, conf) 26 | 27 | @classmethod 28 | def build_image(cls, force=False, tag='bgperf/frr', checkout='HEAD', nocache=False): 29 | cls.dockerfile = ''' 30 | FROM frrouting/frr:v7.5.1 31 | '''.format(checkout) 32 | super(FRRouting, cls).build_image(force, tag, nocache) 33 | 34 | 35 | class FRRoutingTarget(FRRouting, Target): 36 | 37 | CONTAINER_NAME = 'bgperf_frrouting_target' 38 | CONFIG_FILE_NAME = 'bgpd.conf' 39 | 40 | def write_config(self): 41 | 42 | config = """hostname bgpd 43 | password zebra 44 | router bgp {0} 45 | bgp router-id {1} 46 | no bgp ebgp-requires-policy 47 | """.format(self.conf['as'], self.conf['router-id']) 48 | 49 | def gen_neighbor_config(n): 50 | local_addr = n['local-address'] 51 | c = """ neighbor {0} remote-as {1} 52 | neighbor {0} advertisement-interval 1 53 | neighbor {0} disable-connected-check 54 | neighbor {0} timers 30 90 55 | """.format(local_addr, n['as']) # adjust BGP hold-timers if desired 56 | if 'filter' in n: 57 | for p in (n['filter']['in'] if 'in' in n['filter'] else []): 58 | c += ' neighbor {0} route-map {1} export\n'.format(local_addr, p) 59 | return c 60 | 61 | def gen_address_family_neighbor(n): 62 | local_addr = n['local-address'] 63 | c = " neighbor {0} activate\n".format(local_addr) 64 | c +=" neighbor {0} soft-reconfiguration inbound\n".format(local_addr) 65 | if 'filter_test' in self.conf: 66 | c +=" neighbor {0} route-map {1} in\n".format(local_addr, self.conf['filter_test']) 67 | return c 68 | 69 | neighbors = list(flatten(list(t.get('neighbors', {}).values()) for t in self.scenario_global_conf['testers'])) + [self.scenario_global_conf['monitor']] 70 | 71 | with open('{0}/{1}'.format(self.host_dir, self.CONFIG_FILE_NAME), 'w') as f: 72 | f.write(config) 73 | 74 | for n in neighbors: 75 | f.write(gen_neighbor_config(n)) 76 | 77 | f.write(" address-family ipv4 unicast\n") 78 | for n in neighbors: 79 | f.write(gen_address_family_neighbor(n)) 80 | f.write(" exit-address-family\n") 81 | 82 | if 'policy' in self.scenario_global_conf: 83 | seq = 10 84 | for k, v in self.scenario_global_conf['policy'].items(): 85 | match_info = [] 86 | for i, match in enumerate(v['match']): 87 | n = '{0}_match_{1}'.format(k, i) 88 | if match['type'] == 'prefix': 89 | f.write(''.join('ip prefix-list {0} deny {1}\n'.format(n, p) for p in match['value'])) 90 | f.write('ip prefix-list {0} permit any\n'.format(n)) 91 | elif match['type'] == 'as-path': 92 | f.write(''.join('bgp as-path access-list {0} deny _{1}_\n'.format(n, p) for p in match['value'])) 93 | f.write('bgp as-path access-list {0} permit .*\n'.format(n)) 94 | elif match['type'] == 'community': 95 | f.write(''.join('bgp community-list standard {0} permit {1}\n'.format(n, p) for p in match['value'])) 96 | f.write('bgp community-list standard {0} permit\n'.format(n)) 97 | elif match['type'] == 'ext-community': 98 | f.write(''.join('bgp extcommunity-list standard {0} permit {1} {2}\n'.format(n, *p.split(':', 1)) for p in match['value'])) 99 | f.write('bgp extcommunity-list standard {0} permit\n'.format(n)) 100 | 101 | match_info.append((match['type'], n)) 102 | 103 | f.write('route-map {0} permit {1}\n'.format(k, seq)) 104 | for info in match_info: 105 | if info[0] == 'prefix': 106 | f.write('match ip address prefix-list {0}\n'.format(info[1])) 107 | elif info[0] == 'as-path': 108 | f.write('match as-path {0}\n'.format(info[1])) 109 | elif info[0] == 'community': 110 | f.write('match community {0}\n'.format(info[1])) 111 | elif info[0] == 'ext-community': 112 | f.write('match extcommunity {0}\n'.format(info[1])) 113 | 114 | seq += 10 115 | 116 | if 'filter_test' in self.conf: 117 | f.write(self.get_filter_test_config()) 118 | 119 | # we need log level to debug so that we can find End-of-RIB 120 | f.write("log stdout debug\n") 121 | 122 | def get_filter_test_config(self): 123 | file = open("filters/frr.conf", mode='r') 124 | filters = file.read() 125 | file.close 126 | return filters 127 | 128 | def get_startup_cmd(self): 129 | return '\n'.join( 130 | ['#!/bin/bash', 131 | 'ulimit -n 65536', 132 | 'mv /etc/frr /etc/frr.old', 133 | 'mkdir /etc/frr', 134 | 'cp {guest_dir}/{config_file_name} /etc/frr/{config_file_name} && chown frr:frr /etc/frr/{config_file_name}', 135 | '/usr/lib/frr/bgpd -u frr -f /etc/frr/{config_file_name} -Z > {guest_dir}/bgpd.log 2>&1 &', 136 | #'cd /root/config', 137 | #'perf record -F 99 -p 17 -g -- sleep 1300 > perf.out', 138 | #'perf script > /root/config/out.perf', 139 | ] 140 | ).format( 141 | guest_dir=self.guest_dir, 142 | config_file_name=self.CONFIG_FILE_NAME) 143 | 144 | def get_version_cmd(self): 145 | return ['vtysh', '-c', 'show version', '|', 'head -1'] 146 | 147 | def exec_version_cmd(self): 148 | ret = super().exec_version_cmd() 149 | return ret.split('\n')[0] 150 | 151 | def get_neighbors_state(self): 152 | neighbors_accepted = {} 153 | neighbors_received = {} 154 | neighbor_received_output = self.local("vtysh -c 'sh ip bgp summary json'") 155 | if neighbor_received_output: 156 | neighbor_received_output = json.loads(neighbor_received_output.decode('utf-8')) 157 | 158 | for n in neighbor_received_output['ipv4Unicast']['peers'].keys(): 159 | rcd = neighbor_received_output['ipv4Unicast']['peers'][n]['pfxRcd'] 160 | neighbors_accepted[n] = rcd 161 | return neighbors_received, neighbors_accepted 162 | 163 | def _get_EOR_from_log(self, neighbors): 164 | # we are looking at the log files for End-Of-RIB 165 | # 2021/11/05 16:34:38 BGP: bgp_update_receive: rcvd End-of-RIB for IPv4 Unicast from 10.10.0.3 in vrf default 166 | 167 | with open(f"{self.host_dir}/bgpd.log") as f: 168 | log = f.readlines() 169 | EOR = re.compile(r".*rcvd End-of-RIB for IPv4 Unicast from (\d+\.\d+\.\d+\.\d+)") 170 | if len(log) > 1: 171 | for line in log: 172 | m_eor = EOR.match(line) 173 | if m_eor: 174 | neighbors[m_eor.groups()[0]] = True 175 | 176 | return neighbors 177 | 178 | def get_neighbor_received_routes(self): 179 | # FRR doesn't have a counter to look at to see if all the prefixes have been sent 180 | # instead we have to look at the log file and see if End-of-RIB has been sent for the neighbor 181 | neighbors_received_full, neighbors_checked = super(FRRoutingTarget, self).get_neighbor_received_routes() 182 | 183 | assert(all(value == False for value in neighbors_received_full.values())) 184 | neighbors_received_full = self._get_EOR_from_log(neighbors_received_full) 185 | 186 | assert(len(neighbors_received_full) == len(neighbors_checked)) 187 | 188 | return neighbors_received_full, neighbors_checked 189 | 190 | -------------------------------------------------------------------------------- /bird.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2016 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 base import * 17 | import textfsm 18 | 19 | class BIRD(Container): 20 | 21 | CONTAINER_NAME = None 22 | GUEST_DIR = '/root/config' 23 | 24 | def __init__(self, host_dir, conf, image='bgperf/bird', name=None): 25 | super(BIRD, self).__init__(name if name is not None else self.CONTAINER_NAME, image, host_dir, self.GUEST_DIR, conf) 26 | 27 | @classmethod 28 | def build_image(cls, force=False, tag='bgperf/bird', checkout='HEAD', branch='master', nocache=False): 29 | cls.dockerfile = ''' 30 | FROM ubuntu:latest 31 | WORKDIR /root 32 | RUN apt-get update && apt-get install -qy git autoconf libtool gawk make \ 33 | flex bison libncurses-dev libreadline6-dev iproute2 34 | RUN apt-get install -qy flex 35 | RUN git config --global http.sslverify false && git clone https://gitlab.nic.cz/labs/bird.git -b {0} bird 36 | RUN cd bird && git checkout {0} && autoreconf -i && ./configure && make && make install 37 | '''.format(branch) 38 | super(BIRD, cls).build_image(force, tag, nocache) 39 | 40 | 41 | class BIRDTarget(BIRD, Target): 42 | 43 | CONTAINER_NAME = 'bgperf_bird_target' 44 | CONFIG_FILE_NAME = 'bird.conf' 45 | DYNAMIC_NEIGHBORS = True 46 | 47 | def write_config(self): 48 | config = '''router id {0}; 49 | protocol device {{ }} 50 | protocol direct {{ disabled; }} 51 | protocol kernel {{ ipv4 {{ import none; export none; }}; }} 52 | 53 | log stderr all; 54 | #debug protocols all; # this seems to add a lot of extra load especially in internet/mrt tests 55 | '''.format(self.conf['router-id'], ' sorted' if self.conf['single-table'] else '') 56 | 57 | def gen_filter_assignment(n): 58 | if 'filter' in n: 59 | c = [] 60 | if 'in' not in n['filter'] or len(n['filter']['in']) == 0: 61 | c.append('import all;') 62 | else: 63 | c.append('import where {0};'.format( '&&'.join(x + '()' for x in n['filter']['in']))) 64 | 65 | if 'out' not in n['filter'] or len(n['filter']['out']) == 0: 66 | c.append('export all;') 67 | else: 68 | c.append('export where {0};'.format( '&&'.join(x + '()' for x in n['filter']['out']))) 69 | 70 | return '\n'.join(c) 71 | return '''import all; 72 | export all; 73 | ''' 74 | 75 | def gen_neighbor_config(n): 76 | filter = 'all' 77 | if 'filter_test' in self.conf: 78 | filter = f"filter {self.conf['filter_test']}" 79 | return ('''ipv4 table table_{0}; 80 | protocol pipe pipe_{0} {{ 81 | table master4; 82 | peer table table_{0}; 83 | }} 84 | '''.format(n['as']) if not self.conf['single-table'] else '') + '''protocol bgp bgp_{0} {{ 85 | local as {1}; 86 | neighbor {2} as {0}; 87 | 88 | ipv4 {{ import {}; export all; }}; 89 | rs client; 90 | }} 91 | '''.format(n['as'], self.conf['as'], n['local-address'], 'secondary' if self.conf['single-table'] else '', filter) 92 | 93 | 94 | def gen_prefix_filter(name, match): 95 | return '''function {0}() 96 | prefix set prefixes; 97 | {{ 98 | prefixes = [ 99 | {1} 100 | ]; 101 | if net ~ prefixes then return false; 102 | return true; 103 | }} 104 | '''.format(name, ',\n'.join(match['value'])) 105 | 106 | def gen_aspath_filter(name, match): 107 | c = '''function {0}() 108 | {{ 109 | '''.format(name) 110 | c += '\n'.join('if (bgp_path ~ [= * {0} * =]) then return false;'.format(v) for v in match['value']) 111 | c += ''' 112 | return true; 113 | } 114 | ''' 115 | return c 116 | 117 | def gen_community_filter(name, match): 118 | c = '''function {0}() 119 | {{ 120 | '''.format(name) 121 | c += '\n'.join('if ({0}, {1}) ~ bgp_community then return false;'.format(*v.split(':')) for v in match['value']) 122 | c += ''' 123 | return true; 124 | } 125 | ''' 126 | return c 127 | 128 | def gen_ext_community_filter(name, match): 129 | c = '''function {0}() 130 | {{ 131 | '''.format(name) 132 | c += '\n'.join('if ({0}, {1}, {2}) ~ bgp_ext_community then return false;'.format(*v.split(':')) for v in match['value']) 133 | c += ''' 134 | return true; 135 | } 136 | ''' 137 | return c 138 | 139 | def gen_filter(name, match): 140 | c = ['function {0}()'.format(name), '{'] 141 | for typ, name in match: 142 | c.append(' if ! {0}() then return false;'.format(name)) 143 | c.append('return true;') 144 | c.append('}') 145 | return '\n'.join(c) + '\n' 146 | 147 | with open('{0}/{1}'.format(self.host_dir, self.CONFIG_FILE_NAME), 'w') as f: 148 | f.write(config) 149 | if 'filter_test' in self.conf: 150 | f.write(self.get_filter_test_config()) 151 | 152 | if 'policy' in self.scenario_global_conf: 153 | for k, v in self.scenario_global_conf['policy'].items(): 154 | match_info = [] 155 | for i, match in enumerate(v['match']): 156 | n = '{0}_match_{1}'.format(k, i) 157 | if match['type'] == 'prefix': 158 | f.write(gen_prefix_filter(n, match)) 159 | elif match['type'] == 'as-path': 160 | f.write(gen_aspath_filter(n, match)) 161 | elif match['type'] == 'community': 162 | f.write(gen_community_filter(n, match)) 163 | elif match['type'] == 'ext-community': 164 | f.write(gen_ext_community_filter(n, match)) 165 | match_info.append((match['type'], n)) 166 | f.write(gen_filter(k, match_info)) 167 | if self.DYNAMIC_NEIGHBORS: 168 | config = self.get_dynamic_neighbor_config() 169 | f.write(config) 170 | f.flush() 171 | 172 | else: 173 | for n in sorted(list(flatten(list(t.get('neighbors', {}).values()) for t in self.scenario_global_conf['testers'])) + [self.scenario_global_conf['monitor']], key=lambda n: n['as']): 174 | f.write(gen_neighbor_config(n)) 175 | 176 | 177 | def get_dynamic_neighbor_config(self): 178 | filter = 'all' 179 | if 'filter_test' in self.conf: 180 | filter = f"filter {self.conf['filter_test']}" 181 | config = '''protocol bgp everything {{ 182 | local as {}; 183 | neighbor range 10.0.0.0/8 external; 184 | #hold time 10; 185 | connect delay time 1; 186 | ipv4 {{import {}; export all; }}; 187 | #rs client; 188 | }} 189 | '''.format(self.conf['as'], filter) 190 | 191 | return config 192 | 193 | 194 | def get_filter_test_config(self): 195 | file = open("filters/bird.conf", mode='r') 196 | filters = file.read() 197 | file.close 198 | return filters 199 | 200 | def get_startup_cmd(self): 201 | return '\n'.join( 202 | ['#!/bin/bash', 203 | 'ulimit -n 65536', 204 | 'bird -c {guest_dir}/{config_file_name} -d > {guest_dir}/bird.log 2>&1'] 205 | ).format( 206 | guest_dir=self.guest_dir, 207 | config_file_name=self.CONFIG_FILE_NAME) 208 | 209 | def get_version_cmd(self): 210 | return "bird --version" 211 | 212 | def exec_version_cmd(self): 213 | version = self.get_version_cmd() 214 | i = dckr.exec_create(container=self.name, cmd=version, stderr=True) 215 | ret =dckr.exec_start(i['Id'], stream=False, detach=False).decode('utf-8') 216 | if len(ret) > 2: 217 | return ret.split(' ')[2].strip('\n') 218 | else: 219 | return ret.strip('\n') 220 | 221 | def get_neighbors_state(self): 222 | neighbors_accepted = {} 223 | neighbors_received = {} 224 | neighbor_received_output = self.local("birdc 'show protocols all'").decode('utf-8') 225 | 226 | with open('bird.tfsm') as template: 227 | fsm = textfsm.TextFSM(template) 228 | result = fsm.ParseText(neighbor_received_output) 229 | 230 | for r in result: 231 | if r[0] == '' : 232 | continue 233 | else: 234 | neighbors_accepted[r[0]] = int(r[2]) if r[2] != '' else 0 235 | neighbors_received[r[0]] = int(r[1]) if r[1] != '' else 0 236 | 237 | return neighbors_received, neighbors_accepted 238 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /base.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2016 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 settings import dckr 17 | import io 18 | import os 19 | from itertools import chain 20 | from threading import Thread 21 | import netaddr 22 | import sys 23 | import time 24 | import datetime 25 | from jinja2 import Environment, FileSystemLoader, PackageLoader, StrictUndefined, make_logging_undefined 26 | 27 | 28 | flatten = lambda l: chain.from_iterable(l) 29 | 30 | def get_ctn_names(): 31 | names = list(flatten(n['Names'] for n in dckr.containers(all=True))) 32 | return [n[1:] if n[0] == '/' else n for n in names] 33 | 34 | 35 | def ctn_exists(name): 36 | return name in get_ctn_names() 37 | 38 | 39 | def img_exists(name): 40 | return name in [ctn['RepoTags'][0].split(':')[0] for ctn in dckr.images() if ctn['RepoTags'] != None and len(ctn['RepoTags']) > 0] 41 | 42 | 43 | def rm_line(): 44 | print('\x1b[1A\x1b[2K\x1b[1D\x1b[1A') 45 | 46 | 47 | class Container(object): 48 | def __init__(self, name, image, host_dir, guest_dir, conf): 49 | self.name = name 50 | self.image = image 51 | self.host_dir = host_dir 52 | self.guest_dir = guest_dir 53 | self.conf = conf 54 | self.config_name = None 55 | self.stop_monitoring = False 56 | self.command = None 57 | self.environment = None 58 | self.volumes = [self.guest_dir] 59 | if not os.path.exists(host_dir): 60 | os.makedirs(host_dir) 61 | os.chmod(host_dir, 0o777) 62 | 63 | @classmethod 64 | def build_image(cls, force, tag, nocache=False): 65 | def insert_after_from(dockerfile, line): 66 | lines = dockerfile.split('\n') 67 | i = -1 68 | for idx, l in enumerate(lines): 69 | elems = [e.strip() for e in l.split()] 70 | if len(elems) > 0 and elems[0] == 'FROM': 71 | i = idx 72 | if i < 0: 73 | raise Exception('no FROM statement') 74 | lines.insert(i+1, line) 75 | return '\n'.join(lines) 76 | 77 | for env in ['http_proxy', 'https_proxy']: 78 | if env in os.environ: 79 | cls.dockerfile = insert_after_from(cls.dockerfile, 'ENV {0} {1}'.format(env, os.environ[env])) 80 | 81 | f = io.BytesIO(cls.dockerfile.encode('utf-8')) 82 | if force or not img_exists(tag): 83 | print('build {0}...'.format(tag)) 84 | for line in dckr.build(fileobj=f, rm=False, tag=tag, decode=True, nocache=nocache): 85 | if 'stream' in line: 86 | print(line['stream'].strip()) 87 | 88 | if 'errorDetail' in line: 89 | print(line['errorDetail']) 90 | 91 | def get_ipv4_addresses(self): 92 | if 'local-address' in self.conf: 93 | local_addr = self.conf['local-address'] 94 | return [local_addr] 95 | raise NotImplementedError() 96 | 97 | def get_host_config(self): 98 | host_config = dckr.create_host_config( 99 | binds=['{0}:{1}'.format(os.path.abspath(self.host_dir), self.guest_dir)], 100 | privileged=True, 101 | network_mode='bridge', 102 | cap_add=['NET_ADMIN'] 103 | ) 104 | return host_config 105 | 106 | def run(self, dckr_net_name='', rm=True): 107 | 108 | if rm and ctn_exists(self.name): 109 | print('remove container:', self.name) 110 | dckr.remove_container(self.name, force=True) 111 | 112 | host_config = self.get_host_config() 113 | 114 | ctn = dckr.create_container(image=self.image, command=self.command, environment=self.environment, 115 | detach=True, name=self.name, 116 | stdin_open=True, volumes=self.volumes, host_config=host_config) 117 | self.ctn_id = ctn['Id'] 118 | 119 | ipv4_addresses = self.get_ipv4_addresses() 120 | 121 | net_id = None 122 | for network in dckr.networks(names=[dckr_net_name]): 123 | if network['Name'] != dckr_net_name: 124 | continue 125 | 126 | net_id = network['Id'] 127 | if not 'IPAM' in network: 128 | print(('can\'t verify if container\'s IP addresses ' 129 | 'are valid for Docker network {}: missing IPAM'.format(dckr_net_name))) 130 | break 131 | ipam = network['IPAM'] 132 | 133 | if not 'Config' in ipam: 134 | print(('can\'t verify if container\'s IP addresses ' 135 | 'are valid for Docker network {}: missing IPAM.Config'.format(dckr_net_name))) 136 | break 137 | 138 | ip_ok = False 139 | network_subnets = [item['Subnet'] for item in ipam['Config'] if 'Subnet' in item] 140 | for ip in ipv4_addresses: 141 | for subnet in network_subnets: 142 | ip_ok = netaddr.IPAddress(ip) in netaddr.IPNetwork(subnet) 143 | 144 | if not ip_ok: 145 | print(('the container\'s IP address {} is not valid for Docker network {} ' 146 | 'since it\'s not part of any of its subnets ({})'.format( 147 | ip, dckr_net_name, ', '.join(network_subnets)))) 148 | print(('Please consider removing the Docket network {net} ' 149 | 'to allow bgperf to create it again using the ' 150 | 'expected subnet:\n' 151 | ' docker network rm {net}'.format(net=dckr_net_name))) 152 | sys.exit(1) 153 | break 154 | 155 | if net_id is None: 156 | print('Docker network "{}" not found!'.format(dckr_net_name)) 157 | return 158 | 159 | dckr.connect_container_to_network(self.ctn_id, net_id, ipv4_address=ipv4_addresses[0]) 160 | dckr.start(container=self.name) 161 | 162 | if len(ipv4_addresses) > 1: 163 | 164 | # get the interface used by the first IP address already added by Docker 165 | dev = None 166 | pxlen = None 167 | res = self.local('ip addr').decode("utf-8") 168 | 169 | for line in res.split('\n'): 170 | if ipv4_addresses[0] in line: 171 | dev = line.split(' ')[-1].strip() 172 | pxlen = line.split('/')[1].split(' ')[0].strip() 173 | if not dev: 174 | dev = "eth0" 175 | pxlen = 8 176 | 177 | for ip in ipv4_addresses[1:]: 178 | self.local(f'ip addr add {ip}/{pxlen} dev {dev}') 179 | 180 | return ctn 181 | 182 | def stats(self, queue): 183 | def stats(): 184 | if self.stop_monitoring: 185 | return 186 | 187 | for stat in dckr.stats(self.ctn_id, decode=True): 188 | if self.stop_monitoring: 189 | return 190 | 191 | cpu_percentage = 0.0 192 | prev_cpu = stat['precpu_stats']['cpu_usage']['total_usage'] 193 | if 'system_cpu_usage' in stat['precpu_stats']: 194 | prev_system = stat['precpu_stats']['system_cpu_usage'] 195 | else: 196 | prev_system = 0 197 | cpu = stat['cpu_stats']['cpu_usage']['total_usage'] 198 | system = stat['cpu_stats']['system_cpu_usage'] if 'system_cpu_usage' in stat['cpu_stats'] else 0 199 | 200 | cpu_num = stat['cpu_stats']['online_cpus'] 201 | cpu_delta = float(cpu) - float(prev_cpu) 202 | system_delta = float(system) - float(prev_system) 203 | if system_delta > 0.0 and cpu_delta > 0.0: 204 | cpu_percentage = (cpu_delta / system_delta) * float(cpu_num) * 100.0 205 | mem_usage = stat['memory_stats'].get('usage', 0) 206 | queue.put({'who': self.name, 'cpu': cpu_percentage, 'mem': mem_usage, 'time': datetime.datetime.now()}) 207 | 208 | t = Thread(target=stats) 209 | t.daemon = True 210 | t.start() 211 | 212 | def neighbor_stats(self, queue): 213 | def stats(): 214 | while True: 215 | if self.stop_monitoring: 216 | return 217 | neighbors_received_full, neighbors_checked = self.get_neighbor_received_routes() 218 | queue.put({'who': self.name, 'neighbors_checked': neighbors_checked}) 219 | queue.put({'who': self.name, 'neighbors_received_full': neighbors_received_full}) 220 | time.sleep(1) 221 | 222 | t = Thread(target=stats) 223 | t.daemon = True 224 | t.start() 225 | 226 | def local(self, cmd, stream=False, detach=False, stderr=False): 227 | i = dckr.exec_create(container=self.name, cmd=cmd, stderr=stderr) 228 | return dckr.exec_start(i['Id'], stream=stream, detach=detach) 229 | 230 | def get_startup_cmd(self): 231 | raise NotImplementedError() 232 | 233 | def get_version_cmd(self): 234 | raise NotImplementedError() 235 | 236 | def exec_version_cmd(self): 237 | version = self.get_version_cmd() 238 | i = dckr.exec_create(container=self.name, cmd=version, stderr=False) 239 | return dckr.exec_start(i['Id'], stream=False, detach=False).decode('utf-8') 240 | 241 | def exec_startup_cmd(self, stream=False, detach=False): 242 | startup_content = self.get_startup_cmd() 243 | 244 | if not startup_content: 245 | return 246 | filename = '{0}/start.sh'.format(self.host_dir) 247 | with open(filename, 'w') as f: 248 | f.write(startup_content) 249 | os.chmod(filename, 0o777) 250 | 251 | return self.local('{0}/start.sh'.format(self.guest_dir), 252 | detach=detach, 253 | stream=stream) 254 | 255 | def get_test_counts(self): 256 | '''gets the configured counts that each tester is supposed to send''' 257 | tester_count = {} 258 | neighbors_checked = {} 259 | for tester in self.scenario_global_conf['testers']: 260 | for n in tester['neighbors'].keys(): 261 | tester_count[n] = tester['neighbors'][n]['check-points'] 262 | neighbors_checked[n] = False 263 | return tester_count, neighbors_checked 264 | 265 | def get_neighbor_received_routes(self): 266 | ## if we ccall this before the daemon starts we will not get output 267 | 268 | tester_count, neighbors_checked = self.get_test_counts() 269 | neighbors_received_full = neighbors_checked.copy() 270 | neighbors_received, neighbors_accepted = self.get_neighbors_state() 271 | for n in neighbors_accepted.keys(): 272 | 273 | #this will include the monitor, we don't want to check that 274 | if n in tester_count and neighbors_accepted[n] >= tester_count[n]: 275 | neighbors_checked[n] = True 276 | 277 | 278 | for n in neighbors_received.keys(): 279 | 280 | #this will include the monitor, we don't want to check that 281 | if (n in tester_count and neighbors_received[n] >= tester_count[n]) or neighbors_received[n] == True: 282 | neighbors_received_full[n] = True 283 | 284 | return neighbors_received_full, neighbors_checked 285 | 286 | class Target(Container): 287 | 288 | CONFIG_FILE_NAME = None 289 | 290 | def write_config(self): 291 | raise NotImplementedError() 292 | 293 | def use_existing_config(self): 294 | if 'config_path' in self.conf: 295 | with open('{0}/{1}'.format(self.host_dir, self.CONFIG_FILE_NAME), 'w') as f: 296 | with open(self.conf['config_path'], 'r') as orig: 297 | f.write(orig.read()) 298 | return True 299 | return False 300 | 301 | def run(self, scenario_global_conf, dckr_net_name=''): 302 | self.scenario_global_conf = scenario_global_conf 303 | # create config before container is created 304 | if not self.use_existing_config(): 305 | self.write_config() 306 | 307 | ctn = super(Target, self).run(dckr_net_name) 308 | 309 | 310 | self.exec_startup_cmd(detach=True) 311 | 312 | return ctn 313 | 314 | def get_template(self, data, template_file="junos.j2",): 315 | env = Environment(loader=FileSystemLoader(searchpath="./nos_templates")) 316 | template = env.get_template(template_file) 317 | output = template.render(data=data) 318 | return output 319 | 320 | class Tester(Container): 321 | 322 | CONTAINER_NAME_PREFIX = None 323 | 324 | def __init__(self, name, host_dir, conf, image): 325 | Container.__init__(self, self.CONTAINER_NAME_PREFIX + name, image, host_dir, self.GUEST_DIR, conf) 326 | 327 | def get_ipv4_addresses(self): 328 | res = [] 329 | peers = list(self.conf.get('neighbors', {}).values()) 330 | for p in peers: 331 | res.append(p['local-address']) 332 | return res 333 | 334 | def configure_neighbors(self, target_conf): 335 | raise NotImplementedError() 336 | 337 | def run(self, target_conf, dckr_net_name): 338 | self.ctn = super(Tester, self).run(dckr_net_name) 339 | 340 | self.configure_neighbors(target_conf) 341 | 342 | def launch(self): 343 | output = self.exec_startup_cmd(stream=True, detach=False) 344 | 345 | cnt = 0 346 | prev_pid = 0 347 | for lines in output: # This is the ExaBGP output 348 | lines = lines.decode("utf-8").strip().split('\n') 349 | for line in lines: 350 | fields = line.split('|') 351 | if len(fields) >2: 352 | # Get PID from ExaBGP output 353 | try: 354 | # ExaBGP Version >= 4 355 | # e.g. 00:00:00 | 111 | control | command/comment 356 | pid = int(fields[1]) 357 | except ValueError: 358 | # ExaBGP Version = 3 359 | # e.g. 00:00:00 | INFO | 111 | control | command 360 | pid = int(fields[2]) 361 | if pid != prev_pid: 362 | prev_pid = pid 363 | cnt += 1 364 | if cnt > 1: 365 | rm_line() 366 | print('tester booting.. ({0}/{1})'.format(cnt, len(list(self.conf.get('neighbors', {}).values())))) 367 | else: 368 | print(lines) 369 | 370 | return None 371 | 372 | def find_errors(): 373 | return 0 374 | 375 | def find_timeouts(): 376 | return 0 377 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | bgperf2 2 | ======== 3 | 4 | bgperf2 is a performance measurement tool for BGP implementation. This was forked from https://github.com/osrg/bgperf and has been changed significantly. 5 | 6 | * [How to install](#how_to_install) 7 | * [How to use](#how_to_use) 8 | * [How bgperf2 works](https://github.com/netenglabs/bgperf2/blob/master/docs/how_bgperf_works.md) 9 | * [Benchmark remote target](https://github.com/netenglabs/bgperf2/blob/master/docs/benchmark_remote_target.md) 10 | * [MRT injection](https://github.com/netenglabs/bgperf2/blob/master/docs/mrt.md) 11 | 12 | ## Updates from original bgperf 13 | I've changed bgperf to work with python 3 and work with new versions of all the NOSes. It actually works, the original version that this is a fork of does not work anymore because of newer version of python and each of the routing stacks. 14 | 15 | This version no longer compiles EXABGP or FRR, it gets PIP or containers already created. Quagga has been removed since it doesn't seem to be updated anymore. 16 | 17 | To get bgperf2 to work with all the changes in each stack I've had to change configuration. I 18 | don't know if all the features of bgperr still work: I've gotten the simplest version of 19 | each config to work. 20 | 21 | Caveats: 22 | 23 | I don't know if adding more policy will still work for all targets. 24 | I haven't tested remote targets. 25 | 26 | ## What it does 27 | bgperf2 creates containers of bgp software to be performance tested. It then can run either one off tests "bench" or 28 | a group of tests "batch". It will create graphs to help you understand what the bgp software is doing as well as to 29 | compare across batches. 30 | 31 | bgperf2 has two main ways of producing prefixes for BGP performance testing. The first way uses either BIRD or EXABGP 32 | to create prefixes and send them. These are pretty lightweight, so it's easy to generate hundreds (or even thousands) 33 | of neighbors with a small amount (hundreds or thousands) or prefixes. BIRD is generally faster, so it is the default 34 | but EXABGP is around for some extra testing. 35 | 36 | The second way to generate traffic is by playing back MRT files using [bgpdump2](https://github.com/rtbrick/bgpdump2). 37 | This is much faster, and is good for playing back internet size tables. [RouteViews](http://archive.routeviews.org/) 38 | is a good place to get MRT files to play back. 39 | 40 | 41 | ## Prerequisites 42 | 43 | * Python 3.7 or later 44 | * Docker 45 | * Sysstat 46 | 47 | ## How to install 48 | 49 | ```bash 50 | $ git clone https://github.com:jopietsch/bgperf.git 51 | $ cd bgperf2 52 | $ pip3 install -r pip-requirements.txt 53 | $ ./bgperf2.py --help 54 | usage: bgperf2.py [-h] [-b BENCH_NAME] [-d DIR] 55 | {doctor,prepare,update,bench,config} ... 56 | 57 | BGP performance measuring tool 58 | 59 | positional arguments: 60 | {doctor,prepare,update,bench,config} 61 | doctor check env 62 | prepare prepare env 63 | update pull bgp docker images 64 | bench run benchmarks 65 | config generate config 66 | 67 | optional arguments: 68 | -h, --help show this help message and exit 69 | -b BENCH_NAME, --bench-name BENCH_NAME 70 | -d DIR, --dir DIR 71 | $ ./bgperf2.py prepare 72 | $ ./bgperf2.py doctor 73 | docker version ... ok (1.9.1) 74 | bgperf2 image ... ok 75 | gobgp image ... ok 76 | bird image ... ok 77 | ``` 78 | ## How to use 79 | 80 | Use `bench` command to start benchmark test. 81 | By default, `bgperf2` benchmarks [GoBGP](https://github.com/osrg/gobgp). 82 | `bgperf2` boots 100 BGP test peers each advertises 100 routes to `GoBGP`. 83 | 84 | ```bash 85 | $ python3 bgperf2.py bench 86 | run monitor 87 | run gobgp 88 | Waiting 5 seconds for neighbor 89 | run tester tester type normal 90 | tester booting.. (100/100) 91 | elapsed: 2sec, cpu: 0.79%, mem: 42.27MB, recved: 10000 92 | gobgp: 2.29.0 93 | Max cpu: 554.03, max mem: 45.71MB 94 | Time since first received prefix: 2 95 | total time: 24.07s 96 | 97 | name, target, version, peers, prefixes per peer, neighbor (s), elapsed (s), prefix received (s), exabgp (s), total time, max cpu %, max mem (GB), flags, date,cores,Mem (GB) 98 | gobgp,gobgp,2.29.0,100,100,5,2,0,2,24.07,554,0.045,,2021-08-02,32,62.82GB 99 | ``` 100 | 101 | As you might notice, the interesting statistics are shown twice, once in an easy to read format and the second 102 | in a CSV format to easily copy and paste to do analysis later. 103 | 104 | To change a target implementation, use `-t` option. 105 | Currently, `bgperf2` supports [BIRD](http://bird.network.cz/) and [FRRouting](https://frrouting.org/) 106 | (other than GoBGP. There is very intial support for[RustyBGP](https://github.com/osrg/rustybgp), partly 107 | because RustyBGP doesn't support all policy that Bgperf2 tries to use for policy testing. If you just want to 108 | do routes and neighbors then RustyBGP works. 109 | 110 | ```bash 111 | $ python3 bgperf2.py bench -t bird 112 | run monitor 113 | run bird 114 | Waiting 4 seconds for neighbor 115 | run tester tester type normal 116 | tester booting.. (100/100) 117 | elapsed: 1sec, cpu: 1.79%, mem: 110.64MB, recved: 10000 118 | bird: v2.0.8-59-gf761be6b 119 | Max cpu: 1.79, max mem: 110.64MB 120 | Time since first received prefix: 1 121 | total time: 20.73s 122 | 123 | name, target, version, peers, prefixes per peer, neighbor (s), elapsed (s), prefix received (s), exabgp (s), total time, max cpu %, max mem (GB), flags, date,cores,Mem (GB) 124 | bird,bird,v2.0.8-59-gf761be6b,100,100,4,1,0,1,20.73,2,0.108,,2021-08-02,32,62.82GB 125 | ``` 126 | 127 | To change a load, use following options. 128 | 129 | * `-n` : the number of BGP test peer (default 100) 130 | * `-p` : the number of prefix each peer advertise (default 100) 131 | * `-a` : the number of as-path filter (default 0) 132 | * `-e` : the number of prefix-list filter (default 0) 133 | * `-c` : the number of community-list filter (default 0) 134 | * `-x` : the number of ext-community-list filter (default 0) 135 | 136 | ```bash 137 | $ python3 bgperf2.py bench 138 | run monitor 139 | run gobgp 140 | Waiting 5 seconds for neighbor 141 | run tester tester type normal 142 | tester booting.. (100/100) 143 | elapsed: 2sec, cpu: 0.79%, mem: 42.27MB, recved: 10000 144 | gobgp: 2.29.0 145 | Max cpu: 554.03, max mem: 45.71MB 146 | Time since first received prefix: 2 147 | total time: 24.07s 148 | 149 | name, target, version, peers, prefixes per peer, neighbor (s), elapsed (s), prefix received (s), exabgp (s), total time, max cpu %, max mem (GB), flags, date,cores,Mem (GB) 150 | gobgp,gobgp,2.29.0,100,100,5,2,0,2,24.07,554,0.045,,2021-08-02,32,62.82GB 151 | ``` 152 | 153 | For a comprehensive list of options, run `python3 ./bgperf2.py bench --help`. 154 | 155 | ## targets 156 | 157 | Targets are the container being tested. bgperf2 was initially created to create containers of BGP software and 158 | and make them testable. However, a challenge is the best way to be able to do this over time. For instance, 159 | the instructions for how to build these software stacks has changed over time. So is the best way to keep up 160 | to compile the software ourselves or to try to download containers from the open source project themsevles. 161 | When I originally forked bgperf2 it hadn't changed in 4 years, so almost none of the containers could be built 162 | and all of the software had changed how they interat. I'm not sure how best to make bgperf2 work over time. 163 | 164 | Right now that is demonstrated most readily with FRR. If you use bench -t FRR it will use a prebuilt FRRouting 165 | container that is hardcoded to 7.5.1. However, I've also created another target called frr_c, which is a container 166 | that checks FRRouting out of git with the 8.0 tag and builds the container. This container is not automatically 167 | built when you do bgperf2 bench. 168 | 169 | ### Testing commercial BGP Stacks 170 | 171 | bgperf2 was originally created to test open source bgp software, so for most containers it compiles the software 172 | and creates a container. For commerical NOSes this doesn't make sense. For those you will need to download 173 | the container images manually and then use bgperf2. 174 | 175 | For most of these images, bgperf2 mounts a local directory (usually in /tmp/bgperf2) to the container. These 176 | commerical stacks then write back data as root, and set the privleges so that a regular user cannot delete these 177 | files and directories. 178 | 179 | bgperf2 tries to delete /tmp/bgperf2 before it runs, but it can't with data from these stacks, so you 180 | might need to remove them yourself. The other option is to run bgperf2 as root \, that's not a good idea. 181 | 182 | ``` 183 | sudo rm -rf /tmp/bpgperf2 184 | ``` 185 | 186 | I have setup multi-threaded support by default in both of these. If you want to do uni-threaded performance 187 | testing you will have to edit config files, which is documented below. 188 | 189 | **Warning** The license for these stacks prohibits publishing results. Don't publish results. 190 | 191 | #### EOS 192 | 193 | [cEOS overview](https://www.arista.com/en/products/software-controlled-container-networking) 194 | 195 | To download, after getting an account: https://www.arista.com/en/support/software-download. Make sure you get the cEOS64 196 | image. I didn't the first time, and the results are frustringly slow. After downloading: 197 | 198 | ``` bash 199 | $ docker import ../NOS/cEOS64-lab-4.27.0F.tar.xz ceos:latest 200 | ``` 201 | 202 | Be sure to use this command for importing: if you don't tag the image as ceos:latest then bgperf2 203 | won't be able to find the image. 204 | 205 | N.B. EOS takes longer to startup than other BGP software I've tested with bgperf2, so don't be alarmed. 206 | However, if it's taken more than 60 seconds to establish a neighbor, something is wrong and start 207 | looking at logs. 208 | 209 | EOS has less clear directions on how to setup multithreading and the consequences. I can't find an authoritative doc to point to. 210 | 211 | However, if you want to remove multi-threading support, remove this line from nos_templates/eos.j2 212 | 213 | ```bash 214 | service routing protocols model multi-agent 215 | ``` 216 | 217 | There is no way to adjust the number of threads being used. For cEOS it appears to be hardcoded 218 | no matter the hardware that you have. 219 | 220 | #### Juniper 221 | 222 | [Junos cRPD deployment guide](https://www.juniper.net/documentation/us/en/software/crpd/crpd-deployment/index.html) 223 | 224 | 225 | Download the image to your local machine and then run: 226 | 227 | ``` bash 228 | $ docker load -i ../NOS/junos-routing-crpd-docker-21.3R1-S1.1.tgz 229 | $ docker tag crpd:21.3R1-S1.1 crpd:latest 230 | ``` 231 | 232 | Be sure you tag the image or bgperf2 cannot find the image and everything will fail. 233 | 234 | bgperf2 mounts the log directory as /tmp/bgperf2/junos/logs, however there are a lot there and most of it 235 | is not relevant. To see if your config worked correctly on startup: 236 | 237 | ``` bash 238 | $ docker logs bgperf_junos_target 239 | ``` 240 | 241 | [Deploying BGP RIB Sharding and Update Threading](https://www.juniper.net/documentation/en_US/day-one-books/DO_BGPSharding.pdf) -- while informative it's weird to me that it's 2021 and getting mult-threaded performance requires a 40 page document. How am I not supposed to think that networking is two decades behind everybody else in software? (This isn't just a Juniper problem by any means) 242 | 243 | For multithreading, as mentioned above bgperf2 sets this up by default in nos_tempaltes/junos.j2 244 | 245 | ``` bash 246 | processes { 247 | routing { 248 | bgp { 249 | rib-sharding { 250 | number-of-shards {{ data.cores }}; 251 | } 252 | update-threading { 253 | number-of-threads {{ data.cores }}; 254 | } 255 | } 256 | } 257 | } 258 | ``` 259 | 260 | data.cores is set in junos.py and by default it's half the number of availble cores on the test machine, 261 | with a max of 31, since that is the Junos max. If you want to try setting the threads to something different 262 | you can hard code those values. If you want to see without multi-threading, delete that whole section. 263 | 264 | ## batch 265 | A feature called batch lets you run multiple tests, collect all the data, and produces graphs. 266 | If you run a test that runs out of physical RAM on your machine, linux OOM killer will just kill the process and you'll lose the data from that experiment. 267 | 268 | There is an included file batch_example.yaml that shows how it works. You can list the targets that you want 269 | tested in a batch, as well as iterate through prefix count and neighbor count. 270 | 271 | If you use a file that looks like this: 272 | 273 | ```YAML 274 | tests: 275 | - 276 | name: 10K 277 | neighbors: [10, 30, 50, 100] 278 | prefixes: [10_000] 279 | filter_test: [None] 280 | targets: 281 | - 282 | name: bird 283 | label: bird -s 284 | single_table: True 285 | - 286 | name: frr 287 | - 288 | name: gobgp 289 | - 290 | name: frr_c 291 | label: frr 8 292 | - 293 | name: rustybgp 294 | ``` 295 | 296 | You will get output like this: 297 | 298 | ```bash 299 | name, target, version, peers, prefixes per peer, neighbor (s), elapsed (s), prefix received (s), exabgp (s), total time, max cpu %, max mem (GB), flags, date,cores,Mem (GB) 300 | bird -s,bird,v2.0.8-59-gf761be6b,10,10000,3,2,0,2,13.9,30,0.015,-s,2021-08-02,32,62.82GB 301 | frr,frr,FRRouting 7.5.1_git (910c507f1541).,10,10000,0,3,0,3,11.58,37,0.089,,2021-08-02,32,62.82GB 302 | gobgp,gobgp,2.29.0,10,10000,6,9,0,9,24.65,1450,0.141,,2021-08-02,32,62.82GB 303 | frr 8,frr_c,FRRouting 8.0-bgperf (489e9d4e8956).,10,10000,0,3,0,3,11.61,31,0.1,,2021-08-02,32,62.82GB 304 | rustybgp,rustybgp,exec,10,10000,4,3,0,3,15.94,262,0.032,,2021-08-02,32,62.82GB 305 | bird -s,bird,v2.0.8-59-gf761be6b,30,10000,4,3,0,3,26.99,100,0.161,-s,2021-08-02,32,62.82GB 306 | frr,frr,FRRouting 7.5.1_git (ab68d18f80c7).,30,10000,0,3,0,3,22.53,86,0.302,,2021-08-02,32,62.82GB 307 | gobgp,gobgp,2.29.0,30,10000,5,61,0,61,85.8,1620,0.447,,2021-08-02,32,62.82GB 308 | frr 8,frr_c,FRRouting 8.0-bgperf (750804dc0e98).,30,10000,0,3,0,3,22.21,78,0.296,,2021-08-02,32,62.82GB 309 | rustybgp,rustybgp,exec,30,10000,4,4,0,4,28.09,446,0.128,,2021-08-02,32,62.82GB 310 | bird -s,bird,v2.0.8-59-gf761be6b,50,10000,3,6,0,6,42.35,100,0.396,-s,2021-08-02,32,62.82GB 311 | frr,frr,FRRouting 7.5.1_git (9e4604a042a6).,50,10000,0,4,0,4,35.48,102,0.513,,2021-08-02,32,62.82GB 312 | gobgp,gobgp,2.29.0,50,10000,4,160,0,160,194.23,1638,0.875,,2021-08-02,32,62.82GB 313 | frr 8,frr_c,FRRouting 8.0-bgperf (eb81873b8335).,50,10000,1,6,0,6,36.69,103,0.52,,2021-08-02,32,62.82GB 314 | rustybgp,rustybgp,exec,50,10000,4,5,0,5,40.74,469,0.307,,2021-08-02,32,62.82GB 315 | bird -s,bird,v2.0.8-59-gf761be6b,100,10000,3,13,0,13,91.68,100,1.343,-s,2021-08-02,32,62.82GB 316 | frr,frr,FRRouting 7.5.1_git (8dc6f2f40d8c).,100,10000,1,7,0,7,74.99,101,1.291,,2021-08-02,32,62.82GB 317 | gobgp,gobgp,2.29.0,100,10000,5,661,0,661,724.83,1664,1.846,,2021-08-02,32,62.82GB 318 | frr 8,frr_c,FRRouting 8.0-bgperf (74ac3704b034).,100,10000,1,8,1,7,70.46,103,1.116,,2021-08-02,32,62.82GB 319 | rustybgp,rustybgp,exec,100,10000,6,16,0,16,80.37,597,1.253,,2021-08-02,32,62.82GB 320 | ``` 321 | 322 | It will create graphs and a CSV file of the output. 323 | 324 | And some graphs. These are some of the important ones 325 | 326 | ![Time to receive all routes](docs/bgperf_10K_route_reception.png) 327 | 328 | ![CPU Usage ](docs/bgperf_10K_max_cpu.png) 329 | 330 | ![Memory Usage ](docs/bgperf_10K_max_mem.png) 331 | 332 | ## Debugging 333 | 334 | If you try to change the config, it's a little tricky to debug what's going on since there are so many containers. What bgperf is doing is creating configs and startup scripts in 2 and then it copies those to the containers before launching them. It creates three containers: bgperf_exabgp_tester_tester, bgperf_\_target, and bgperf2_monitor. If things aren't working, it's probably because the config for the target is not correct. bgperf2 puts all the log output in /tmp/bgperf2/*.log, but what it doesn't do is capture the output of the startup script. 335 | 336 | If it doesn't seem to be working, try with 1 peer and 1 route (-n1 -p1) and make sure 337 | that it connecting. If it's just stuck at waiting to connect to the neighbor, then probably the config is wrong and neighbors are not being established between the monitor (gobgp) and the NOS being tested 338 | 339 | You'll have to break into gobgp and the test config. 340 | 341 | if you want to see what is happening when the test containers starts, after the test is over (or you've killed it), run 342 | ```$ docker exec bgperf_bird_target /root/config/start.sh``` 343 | that's what bgperf2 is doing. It creates a /root/config/start.sh command and is running it, so if you run it manually you can see if that command produces output to help you debug. 344 | 345 | to clean up any existing docker containers 346 | 347 | ```$ docker kill `docker ps -q`; docker rm `docker ps -aq` ``` 348 | 349 | The startup script is in /tmp/bgperf/\/start.sh and gets copied to the target as /root/config/start.sh. 350 | 351 | In other words, to launch the start.sh and see the output you can run this docker command: 352 | 353 | ```bash 354 | $ docker exec bgperf_bird_target /root/config/start.sh 355 | bird: I found another BIRD running. 356 | 357 | ``` 358 | In this case, things were already working, so I'll run ps and kill the old bird and start a new one. 359 | 360 | ``` 361 | $ docker exec bgperf_bird_target ps auxww 362 | USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND 363 | root 1 0.0 0.0 3984 2820 ? Ss 21:21 0:00 bash 364 | root 14 0.0 0.0 4144 2016 ? Ss 21:21 0:00 bird -c /root/config/bird.conf 365 | root 22 0.0 0.0 5904 2784 ? Rs 21:22 0:00 ps auxww 366 | $ docker exec bgperf_bird_target kill 14 367 | ``` 368 | 369 | ``` 370 | $ docker exec bgperf_bird_target /root/config/start.sh 371 | ``` 372 | No output, so it was just fine. 373 | -------------------------------------------------------------------------------- /bgperf2.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Copyright (C) 2015, 2016 Nippon Telegraph and Telephone Corporation. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 14 | # implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | import argparse 19 | import os 20 | import sys 21 | import yaml 22 | import time 23 | import shutil 24 | import netaddr 25 | import datetime 26 | from collections import defaultdict 27 | from argparse import ArgumentParser, REMAINDER 28 | from itertools import chain, islice 29 | from requests.exceptions import ConnectionError 30 | from pyroute2 import IPRoute 31 | from socket import AF_INET 32 | from nsenter import Namespace 33 | from psutil import virtual_memory 34 | from subprocess import check_output 35 | import matplotlib.pyplot as plt 36 | import numpy as np 37 | from base import * 38 | from exabgp import ExaBGP, ExaBGP_MRTParse 39 | from gobgp import GoBGP, GoBGPTarget 40 | from bird import BIRD, BIRDTarget 41 | from frr import FRRouting, FRRoutingTarget 42 | from frr_compiled import FRRoutingCompiled, FRRoutingCompiledTarget 43 | from rustybgp import RustyBGP, RustyBGPTarget 44 | from openbgp import OpenBGP, OpenBGPTarget 45 | from flock import Flock, FlockTarget 46 | from srlinux import SRLinux, SRLinuxTarget 47 | from junos import Junos, JunosTarget 48 | from eos import Eos, EosTarget 49 | from tester import ExaBGPTester, BIRDTester 50 | from mrt_tester import GoBGPMRTTester, ExaBGPMrtTester 51 | from bgpdump2 import Bgpdump2, Bgpdump2Tester 52 | from monitor import Monitor 53 | from settings import dckr 54 | from queue import Queue 55 | from mako.template import Template 56 | from packaging import version 57 | from docker.types import IPAMConfig, IPAMPool 58 | import re 59 | 60 | def gen_mako_macro(): 61 | return '''<% 62 | import netaddr 63 | from itertools import islice 64 | 65 | it = netaddr.iter_iprange('100.0.0.0','160.0.0.0') 66 | 67 | def gen_paths(num): 68 | return list('{0}/32'.format(ip) for ip in islice(it, num)) 69 | %> 70 | ''' 71 | 72 | def rm_line(): 73 | #print('\x1b[1A\x1b[2K\x1b[1D\x1b[1A') 74 | pass 75 | 76 | 77 | def gc_thresh3(): 78 | gc_thresh3 = '/proc/sys/net/ipv4/neigh/default/gc_thresh3' 79 | with open(gc_thresh3) as f: 80 | return int(f.read().strip()) 81 | 82 | 83 | def doctor(args): 84 | ver = dckr.version()['Version'] 85 | if ver.endswith('-ce'): 86 | curr_version = version.parse(ver.replace('-ce', '')) 87 | else: 88 | curr_version = version.parse(ver) 89 | min_version = version.parse('1.9.0') 90 | ok = curr_version >= min_version 91 | print('docker version ... {1} ({0})'.format(ver, 'ok' if ok else 'update to {} at least'.format(min_version))) 92 | 93 | print('bgperf image', end=' ') 94 | if img_exists('bgperf/exabgp'): 95 | print('... ok') 96 | else: 97 | print('... not found. run `bgperf prepare`') 98 | 99 | for name in ['gobgp', 'bird', 'frr_c', 'rustybgp', 'openbgp', 'flock', 'srlinux']: 100 | print('{0} image'.format(name), end=' ') 101 | if img_exists('bgperf/{0}'.format(name)): 102 | print('... ok') 103 | else: 104 | print('... not found. if you want to bench {0}, run `bgperf prepare`'.format(name)) 105 | 106 | print('/proc/sys/net/ipv4/neigh/default/gc_thresh3 ... {0}'.format(gc_thresh3())) 107 | 108 | 109 | def prepare(args): 110 | ExaBGP.build_image(args.force, nocache=args.no_cache) 111 | ExaBGP_MRTParse.build_image(args.force, nocache=args.no_cache) 112 | GoBGP.build_image(args.force, nocache=args.no_cache) 113 | BIRD.build_image(args.force, nocache=args.no_cache) 114 | #FRRouting.build_image(args.force, nocache=args.no_cache) 115 | RustyBGP.build_image(args.force, nocache=args.no_cache) 116 | OpenBGP.build_image(args.force, nocache=args.no_cache) 117 | FRRoutingCompiled.build_image(args.force, nocache=args.no_cache) 118 | Bgpdump2.build_image(args.force, nocache=args.no_cache) 119 | #don't do anything for srlinux, junos, eos because it's just a download out of band 120 | 121 | 122 | 123 | def update(args): 124 | if args.image == 'all' or args.image == 'exabgp': 125 | ExaBGP.build_image(True, checkout=args.checkout, nocache=args.no_cache) 126 | if args.image == 'all' or args.image == 'exabgp_mrtparse': 127 | ExaBGP_MRTParse.build_image(True, checkout=args.checkout, nocache=args.no_cache) 128 | if args.image == 'all' or args.image == 'gobgp': 129 | GoBGP.build_image(True, checkout=args.checkout, nocache=args.no_cache) 130 | if args.image == 'all' or args.image == 'bird': 131 | BIRD.build_image(True, checkout=args.checkout, nocache=args.no_cache) 132 | if args.image == 'all' or args.image == 'frr': 133 | FRRouting.build_image(True, checkout=args.checkout, nocache=args.no_cache) 134 | if args.image == 'all' or args.image == 'rustybgp': 135 | RustyBGP.build_image(True, checkout=args.checkout, nocache=args.no_cache) 136 | if args.image == 'all' or args.image == 'openbgp': 137 | OpenBGP.build_image(True, checkout=args.checkout, nocache=args.no_cache) 138 | if args.image == 'all' or args.image == 'flock': 139 | Flock.build_image(True, checkout=args.checkout, nocache=args.no_cache) 140 | if args.image == 'all' or args.image == 'frr_c': 141 | FRRoutingCompiled.build_image(True, checkout=args.checkout, nocache=args.no_cache) 142 | if args.image == 'eos': 143 | Eos.build_image(True, checkout=args.checkout, nocache=args.no_cache) 144 | if args.image == 'bgpdump2': 145 | Bgpdump2.build_image(True, checkout=args.checkout, nocache=args.no_cache) 146 | 147 | def remove_target_containers(): 148 | for target_class in [BIRDTarget, GoBGPTarget, FRRoutingTarget, FRRoutingCompiledTarget, 149 | RustyBGPTarget, OpenBGPTarget, FlockTarget, JunosTarget, SRLinuxTarget, EosTarget]: 150 | if ctn_exists(target_class.CONTAINER_NAME): 151 | print('removing target container', target_class.CONTAINER_NAME) 152 | dckr.remove_container(target_class.CONTAINER_NAME, force=True) 153 | 154 | def remove_old_containers(): 155 | if ctn_exists(Monitor.CONTAINER_NAME): 156 | print('removing monitor container', Monitor.CONTAINER_NAME) 157 | dckr.remove_container(Monitor.CONTAINER_NAME, force=True) 158 | 159 | for i, ctn_name in enumerate (get_ctn_names()): 160 | if ctn_name.startswith(ExaBGPTester.CONTAINER_NAME_PREFIX) or \ 161 | ctn_name.startswith(ExaBGPMrtTester.CONTAINER_NAME_PREFIX) or \ 162 | ctn_name.startswith(GoBGPMRTTester.CONTAINER_NAME_PREFIX) or \ 163 | ctn_name.startswith(Bgpdump2Tester.CONTAINER_NAME_PREFIX) or \ 164 | ctn_name.startswith(BIRDTester.CONTAINER_NAME_PREFIX): 165 | print(f"removing tester container {i} {ctn_name}") 166 | if i > 0: 167 | rm_line() 168 | dckr.remove_container(ctn_name, force=True) 169 | 170 | 171 | def controller_idle_percent(queue): 172 | '''collect stats on the whole machine that is running the tests''' 173 | stop_monitoring = False 174 | def stats(): 175 | output = {} 176 | output['who'] = 'controller' 177 | 178 | while True: 179 | if stop_monitoring == True: 180 | return 181 | utilization = check_output(['mpstat', '1' ,'1']).decode('utf-8').split('\n')[3] 182 | g = re.match(r'.*all\s+.*\d+\s+(\d+\.\d+)', utilization).groups() 183 | output['idle'] = float(g[0]) 184 | output['time'] = datetime.datetime.now() 185 | queue.put(output) 186 | # dont' sleep because mpstat is already taking 1 second to run 187 | 188 | t = Thread(target=stats) 189 | t.daemon = True 190 | t.start() 191 | 192 | def controller_memory_free(queue): 193 | '''collect stats on the whole machine that is running the tests''' 194 | stop_monitoring = False 195 | def stats(): 196 | output = {} 197 | output['who'] = 'controller' 198 | 199 | while True: 200 | if stop_monitoring == True: 201 | return 202 | free = check_output(['free', '-m']).decode('utf-8').split('\n')[1] 203 | g = re.match(r'.*\d+\s+(\d+)', free).groups() 204 | output['free'] = float(g[0]) * 1024 * 1024 205 | output['time'] = datetime.datetime.now() 206 | queue.put(output) 207 | time.sleep(1) 208 | 209 | t = Thread(target=stats) 210 | t.daemon = True 211 | t.start() 212 | 213 | stop_monitoring = False 214 | 215 | def bench(args): 216 | output_stats = {} 217 | config_dir = '{0}/{1}'.format(args.dir, args.bench_name) 218 | dckr_net_name = args.docker_network_name or args.bench_name + '-br' 219 | 220 | remove_target_containers() 221 | 222 | if not args.repeat: 223 | remove_old_containers() 224 | 225 | if os.path.exists(config_dir): 226 | shutil.rmtree(config_dir, ignore_errors=True) 227 | 228 | bench_start = time.time() 229 | if args.file: 230 | with open(args.file) as f: 231 | conf = yaml.safe_load(Template(f.read()).render()) 232 | else: 233 | conf = gen_conf(args) 234 | 235 | if not os.path.exists(config_dir): 236 | os.makedirs(config_dir) 237 | with open('{0}/scenario.yaml'.format(config_dir), 'w') as f: 238 | f.write(conf) 239 | conf = yaml.safe_load(Template(conf).render()) 240 | 241 | bridge_found = False 242 | for network in dckr.networks(names=[dckr_net_name]): 243 | if network['Name'] == dckr_net_name: 244 | print('Docker network "{}" already exists'.format(dckr_net_name)) 245 | bridge_found = True 246 | break 247 | if not bridge_found: 248 | subnet = conf['local_prefix'] 249 | print('creating Docker network "{}" with subnet {}'.format(dckr_net_name, subnet)) 250 | ipam = IPAMConfig(pool_configs=[IPAMPool(subnet=subnet)]) 251 | network = dckr.create_network(dckr_net_name, driver='bridge', ipam=ipam) 252 | 253 | num_tester = sum(len(t.get('neighbors', [])) for t in conf.get('testers', [])) 254 | if num_tester > gc_thresh3(): 255 | print('gc_thresh3({0}) is lower than the number of peer({1})'.format(gc_thresh3(), num_tester)) 256 | print('type next to increase the value') 257 | print('$ echo 16384 | sudo tee /proc/sys/net/ipv4/neigh/default/gc_thresh3') 258 | 259 | print('run monitor') 260 | m = Monitor(config_dir+'/monitor', conf['monitor']) 261 | m.monitor_for = args.target 262 | m.run(conf, dckr_net_name) 263 | 264 | 265 | ## I'd prefer to start up the testers and then start up the target 266 | # however, bgpdump2 isn't smart enough to wait and rety connections so 267 | # this is the order 268 | testers = [] 269 | mrt_injector = None 270 | if not args.repeat: 271 | valid_indexes = None 272 | asns = None 273 | for idx, tester in enumerate(conf['testers']): 274 | if 'name' not in tester: 275 | name = 'tester{0}'.format(idx) 276 | else: 277 | name = tester['name'] 278 | if not 'type' in tester: 279 | tester_type = 'bird' 280 | else: 281 | tester_type = tester['type'] 282 | if tester_type == 'exa': 283 | tester_class = ExaBGPTester 284 | elif tester_type == 'bird': 285 | tester_class = BIRDTester 286 | elif tester_type == 'mrt': 287 | if 'mrt_injector' not in tester: 288 | mrt_injector = 'gobgp' 289 | else: 290 | mrt_injector = tester['mrt_injector'] 291 | if mrt_injector == 'gobgp': 292 | tester_class = GoBGPMRTTester 293 | elif mrt_injector == 'exabgp': 294 | tester_class = ExaBGPMrtTester 295 | elif mrt_injector == 'bgpdump2': 296 | tester_class = Bgpdump2Tester 297 | else: 298 | print('invalid mrt_injector:', mrt_injector) 299 | sys.exit(1) 300 | 301 | else: 302 | print('invalid tester type:', tester_type) 303 | sys.exit(1) 304 | 305 | 306 | t = tester_class(name, config_dir+'/'+name, tester) 307 | if not mrt_injector: 308 | print('run tester', name, 'type', tester_type) 309 | else: 310 | print('run tester', name, 'type', tester_type, mrt_injector) 311 | if idx > 0: 312 | rm_line() 313 | t.run(conf['target'], dckr_net_name) 314 | testers.append(t) 315 | 316 | 317 | # have to do some extra stuff with bgpdump2 318 | # because it's sending real data, we need to figure out 319 | # wich neighbor has data and what the actual ASN is 320 | if tester_type == 'mrt' and mrt_injector == 'bgpdump2' and not valid_indexes: 321 | print("finding asns and such from mrt file") 322 | valid_indexes = t.get_index_valid(args.prefix_num) 323 | asns = t.get_index_asns() 324 | 325 | for test in conf['testers']: 326 | test['bgpdump-index'] = valid_indexes[test['mrt-index'] % len(valid_indexes)] 327 | neighbor = next(iter(test['neighbors'].values())) 328 | neighbor['as'] = asns[test['bgpdump-index']] 329 | 330 | # TODO: this needs to all be moved to it's own object and file 331 | # so this stuff isn't copied around 332 | str_conf = gen_mako_macro() + yaml.dump(conf, default_flow_style=False) 333 | with open('{0}/scenario.yaml'.format(config_dir), 'w') as f: 334 | f.write(str_conf) 335 | 336 | is_remote = True if 'remote' in conf['target'] and conf['target']['remote'] else False 337 | 338 | if is_remote: 339 | print('target is remote ({})'.format(conf['target']['local-address'])) 340 | 341 | ip = IPRoute() 342 | 343 | # r: route to the target 344 | r = ip.get_routes(dst=conf['target']['local-address'], family=AF_INET) 345 | if len(r) == 0: 346 | print('no route to remote target {0}'.format(conf['target']['local-address'])) 347 | sys.exit(1) 348 | 349 | # intf: interface used to reach the target 350 | idx = [t[1] for t in r[0]['attrs'] if t[0] == 'RTA_OIF'][0] 351 | intf = ip.get_links(idx)[0] 352 | intf_name = intf.get_attr('IFLA_IFNAME') 353 | 354 | # raw_bridge_name: Linux bridge name of the Docker bridge 355 | # TODO: not sure if the linux bridge name is always given by 356 | # "br-". 357 | raw_bridge_name = args.bridge_name or 'br-{}'.format(network['Id'][0:12]) 358 | 359 | # raw_bridges: list of Linux bridges that match raw_bridge_name 360 | raw_bridges = ip.link_lookup(ifname=raw_bridge_name) 361 | if len(raw_bridges) == 0: 362 | if not args.bridge_name: 363 | print(('can\'t determine the Linux bridge interface name starting ' 364 | 'from the Docker network {}'.format(dckr_net_name))) 365 | else: 366 | print(('the Linux bridge name provided ({}) seems nonexistent'.format( 367 | raw_bridge_name))) 368 | print(('Since the target is remote, the host interface used to ' 369 | 'reach the target ({}) must be part of the Linux bridge ' 370 | 'used by the Docker network {}, but without the correct Linux ' 371 | 'bridge name it\'s impossible to verify if that\'s true'.format( 372 | intf_name, dckr_net_name))) 373 | if not args.bridge_name: 374 | print(('Please supply the Linux bridge name corresponding to the ' 375 | 'Docker network {} using the --bridge-name argument.'.format( 376 | dckr_net_name))) 377 | sys.exit(1) 378 | 379 | # intf_bridge: bridge interface that intf is already member of 380 | intf_bridge = intf.get_attr('IFLA_MASTER') 381 | 382 | # if intf is not member of the bridge, add it 383 | if intf_bridge not in raw_bridges: 384 | if intf_bridge is None: 385 | print(('Since the target is remote, the host interface used to ' 386 | 'reach the target ({}) must be part of the Linux bridge ' 387 | 'used by the Docker network {}'.format( 388 | intf_name, dckr_net_name))) 389 | sys.stdout.write('Do you confirm to add the interface {} ' 390 | 'to the bridge {}? [yes/NO] '.format( 391 | intf_name, raw_bridge_name 392 | )) 393 | try: 394 | answer = input() 395 | except: 396 | print('aborting') 397 | sys.exit(1) 398 | answer = answer.strip() 399 | if answer.lower() != 'yes': 400 | print('aborting') 401 | sys.exit(1) 402 | 403 | print('adding interface {} to the bridge {}'.format( 404 | intf_name, raw_bridge_name 405 | )) 406 | br = raw_bridges[0] 407 | 408 | try: 409 | ip.link('set', index=idx, master=br) 410 | except Exception as e: 411 | print(('Something went wrong: {}'.format(str(e)))) 412 | print(('Please consider running the following command to ' 413 | 'add the {iface} interface to the {br} bridge:\n' 414 | ' sudo brctl addif {br} {iface}'.format( 415 | iface=intf_name, br=raw_bridge_name))) 416 | print('\n\n\n') 417 | raise 418 | else: 419 | curr_bridge_name = ip.get_links(intf_bridge)[0].get_attr('IFLA_IFNAME') 420 | print(('the interface used to reach the target ({}) ' 421 | 'is already member of the bridge {}, which is not ' 422 | 'the one used in this configuration'.format( 423 | intf_name, curr_bridge_name))) 424 | print(('Please consider running the following command to ' 425 | 'remove the {iface} interface from the {br} bridge:\n' 426 | ' sudo brctl addif {br} {iface}'.format( 427 | iface=intf_name, br=curr_bridge_name))) 428 | sys.exit(1) 429 | else: 430 | if args.target == 'gobgp': 431 | target_class = GoBGPTarget 432 | elif args.target == 'bird': 433 | target_class = BIRDTarget 434 | elif args.target == 'frr': 435 | target_class = FRRoutingTarget 436 | elif args.target == 'frr_c': 437 | target_class = FRRoutingCompiledTarget 438 | elif args.target == 'rustybgp': 439 | target_class = RustyBGPTarget 440 | elif args.target == 'openbgp': 441 | target_class = OpenBGPTarget 442 | elif args.target == 'flock': 443 | target_class = FlockTarget 444 | elif args.target == 'srlinux': 445 | target_class = SRLinuxTarget 446 | elif args.target == 'junos': 447 | target_class = JunosTarget 448 | elif args.target == 'eos': 449 | target_class = EosTarget 450 | else: 451 | print(f"incorrect target {args.target}") 452 | print('run', args.target) 453 | if args.image: 454 | target = target_class('{0}/{1}'.format(config_dir, args.target), conf['target'], image=args.image) 455 | else: 456 | target = target_class('{0}/{1}'.format(config_dir, args.target), conf['target']) 457 | target.run(conf, dckr_net_name) 458 | 459 | time.sleep(1) 460 | 461 | output_stats['monitor_wait_time'] = m.wait_established(conf['target']['local-address']) 462 | output_stats['cores'], output_stats['memory'] = get_hardware_info() 463 | if target_class == EosTarget: 464 | print("Waiting extra 10 seconds for EOS ") 465 | time.sleep(10) 466 | 467 | start = datetime.datetime.now() 468 | 469 | q = Queue() 470 | 471 | m.stats(q) 472 | controller_idle_percent(q) 473 | controller_memory_free(q) 474 | if not is_remote: 475 | target.stats(q) 476 | target.neighbor_stats(q) 477 | 478 | 479 | # want to launch all the neighbors at the same(ish) time 480 | # launch them after the test starts because as soon as they start they can send info at least for mrt 481 | # does it need to be in a different place for mrt than exabgp? 482 | for i in range(len(testers)): 483 | testers[i].launch() 484 | if i > 0: 485 | rm_line() 486 | print(f"launched {i+1} testers") 487 | # if args.prefix_num >= 100_000: 488 | # time.sleep(1) 489 | 490 | f = open(args.output, 'w') if args.output else None 491 | cpu = 0 492 | mem = 0 493 | 494 | output_stats['max_cpu'] = 0 495 | output_stats['max_mem'] = 0 496 | output_stats['first_received_time'] = start - start 497 | output_stats['min_idle'] = 100 498 | output_stats['min_free'] = 1_000_000_000_000_000 499 | 500 | output_stats['required'] = conf['monitor']['check-points'][0] 501 | bench_stats = [] 502 | neighbors_checked = 0 503 | neighbors_received_full = 0 504 | percent_idle = 0 505 | mem_free = 0 506 | 507 | recved_checkpoint = False 508 | neighbors_checkpoint = False 509 | last_recved = 0 510 | last_recved_count = 0 511 | last_neighbors_checked = 0 512 | recved = 0 513 | less_last_received = 0 514 | while True: 515 | info = q.get() 516 | 517 | if not is_remote and info['who'] == target.name: 518 | if 'neighbors_checked' in info: 519 | if len(info['neighbors_checked']) > 0 and all(value == True for value in info['neighbors_checked'].values()): 520 | neighbors_checked = sum(1 if value == True else 0 for value in info['neighbors_checked'].values()) 521 | neighbors_checkpoint = True 522 | else: 523 | neighbors_checked = sum(1 if value == True else 0 for value in info['neighbors_checked'].values()) 524 | elif 'neighbors_received_full' in info: 525 | 526 | if len(info['neighbors_received_full']) >= 1 and all(value == True for value in info['neighbors_received_full'].values()): 527 | neighbors_received_full = sum(1 if value == True else 0 for value in info['neighbors_received_full'].values()) 528 | neighbors_checkpoint = True 529 | else: 530 | neighbors_received_full = sum(1 if value == True else 0 for value in info['neighbors_received_full'].values()) 531 | else: 532 | cpu = info['cpu'] 533 | mem = info['mem'] 534 | output_stats['max_cpu'] = cpu if cpu > output_stats['max_cpu'] else output_stats['max_cpu'] 535 | output_stats['max_mem'] = mem if mem > output_stats['max_mem'] else output_stats['max_mem'] 536 | 537 | if info['who'] == 'controller': 538 | if 'free' in info: 539 | mem_free = info['free'] 540 | output_stats['min_free'] = mem_free if mem_free < output_stats['min_free'] else output_stats['min_free'] 541 | elif 'idle' in info: 542 | percent_idle = info['idle'] 543 | output_stats['min_idle'] = percent_idle if percent_idle < output_stats['min_idle'] else output_stats['min_idle'] 544 | if info['who'] == m.name: 545 | 546 | elapsed = info['time'] - start 547 | output_stats['elapsed'] = elapsed 548 | recved = info['afi_safis'][0]['state']['accepted'] if 'accepted' in info['afi_safis'][0]['state'] else 0 549 | 550 | if last_recved > recved: 551 | if neighbors_checked >= last_neighbors_checked: 552 | less_last_received += 1 553 | else: 554 | less_last_received = 0 555 | if less_last_received >= 10 and (last_recved - recved) / last_recved > .01: 556 | 557 | output_stats['recved'] = recved 558 | f.close() if f else None 559 | output_stats['fail_msg'] = f"FAILED: dropping received count {recved} neighbors_checked {neighbors_checked}" 560 | output_stats['tester_errors'] = tester_class.find_errors() 561 | output_stats['tester_timeouts'] = tester_class.find_timeouts() 562 | print("FAILED") 563 | o_s = finish_bench(args, output_stats, bench_stats, bench_start,target, m, fail=True) 564 | return o_s 565 | 566 | elif (last_neighbors_checked > 0 or neighbors_received_full > 0) and recved == last_recved: 567 | last_recved_count +=1 568 | else: 569 | last_recved = recved 570 | last_recved_count = 0 571 | 572 | if neighbors_checked != last_neighbors_checked: 573 | last_neighbors_checked = neighbors_checked 574 | last_recved_count = 0 575 | 576 | if elapsed.seconds > 0: 577 | rm_line() 578 | 579 | print('elapsed: {0}sec, cpu: {1:>4.2f}%, mem: {2}, mon recved: {3}, neighbors_received: {4}, neighbors_accepted: {5}, %idle {6}, free mem {7}'.format(elapsed.seconds, 580 | cpu, mem_human(mem), recved, neighbors_received_full, neighbors_checked, percent_idle, mem_human(mem_free))) 581 | bench_stats.append([elapsed.seconds, float(f"{cpu:>4.2f}"), mem, recved, neighbors_checked, percent_idle, mem_free]) 582 | f.write('{0}, {1}, {2}, {3}\n'.format(elapsed.seconds, cpu, mem, recved)) if f else None 583 | f.flush() if f else None 584 | 585 | if info['checked']: 586 | recved_checkpoint = True 587 | 588 | if recved > 0 and output_stats['first_received_time'] == start - start: 589 | output_stats['first_received_time'] = elapsed 590 | 591 | 592 | # we are trying to discover if the tests have finished 593 | # in the ieal world, we'd know how many prefixes were sent and we'd just check for that 594 | # that's how things work when generating with bird or exa, but not with MRT playback or when testing filtering 595 | # with MRT Playback, not all prefixes overlap, so we aren't sure how many there will be. 596 | # for example, each instance might send 800K prefixes, but the total amount of unique prefixes 597 | # might be 867342. We don't want to just stop at 800K, we want to wait a while until things have stablized 598 | # similarly with filtering, we don't know the amount that should be received 599 | # so we have to wait longer to make sure we've received a stable amount of prefixes 600 | # for all cases we make sure that the target has recevied (but not accepted) as many prefixes as specified 601 | # but in the end we want to wait until the monitor has received a stable number of prefixes 602 | 603 | time_for_assurance = 20 604 | 605 | # make sure it's stable but not wait as long as if we hadn't received 606 | # at least as many prefixes as specified 607 | if recved_checkpoint: 608 | time_for_assurance = 5 609 | 610 | if neighbors_checkpoint and last_recved_count >=time_for_assurance: 611 | output_stats['recved']= recved 612 | output_stats['tester_errors'] = tester_class.find_errors() 613 | output_stats['tester_timeouts'] = tester_class.find_timeouts() 614 | 615 | f.close() if f else None 616 | 617 | # subract the last time_for_assurance seconds, it was done by this time, we were just making sure 618 | # TODO: recaulate all min/max stats after removing these stats 619 | # should move to always calculating based on bench_stats rather than while counting 620 | 621 | if last_recved_count >= time_for_assurance: 622 | print(f"last recevied: {last_recved_count}") 623 | output_stats['elapsed'] = datetime.timedelta(seconds = int(output_stats['elapsed'].seconds) - time_for_assurance + 1) 624 | bench_stats = bench_stats[0:len(bench_stats)-time_for_assurance] 625 | o_s = finish_bench(args, output_stats, bench_stats, bench_start,target, m) 626 | return o_s 627 | 628 | if elapsed.seconds % 120 == 0 and elapsed.seconds > 1: 629 | bench_prefix = f"{args.target}_{args.tester_type}_{args.prefix_num}_{args.neighbor_num}" 630 | create_bench_graphs(bench_stats, prefix=bench_prefix) 631 | 632 | if elapsed.seconds > 15 and recved_checkpoint == 0 and last_recved_count == 0 and recved == 0: 633 | last_recved_count = 1_000_000 # make it artifically high so things fail quickly 634 | 635 | # Too many of the same counts in a row, not progressing 636 | # using 600 because in high load some stacks take this longer, or longer 637 | # to process and we want to have good assurance it's really stuck 638 | if last_recved_count >= 600 : 639 | output_stats['recved']= recved 640 | f.close() if f else None 641 | output_stats['fail_msg'] = f"FAILED: stuck received count {recved} neighbors_checked {neighbors_checked}" 642 | output_stats['tester_errors'] = tester_class.find_errors() 643 | output_stats['tester_timeouts'] = tester_class.find_timeouts() 644 | print("FAILED") 645 | o_s = finish_bench(args, output_stats,bench_stats, bench_start,target, m, fail=True) 646 | return o_s 647 | 648 | 649 | def finish_bench(args, output_stats, bench_stats, bench_start,target, m, fail=False): 650 | 651 | bench_stop = time.time() 652 | output_stats['total_time'] = bench_stop - bench_start 653 | m.stop_monitoring = True 654 | target.stop_monitoring = True 655 | stop_monitoring = True 656 | del m 657 | 658 | target_version = target.exec_version_cmd() 659 | 660 | print_final_stats(args, target_version, output_stats) 661 | o_s = create_output_stats(args, target_version, output_stats, fail) 662 | print(stats_header()) 663 | print(','.join(map(str, o_s))) 664 | print() 665 | # it would be better to clean things up, but often I want to to investigate where things ended up 666 | # remove_old_containers() 667 | # remove_target_containers() 668 | bench_prefix = f"{args.target}_{args.tester_type}_{args.prefix_num}_{args.neighbor_num}" 669 | create_bench_graphs(bench_stats, prefix=bench_prefix) 670 | return o_s 671 | 672 | 673 | 674 | def print_final_stats(args, target_version, stats): 675 | 676 | print(f"{args.target}: {target_version}") 677 | print(f"Max cpu: {stats['max_cpu']:4.2f}, max mem: {mem_human(stats['max_mem'])}") 678 | print(f"Min %idle {stats['min_idle']}, Min mem free {mem_human(stats['min_free'])}") 679 | print(f"Time since first received prefix: {stats['elapsed'].seconds - stats['first_received_time'].seconds}") 680 | 681 | print(f"total time: {stats['total_time']:.2f}s") 682 | print(f"elasped time: {stats['elapsed'].seconds}s") 683 | print(f"tester errors: {stats['tester_errors']}") 684 | print(f"tester timeouts: {stats['tester_timeouts']}") 685 | print() 686 | 687 | def stats_header(): 688 | return("name, target, version, peers, prefixes per peer, required, received, monitor (s), elapsed (s), prefix received (s), testers (s), total time, max cpu %, max mem (GB), min idle%, min free mem (GB), flags, date,cores,Mem (GB), tester errors, failed, MSG, filters") 689 | 690 | 691 | def create_output_stats(args, target_version, stats, fail=False): 692 | e = stats['elapsed'].seconds 693 | f = stats['first_received_time'].seconds 694 | d = datetime.date.today().strftime("%Y-%m-%d") 695 | if 'label' in args and args.label: 696 | name = args.label 697 | else: 698 | name = args.target 699 | out = [name, args.target, target_version, str(args.neighbor_num), str(args.prefix_num)] 700 | out.extend([stats['required'], stats['recved']]) 701 | out.extend([stats['monitor_wait_time'], e, f , e-f, float(format(stats['total_time'], ".2f"))]) 702 | out.extend([round(stats['max_cpu']), float(format(stats['max_mem']/1024/1024/1024, ".3f"))]) 703 | out.extend ([round(stats['min_idle']), float(format(stats['min_free']/1024/1024/1024, ".3f"))]) 704 | out.extend(['-s' if args.single_table else '', d, str(stats['cores']), mem_human(stats['memory'])]) 705 | out.extend([stats['tester_errors'],stats['tester_timeouts']]) 706 | out.extend(['FAILED']) if fail else out.extend(['']) 707 | out.extend([stats['fail_msg']]) if 'fail_msg' in stats else out.extend(['']) 708 | out.extend([args.filter_test]) if 'filter_test' in args and args.filter_test else out.extend(['']) 709 | return out 710 | 711 | 712 | def create_ts_graph(bench_stats, stat_index=1, filename='ts.png', ylabel='%cpu', diviser=1): 713 | plt.figure() 714 | #bench_stats.pop(0) 715 | data = np.array(bench_stats) 716 | plt.plot(data[:,0], data[:,stat_index]/diviser) 717 | 718 | #don't want to see 0 element of data, not an accurate measure of what's happening 719 | #plt.xlim([1, len(data)]) 720 | plt.ylabel(ylabel) 721 | plt.xlabel('elapsed seconds') 722 | plt.show() 723 | plt.savefig(filename) 724 | plt.close() 725 | plt.cla() 726 | plt.clf() 727 | 728 | 729 | def create_bench_graphs(bench_stats, prefix='ts_data'): 730 | create_ts_graph(bench_stats, filename=f"{prefix}_cpu.png") 731 | create_ts_graph(bench_stats, stat_index=2, filename=f"{prefix}_mem_used", ylabel="GB", diviser=1024*1024*1024) 732 | create_ts_graph(bench_stats, stat_index=3, filename=f"{prefix}_mon_received", ylabel='prefixes') 733 | create_ts_graph(bench_stats, stat_index=4, filename=f"{prefix}_neighbors", ylabel='neighbors') 734 | create_ts_graph(bench_stats, stat_index=5, filename=f"{prefix}_machine_idle", ylabel="%") 735 | create_ts_graph(bench_stats, stat_index=6, filename=f"{prefix}_free_mem", ylabel="GB", diviser=1024*1024*1024) 736 | 737 | def create_graph(stats, test_name='total time', stat_index=8, test_file='total_time.png', ylabel='seconds'): 738 | labels = {} 739 | data = defaultdict(list) 740 | 741 | try: 742 | for stat in stats: 743 | labels[stat[0]] = True 744 | key = f"{stat[3]}n_{stat[4]}p" 745 | 746 | if stat[24]: 747 | key =f"{key}_{stat[24]}" 748 | 749 | if len(stat) > 23 and stat[22] == 'FAILED':# this means that it failed for some reason 750 | data[key].append(0) 751 | else: 752 | data[key].append(float(stat[stat_index])) 753 | except IndexError as e: 754 | print(e) 755 | print(f"stat line failed: {stat}") 756 | print(f"stat_index {stat_index}") 757 | exit(-1) 758 | 759 | x = np.arange(len(labels)) 760 | 761 | bars = len(data) 762 | width = 0.7 / bars 763 | plt.figure() 764 | for i, d in enumerate(data): 765 | plt.bar(x -0.2+i*width, data[d], width=width, label=d) 766 | 767 | plt.ylabel(ylabel) 768 | #plt.xlabel('neighbors_prefixes') 769 | plt.title(test_name) 770 | plt.xticks(x,labels.keys()) 771 | plt.legend() 772 | 773 | plt.show() 774 | plt.savefig(test_file) 775 | 776 | def batch(args): 777 | """ runs several tests together, produces all the stats together and creates graphs 778 | requires a yaml file to describe the batch of tests to run 779 | 780 | it iterates through a list of targets, number of neighbors and number of prefixes 781 | other variables can be set, but not iterated through 782 | """ 783 | with open(args.batch_config, 'r') as f: 784 | batch_config = yaml.safe_load(f) 785 | 786 | for test in batch_config['tests']: 787 | results = [] 788 | for n in test['neighbors']: 789 | for p in test['prefixes']: 790 | for filter in test['filter_test']: 791 | for t in test['targets']: 792 | a = argparse.Namespace(**vars(args)) 793 | a.func = bench 794 | a.image = None 795 | a.output = None 796 | a.target = t['name'] 797 | a.prefix_num = p 798 | a.neighbor_num = n 799 | a.filter_test = filter if filter != 'None' else None 800 | # read any config attribute that was specified in the yaml batch file 801 | a.local_address_prefix = t['local_address_prefix'] if 'local_address_prefix' in t else '10.10.0.0/16' 802 | for field in ['single_table', 'docker_network_name', 'repeat', 'file', 'target_local_address', 803 | 'label', 'target_local_address', 'monitor_local_address', 'target_router_id', 804 | 'monitor_router_id', 'target_config_file', 'filter_type','mrt_injector', 'mrt_file', 805 | 'tester_type', 'license_file']: 806 | setattr(a, field, t[field]) if field in t else setattr(a, field, None) 807 | 808 | for field in ['as_path_list_num', 'prefix_list_num', 'community_list_num', 'ext_community_list_num']: 809 | setattr(a, field, t[field]) if field in t else setattr(a, field, 0) 810 | results.append(bench(a)) 811 | 812 | # update this each time in case something crashes 813 | with open(f"{test['name']}.csv", 'w') as f: 814 | f.write(stats_header() + '\n') 815 | for stat in results: 816 | f.write(','.join(map(str, stat)) + '\n') 817 | 818 | print() 819 | print(stats_header()) 820 | for stat in results: 821 | print(','.join(map(str, stat))) 822 | 823 | 824 | create_batch_graphs(results, test['name']) 825 | 826 | def create_batch_graphs(results, name): 827 | create_graph(results, test_name='total time', stat_index=11, test_file=f"bgperf_{name}_total_time.png") 828 | create_graph(results, test_name='elapsed', stat_index=8, test_file=f"bgperf_{name}_elapsed.png") 829 | create_graph(results, test_name='neighbor', stat_index=9, test_file=f"bgperf_{name}_neighbor.png") 830 | create_graph(results, test_name='route reception', stat_index=10, test_file=f"bgperf_{name}_route_reception.png") 831 | create_graph(results, test_name='max cpu', stat_index=12, test_file=f"bgperf_{name}_max_cpu.png", ylabel="%") 832 | create_graph(results, test_name='max mem', stat_index=13, test_file=f"bgperf_{name}_max_mem.png", ylabel="GB") 833 | create_graph(results, test_name='min idle', stat_index=14, test_file=f"bgperf_{name}_min_idle.png", ylabel="%") 834 | create_graph(results, test_name='min free mem', stat_index=15, test_file=f"bgperf_{name}_min_free.png", ylabel="GB") 835 | create_graph(results, test_name='tester errors', stat_index=20, test_file=f"bgperf_{name}_tester_error.png", ylabel="errors") 836 | create_graph(results, test_name='prefixes at monitor', stat_index=6, test_file=f"bgperf_{name}_monitor_prefixes.png") 837 | 838 | def mem_human(v): 839 | if v > 1024 * 1024 * 1024: 840 | return '{0:.2f}GB'.format(float(v) / (1024 * 1024 * 1024)) 841 | elif v > 1024 * 1024: 842 | return '{0:.2f}MB'.format(float(v) / (1024 * 1024)) 843 | elif v > 1024: 844 | return '{0:.2f}KB'.format(float(v) / 1024) 845 | else: 846 | return '{0:.2f}B'.format(float(v)) 847 | 848 | def get_hardware_info(): 849 | cores = os.cpu_count() 850 | mem = virtual_memory().total 851 | return cores, mem 852 | 853 | def gen_conf(args): 854 | ''' This creates the scenario.yml that other things need to read to produce device config 855 | ''' 856 | neighbor_num = args.neighbor_num 857 | prefix = args.prefix_num 858 | as_path_list = args.as_path_list_num 859 | prefix_list = args.prefix_list_num 860 | community_list = args.community_list_num 861 | ext_community_list = args.ext_community_list_num 862 | tester_type = args.tester_type 863 | 864 | 865 | local_address_prefix = netaddr.IPNetwork(args.local_address_prefix) 866 | 867 | if args.target_local_address: 868 | target_local_address = netaddr.IPAddress(args.target_local_address) 869 | else: 870 | target_local_address = local_address_prefix.broadcast - 1 871 | 872 | if args.monitor_local_address: 873 | monitor_local_address = netaddr.IPAddress(args.monitor_local_address) 874 | else: 875 | monitor_local_address = local_address_prefix.ip + 2 876 | 877 | if args.target_router_id: 878 | target_router_id = netaddr.IPAddress(args.target_router_id) 879 | else: 880 | target_router_id = target_local_address 881 | 882 | if args.monitor_router_id: 883 | monitor_router_id = netaddr.IPAddress(args.monitor_router_id) 884 | else: 885 | monitor_router_id = monitor_local_address 886 | 887 | filter_test = args.filter_test if 'filter_test' in args else None 888 | 889 | conf = {} 890 | conf['local_prefix'] = str(local_address_prefix) 891 | conf['target'] = { 892 | 'as': 1000, 893 | 'router-id': str(target_router_id), 894 | 'local-address': str(target_local_address), 895 | 'single-table': args.single_table, 896 | } 897 | if args.license_file: 898 | conf['target']['license_file'] = args.license_file 899 | 900 | if args.target_config_file: 901 | conf['target']['config_path'] = args.target_config_file 902 | 903 | if filter_test: 904 | conf['target']['filter_test'] = filter_test 905 | print(f"FILTERING: {filter_test}") 906 | 907 | conf['monitor'] = { 908 | 'as': 1001, 909 | 'router-id': str(monitor_router_id), 910 | 'local-address': str(monitor_local_address), 911 | 'check-points': [prefix * neighbor_num], 912 | } 913 | 914 | mrt_injector = None 915 | if tester_type == 'gobgp' or tester_type == 'bgpdump2': 916 | mrt_injector = tester_type 917 | 918 | 919 | if mrt_injector: 920 | conf['monitor']['check-points'] = [prefix] 921 | 922 | if mrt_injector == 'gobgp': #gobgp doesn't send everything with mrt 923 | conf['monitor']['check-points'][0] = int(conf['monitor']['check-points'][0] * 0.93) 924 | else: #args.target == 'bird': # bird seems to reject severalhandfuls of routes 925 | conf['monitor']['check-points'][0] = int(conf['monitor']['check-points'][0] * 0.99) 926 | 927 | it = netaddr.iter_iprange('90.0.0.0', '100.0.0.0') 928 | 929 | conf['policy'] = {} 930 | 931 | assignment = [] 932 | 933 | if prefix_list > 0: 934 | name = 'p1' 935 | conf['policy'][name] = { 936 | 'match': [{ 937 | 'type': 'prefix', 938 | 'value': list('{0}/32'.format(ip) for ip in islice(it, prefix_list)), 939 | }], 940 | } 941 | assignment.append(name) 942 | 943 | if as_path_list > 0: 944 | name = 'p2' 945 | conf['policy'][name] = { 946 | 'match': [{ 947 | 'type': 'as-path', 948 | 'value': list(range(10000, 10000 + as_path_list)), 949 | }], 950 | } 951 | assignment.append(name) 952 | 953 | if community_list > 0: 954 | name = 'p3' 955 | conf['policy'][name] = { 956 | 'match': [{ 957 | 'type': 'community', 958 | 'value': list('{0}:{1}'.format(int(i/(1<<16)), i%(1<<16)) for i in range(community_list)), 959 | }], 960 | } 961 | assignment.append(name) 962 | 963 | if ext_community_list > 0: 964 | name = 'p4' 965 | conf['policy'][name] = { 966 | 'match': [{ 967 | 'type': 'ext-community', 968 | 'value': list('rt:{0}:{1}'.format(int(i/(1<<16)), i%(1<<16)) for i in range(ext_community_list)), 969 | }], 970 | } 971 | assignment.append(name) 972 | 973 | neighbors = {} 974 | configured_neighbors_cnt = 0 975 | for i in range(3, neighbor_num+3+2): 976 | if configured_neighbors_cnt == neighbor_num: 977 | break 978 | curr_ip = local_address_prefix.ip + i 979 | if curr_ip in [target_local_address, monitor_local_address]: 980 | print(('skipping tester\'s neighbor with IP {} because it collides with target or monitor'.format(curr_ip))) 981 | continue 982 | router_id = str(local_address_prefix.ip + i) 983 | neighbors[router_id] = { 984 | 'as': 1000 + i, 985 | 'router-id': router_id, 986 | 'local-address': router_id, 987 | 'paths': '${{gen_paths({0})}}'.format(prefix), 988 | 'count': prefix, 989 | 'check-points': prefix, 990 | 'filter': { 991 | args.filter_type: assignment, 992 | }, 993 | } 994 | configured_neighbors_cnt += 1 995 | 996 | print(f"Tester Type: {tester_type}") 997 | if tester_type == 'exa' or tester_type == 'bird': 998 | conf['testers'] = [{ 999 | 'name': 'tester', 1000 | 'type': tester_type, 1001 | 'neighbors': neighbors, 1002 | }] 1003 | else: 1004 | conf['testers'] = neighbor_num*[None] 1005 | 1006 | mrt_file = args.mrt_file 1007 | if not mrt_file: 1008 | print("Need to provide an mrtfile to send") 1009 | exit(1) 1010 | for i in range(neighbor_num): 1011 | router_id = str(local_address_prefix.ip + i+3) 1012 | conf['testers'][i] = { 1013 | 'name': f'mrt-injector{i}', 1014 | 'type': 'mrt', 1015 | 'mrt_injector': mrt_injector, 1016 | 'mrt-index': i, 1017 | 'neighbors': { 1018 | router_id: { 1019 | 'as': 1000+i+3, 1020 | 'local-address': router_id, 1021 | 'router-id': router_id, 1022 | 'mrt-file': mrt_file, 1023 | 'only-best': True, 1024 | 'count': prefix, 1025 | 'check-points': int(conf['monitor']['check-points'][0]) 1026 | 1027 | } 1028 | } 1029 | } 1030 | 1031 | yaml.Dumper.ignore_aliases = lambda *args : True 1032 | return gen_mako_macro() + yaml.dump(conf, default_flow_style=False) 1033 | 1034 | 1035 | def config(args): 1036 | conf = gen_conf(args) 1037 | 1038 | with open(args.output, 'w') as f: 1039 | f.write(conf) 1040 | 1041 | def create_args_parser(main=True): 1042 | parser = ArgumentParser(description='BGP performance measuring tool') 1043 | parser.add_argument('-b', '--bench-name', default='bgperf2') 1044 | parser.add_argument('-d', '--dir', default='/tmp') 1045 | s = parser.add_subparsers() 1046 | parser_doctor = s.add_parser('doctor', help='check env') 1047 | parser_doctor.set_defaults(func=doctor) 1048 | 1049 | parser_prepare = s.add_parser('prepare', help='prepare env') 1050 | parser_prepare.add_argument('-f', '--force', action='store_true', help='build even if the container already exists') 1051 | parser_prepare.add_argument('-n', '--no-cache', action='store_true') 1052 | parser_prepare.set_defaults(func=prepare) 1053 | 1054 | parser_update = s.add_parser('update', help='rebuild bgp docker images') 1055 | parser_update.add_argument('image', choices=['exabgp', 'exabgp_mrtparse', 'gobgp', 'bird', 'frr', 'frr_c', 1056 | 'rustybgp', 'openbgp', 'flock', 'bgpdump2', 'all']) 1057 | parser_update.add_argument('-c', '--checkout', default='HEAD') 1058 | parser_update.add_argument('-n', '--no-cache', action='store_true') 1059 | parser_update.set_defaults(func=update) 1060 | 1061 | def add_gen_conf_args(parser): 1062 | parser.add_argument('-n', '--neighbor-num', default=100, type=int) 1063 | parser.add_argument('-p', '--prefix-num', default=100, type=int) 1064 | parser.add_argument('-l', '--filter-type', choices=['in', 'out'], default='in') 1065 | parser.add_argument('-a', '--as-path-list-num', default=0, type=int) 1066 | parser.add_argument('-e', '--prefix-list-num', default=0, type=int) 1067 | parser.add_argument('-c', '--community-list-num', default=0, type=int) 1068 | parser.add_argument('-x', '--ext-community-list-num', default=0, type=int) 1069 | parser.add_argument('-s', '--single-table', action='store_true') 1070 | 1071 | parser.add_argument('--target-config-file', type=str, 1072 | help='target BGP daemon\'s configuration file') 1073 | parser.add_argument('--local-address-prefix', type=str, default='10.10.0.0/16', 1074 | help='IPv4 prefix used for local addresses; default: 10.10.0.0/16') 1075 | parser.add_argument('--target-local-address', type=str, 1076 | help='IPv4 address of the target; default: the last address of the ' 1077 | 'local prefix given in --local-address-prefix') 1078 | parser.add_argument('--target-router-id', type=str, 1079 | help='target\' router ID; default: same as --target-local-address') 1080 | parser.add_argument('--monitor-local-address', type=str, 1081 | help='IPv4 address of the monitor; default: the second address of the ' 1082 | 'local prefix given in --local-address-prefix') 1083 | parser.add_argument('--monitor-router-id', type=str, 1084 | help='monitor\' router ID; default: same as --monitor-local-address') 1085 | parser.add_argument('--filter_test', choices=['transit', 'ixp'], default=None) 1086 | 1087 | parser_bench = s.add_parser('bench', help='run benchmarks') 1088 | parser_bench.add_argument('-t', '--target', choices=['gobgp', 'bird', 'frr_c', 'rustybgp', 1089 | 'openbgp', 'flock', 'srlinux', 'junos', 'eos'], default='bird') 1090 | parser_bench.add_argument('-i', '--image', help='specify custom docker image') 1091 | parser_bench.add_argument('--mrt-file', type=str, 1092 | help='mrt file, requires absolute path') 1093 | parser_bench.add_argument('--license_file', type=str, help='filename of license necesary for EOS', default=None) 1094 | parser_bench.add_argument('-g', '--tester-type', choices=['exa', 'bird', 'gobgp', 'bgpdump2'], default='bird') 1095 | parser_bench.add_argument('--docker-network-name', help='Docker network name; this is the name given by \'docker network ls\'') 1096 | parser_bench.add_argument('--bridge-name', help='Linux bridge name of the ' 1097 | 'interface corresponding to the Docker network; ' 1098 | 'use this argument only if bgperf can\'t ' 1099 | 'determine the Linux bridge name starting from ' 1100 | 'the Docker network name in case of tests of ' 1101 | 'remote targets.') 1102 | parser_bench.add_argument('-r', '--repeat', action='store_true', help='use existing tester/monitor container') 1103 | parser_bench.add_argument('-f', '--file', metavar='CONFIG_FILE') 1104 | parser_bench.add_argument('-o', '--output', metavar='STAT_FILE') 1105 | add_gen_conf_args(parser_bench) 1106 | parser_bench.set_defaults(func=bench) 1107 | 1108 | parser_config = s.add_parser('config', help='generate config') 1109 | parser_config.add_argument('-o', '--output', default='bgperf.yml', type=str) 1110 | add_gen_conf_args(parser_config) 1111 | parser_config.set_defaults(func=config) 1112 | 1113 | parser_batch = s.add_parser('batch', help='run batch benchmarks') 1114 | parser_batch.add_argument('-c', '--batch_config', type=str, help='batch config file') 1115 | parser_batch.set_defaults(func=batch) 1116 | 1117 | return parser 1118 | 1119 | if __name__ == '__main__': 1120 | 1121 | parser = create_args_parser() 1122 | 1123 | args = parser.parse_args() 1124 | 1125 | try: 1126 | func = args.func 1127 | except AttributeError: 1128 | parser.error("too few arguments") 1129 | args.func(args) 1130 | --------------------------------------------------------------------------------