Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions examples/internet/b62_protocol_parama/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Protocol Parameter Tuning

This example is based on `B00_mini_internet`, but it keeps protocol timer
configuration explicit so users can see and adjust the new APIs.

The topology is the same mini Internet shape as B00: transit ASes, route-server
peerings, private peerings, and stub ASes. The difference is in
`protocol_parama.py`:

```python
ebgp = Ebgp().setTimers(holdTime=36000, keepaliveTime=60)
ibgp = Ibgp().setTimers(holdTime=36000, keepaliveTime=60)
ospf = Ospf().setTimers(tick=1, hello=1, dead=4)
```

## Protocol Parameters

`Ospf().setTimers(tick=..., hello=..., dead=...)`

- `tick`: BIRD OSPF scheduler tick interval, in seconds.
- `hello`: OSPF hello interval, in seconds.
- `dead`: OSPF dead interval, in seconds.
- Default values in SeedEMU are `tick=1`, `hello=1`, and `dead=4`.
- For BIRD, all three values are rendered. For FRR, `hello` and `dead` are
rendered because FRR does not expose a matching OSPF `tick` command.

`Ebgp().setTimers(holdTime=..., keepaliveTime=...)`

- `holdTime`: BGP hold time, in seconds.
- `keepaliveTime`: BGP keepalive interval, in seconds.
- Default values in SeedEMU are `holdTime=36000` and `keepaliveTime=60`.

`Ibgp().setTimers(holdTime=..., keepaliveTime=...)`

- Controls the same BGP hold and keepalive timers for iBGP sessions.
- Default values in SeedEMU are `holdTime=36000` and `keepaliveTime=60`.

If these APIs are not called, SeedEMU uses the defaults above. Invalid timer
values, such as non-positive values or `keepaliveTime >= holdTime`, raise an
error during rendering.

## Build And Run

From the repository root:

```sh
python3 seedemu/testing/cli.py clean examples/internet/b62_protocol_parama/example.yaml --artifact-dir ci-artifacts/b62
python3 seedemu/testing/cli.py compile examples/internet/b62_protocol_parama/example.yaml --artifact-dir ci-artifacts/b62
python3 seedemu/testing/cli.py build examples/internet/b62_protocol_parama/example.yaml --artifact-dir ci-artifacts/b62
python3 seedemu/testing/cli.py up examples/internet/b62_protocol_parama/example.yaml --artifact-dir ci-artifacts/b62
python3 seedemu/testing/cli.py probe examples/internet/b62_protocol_parama/example.yaml --artifact-dir ci-artifacts/b62
python3 seedemu/testing/cli.py test examples/internet/b62_protocol_parama/example.yaml --artifact-dir ci-artifacts/b62
python3 seedemu/testing/cli.py down examples/internet/b62_protocol_parama/example.yaml --artifact-dir ci-artifacts/b62
```

The full lifecycle can also be run with:

```sh
python3 seedemu/testing/cli.py all examples/internet/b62_protocol_parama/example.yaml --artifact-dir ci-artifacts/b62
```

## Failure And Recovery Control

After the emulation is running, use `fault_control.py` to stop or recover one
or more services by Docker Compose service name.

Stop multiple containers:

```sh
python3 examples/internet/b62_protocol_parama/fault_control.py down --container brdnode_2_r100,brdnode_2_r101
```

The same command also accepts space-separated names:

```sh
python3 examples/internet/b62_protocol_parama/fault_control.py down --container brdnode_2_r100 brdnode_2_r101
```

Recover them:

```sh
python3 examples/internet/b62_protocol_parama/fault_control.py up --container brdnode_2_r100,brdnode_2_r101
```

List generated Compose services:

```sh
python3 examples/internet/b62_protocol_parama/fault_control.py list
```

By default, the script uses `output/docker-compose.yml` inside this example
directory. Use `--compose PATH` to point it at another Compose file.
81 changes: 81 additions & 0 deletions examples/internet/b62_protocol_parama/example.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
id: internet-b62-protocol-parama
name: Protocol Parameter Tuning
description: Mini Internet topology that demonstrates OSPF and BGP timer APIs plus manual failure and recovery control.
runner: internet
script: protocol_parama.py
platform: amd
features:
- ipv4-default
- mini-internet
- protocol-parameters
- reconvergence
- ebgp-route-server
- ebgp-private-peering
- ibgp
- ospf

compile:
enabled: true
output: output
clean:
- output
expected:
- output/docker-compose.yml
timeout: 600

build:
enabled: true
timeout: 1800

runtime:
compose: output/docker-compose.yml
readiness:
- name: representative B62 services are running
type: compose-ps
services:
- brdnode_2_r100
- brdnode_2_r101
- brdnode_3_r103
- brdnode_4_r104
- brdnode_11_r102
- brdnode_12_r101
- brdnode_150_router0
- brdnode_152_router0
- brdnode_160_router0
- brdnode_171_router0
- hnode_150_host_0
- hnode_152_host_0
- hnode_160_host_0
- hnode_171_host_0
retries: 40
interval: 3

probes:
- name: AS150 reaches AS152 across tier-1 and tier-2 transit
type: ping
service: hnode_150_host_0
target: 10.152.0.71
count: 3
retries: 40
interval: 5

- name: AS150 reaches AS160 through AS3
type: ping
service: hnode_150_host_0
target: 10.160.0.71
count: 3
retries: 40
interval: 5

- name: AS171 reaches AS154 customized host
type: ping
service: hnode_171_host_0
target: 10.154.0.129
count: 3
retries: 40
interval: 5

test_programs:
- name: B62 custom runtime validation
script: test_runtime.py
timeout: 240
122 changes: 122 additions & 0 deletions examples/internet/b62_protocol_parama/fault_control.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
#!/usr/bin/env python3

from __future__ import annotations

import argparse
import subprocess
import sys
from pathlib import Path
from typing import Iterable, List, Sequence

import yaml


SCRIPT_DIR = Path(__file__).resolve().parent
DEFAULT_COMPOSE_FILE = SCRIPT_DIR / "output" / "docker-compose.yml"
SEED_ASN_LABEL = "org.seedsecuritylabs.seedemu.meta.asn"


def docker_compose_command() -> List[str]:
try:
result = subprocess.run(
["docker", "compose", "version"],
text=True,
capture_output=True,
check=False,
)
if result.returncode == 0:
return ["docker", "compose"]
except FileNotFoundError:
pass
return ["docker-compose"]


def parse_container_names(values: Iterable[str]) -> List[str]:
names: List[str] = []
for value in values:
for item in str(value).split(","):
item = item.strip()
if item:
names.append(item)
return names


def load_services(compose_file: Path) -> List[str]:
with compose_file.open("r", encoding="utf-8") as handle:
compose = yaml.safe_load(handle) or {}
names: List[str] = []
for name, service in compose.get("services", {}).items():
labels = service.get("labels", {}) or {}
if SEED_ASN_LABEL in labels:
names.append(str(name))
return sorted(names)


def validate_services(compose_file: Path, containers: Sequence[str]) -> None:
services = set(load_services(compose_file))
missing = [name for name in containers if name not in services]
if missing:
raise SystemExit(
"unknown compose service(s): {}. Run `list` to inspect valid names.".format(
", ".join(missing)
)
)


def run_compose(compose_file: Path, args: Sequence[str]) -> int:
cmd = docker_compose_command() + ["-f", str(compose_file)] + list(args)
print("running: {}".format(" ".join(cmd)))
result = subprocess.run(
cmd,
cwd=str(compose_file.parent),
text=True,
check=False,
)
return int(result.returncode)


def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Stop or recover B62 protocol-parameter example containers."
)
parser.add_argument("action", choices=["down", "up", "list", "status"])
parser.add_argument(
"--compose",
type=Path,
default=DEFAULT_COMPOSE_FILE,
help="Path to docker-compose.yml. Defaults to this example's output directory.",
)
parser.add_argument(
"--container",
nargs="*",
default=[],
help="One or more Compose services. Comma-separated and space-separated forms are both supported.",
)
return parser.parse_args(argv)


def main(argv: Sequence[str] | None = None) -> int:
args = parse_args(argv)
compose_file = args.compose.resolve()
if not compose_file.exists():
raise SystemExit("compose file not found: {}".format(compose_file))

if args.action == "list":
for service in load_services(compose_file):
print(service)
return 0

containers = parse_container_names(args.container)
if not containers:
raise SystemExit("--container is required for `{}`".format(args.action))
validate_services(compose_file, containers)

if args.action == "down":
return run_compose(compose_file, ["stop"] + containers)
if args.action == "up":
return run_compose(compose_file, ["up", "-d", "--no-deps"] + containers)
return run_compose(compose_file, ["ps"] + containers)


if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))
Loading
Loading