mirror of
https://github.com/jmcorgan/fips.git
synced 2026-07-30 19:46:15 +00:00
Two concurrent ci-local runs could not both run chaos. Each child's subnet came from its position in the suite list, so both runs walked 10.30.0 through 10.30.12 and requested identical ranges; whichever reached the daemon first won and the other died with a pool overlap. A run index or hashed offset would only make that unlikely, and it is precisely the failure being removed. The simulator now claims its range: attempt-create on a candidate, advance on docker's own overlap error, fail loudly on anything else. Docker's address pool becomes the arbiter, so an overlap is impossible rather than improbable. The claim lives in the sim rather than in ci-local because only the process that creates the network can advance on conflict, and because it fixes the bare chaos.sh path too, which a ci-local-only fix would have left broken. The generated compose now declares the network external over the claimed one, and teardown releases the range from both paths, including the setup-failed path where a run that fell over after claiming still holds one. Ordering matters here and is not obvious: node IPs derive from the subnet during topology generation, and traffic shaping keys its filters on those addresses, so the claim happens before the topology exists. Claiming later would give a network on one range and filters on another, which does not fail at bring-up and instead leaves the shaping matching nothing. --subnet survives as an explicit pin for when a known range is wanted, and now fails loudly if that range is taken rather than silently overlapping. ci-local no longer passes it. Validated by running two instances of the same scenario deliberately concurrently: they claimed 10.30.1.0/24 and 10.30.0.0/24, both exited 0 with their assertions passing, and both released their networks. The negative control holds too: with a squatter on a pinned range the run aborts with a message naming the range rather than proceeding.
99 lines
2.7 KiB
Python
99 lines
2.7 KiB
Python
"""Generate docker-compose.yml for a simulation topology."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
|
|
from jinja2 import Template
|
|
|
|
from .scenario import Scenario
|
|
from .topology import SimTopology
|
|
|
|
# Image name for the pre-built FIPS test image.
|
|
# The runner builds this once before starting containers.
|
|
FIPS_SIM_IMAGE = "fips-test:latest"
|
|
|
|
# Jinja2 template for the compose file.
|
|
# Uses a pre-built image instead of per-service build to support large topologies.
|
|
_COMPOSE_TEMPLATE = Template(
|
|
"""\
|
|
networks:
|
|
fips-net:
|
|
# External, and created by the sim rather than by compose. The range has to
|
|
# be *claimed* -- attempt-create, advance on docker's own overlap error --
|
|
# so that two concurrent runs cannot select the same one, and only the
|
|
# process that creates the network can do that. See sim/netclaim.py.
|
|
external: true
|
|
name: {{ network_name }}
|
|
|
|
x-fips-common: &fips-common
|
|
image: {{ image }}
|
|
cap_add:
|
|
- NET_ADMIN
|
|
- NET_RAW
|
|
devices:
|
|
- /dev/net/tun:/dev/net/tun
|
|
sysctls:
|
|
- net.ipv6.conf.all.disable_ipv6=0
|
|
restart: "no"
|
|
env_file:
|
|
- ./npubs.env
|
|
environment:
|
|
- RUST_LOG={{ rust_log }}
|
|
- RUST_BACKTRACE=1
|
|
- FIPS_TEST_MODE=chaos
|
|
|
|
services:
|
|
{% for node in nodes %}
|
|
{{ node.node_id }}:
|
|
<<: *fips-common
|
|
container_name: {{ topology.container_name(node.node_id) }}
|
|
hostname: {{ node.node_id }}
|
|
volumes:
|
|
- ./{{ node.node_id }}.yaml:/etc/fips/fips.yaml:ro
|
|
- {{ resolv_conf }}:/etc/resolv.conf:ro
|
|
networks:
|
|
fips-net:
|
|
ipv4_address: {{ node.docker_ip }}
|
|
{% endfor %}
|
|
"""
|
|
)
|
|
|
|
|
|
def generate_compose(
|
|
topology: SimTopology,
|
|
scenario: Scenario,
|
|
output_dir: str,
|
|
network_name: str,
|
|
) -> str:
|
|
"""Render docker-compose.yml and write to output_dir. Returns the file path.
|
|
|
|
``network_name`` is the docker network the sim has already claimed; the
|
|
compose file refers to it as external rather than declaring a subnet, so
|
|
that the claim and the creation are the same operation.
|
|
"""
|
|
os.makedirs(output_dir, exist_ok=True)
|
|
|
|
nodes = [topology.nodes[nid] for nid in sorted(topology.nodes)]
|
|
|
|
# Absolute path to the shared resolv.conf (bind-mounted into containers
|
|
# so Docker's runtime resolv.conf generation doesn't overwrite it).
|
|
resolv_conf = os.path.normpath(
|
|
os.path.join(os.path.dirname(__file__), "..", "..", "docker", "resolv.conf")
|
|
)
|
|
|
|
content = _COMPOSE_TEMPLATE.render(
|
|
network_name=network_name,
|
|
rust_log=scenario.logging.rust_log,
|
|
image=FIPS_SIM_IMAGE,
|
|
nodes=nodes,
|
|
resolv_conf=resolv_conf,
|
|
topology=topology,
|
|
)
|
|
|
|
path = os.path.join(output_dir, "docker-compose.yml")
|
|
with open(path, "w") as f:
|
|
f.write(content)
|
|
|
|
return path
|