├── .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 | 
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 | 
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 | 
327 |
328 | 
329 |
330 | 
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 |
--------------------------------------------------------------------------------