mirror of
https://github.com/jmcorgan/fips.git
synced 2026-07-30 19:46:15 +00:00
Three infrastructure bugs fixed: - DNS resolution never populated the identity cache because Docker overwrites /etc/resolv.conf at container start. Fix: bind-mount resolv.conf in the chaos compose template, matching the static and sidecar test configs. - Traffic generator used config-time npubs for iperf3 targets, but ephemeral nodes generate fresh keypairs at startup. Fix: share the peer churn manager's runtime npub cache with the traffic manager. - Log analysis had no discovery counters and used wrong patterns. Fix: add discovery section with correct log message matching. Also adds timestamped output directories (YYYYMMDD-HHMMSS-scenario), coord_ttl_secs override in maelstrom.yaml, FIPS_SIM_OUTPUT env var support, and maelstrom-sparse scenario for sparse topology testing.
174 lines
5.7 KiB
Python
174 lines
5.7 KiB
Python
"""Random iperf3 traffic generation between node pairs.
|
|
|
|
Spawns iperf3 clients as background processes in containers. The iperf3
|
|
server is already running in each container (started by the Dockerfile
|
|
entrypoint).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
import random
|
|
import time
|
|
from dataclasses import dataclass, field
|
|
|
|
from .docker_exec import docker_exec_quiet
|
|
from .scenario import TrafficConfig
|
|
from .topology import SimTopology
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
@dataclass
|
|
class TrafficSession:
|
|
client_node: str
|
|
server_node: str
|
|
started_at: float
|
|
duration_secs: int
|
|
container: str
|
|
result_file: str = ""
|
|
|
|
|
|
class TrafficManager:
|
|
"""Manages random iperf3 sessions across the mesh."""
|
|
|
|
def __init__(
|
|
self,
|
|
topology: SimTopology,
|
|
config: TrafficConfig,
|
|
rng: random.Random,
|
|
down_nodes: set[str] | None = None,
|
|
npub_cache: dict[str, str] | None = None,
|
|
):
|
|
self.topology = topology
|
|
self.config = config
|
|
self.rng = rng
|
|
self.down_nodes = down_nodes or set()
|
|
self.npub_cache = npub_cache or {}
|
|
self.active_sessions: list[TrafficSession] = []
|
|
self.completed_results: list[dict] = []
|
|
|
|
@property
|
|
def active_count(self) -> int:
|
|
return len(self.active_sessions)
|
|
|
|
def maybe_spawn(self):
|
|
"""Spawn a new iperf3 session if under the concurrency limit."""
|
|
if self.active_count >= self.config.max_concurrent:
|
|
log.debug(
|
|
"At max_concurrent (%d), skipping traffic spawn",
|
|
self.config.max_concurrent,
|
|
)
|
|
return
|
|
|
|
node_ids = [nid for nid in self.topology.nodes if nid not in self.down_nodes]
|
|
if len(node_ids) < 2:
|
|
return
|
|
|
|
# Pick random client and server (different nodes, both up)
|
|
client, server = self.rng.sample(node_ids, 2)
|
|
server_npub = self.npub_cache.get(server, self.topology.nodes[server].npub)
|
|
container = self.topology.container_name(client)
|
|
|
|
duration = int(
|
|
self.rng.uniform(
|
|
self.config.duration_secs.min,
|
|
self.config.duration_secs.max,
|
|
)
|
|
)
|
|
streams = self.config.parallel_streams
|
|
|
|
# Result file inside the container for JSON capture
|
|
ts = int(time.time())
|
|
result_file = f"/tmp/iperf3-{client}-{server}-{ts}.json"
|
|
|
|
# Start iperf3 in background with JSON output
|
|
cmd = (
|
|
f"nohup iperf3 -c {server_npub}.fips -t {duration} "
|
|
f"-P {streams} --json > {result_file} 2>&1 &"
|
|
)
|
|
result = docker_exec_quiet(container, cmd)
|
|
if result is not None:
|
|
session = TrafficSession(
|
|
client_node=client,
|
|
server_node=server,
|
|
started_at=time.time(),
|
|
duration_secs=duration,
|
|
container=container,
|
|
result_file=result_file,
|
|
)
|
|
self.active_sessions.append(session)
|
|
log.info(
|
|
"Traffic: %s -> %s (%ds, %d streams)",
|
|
client,
|
|
server,
|
|
duration,
|
|
streams,
|
|
)
|
|
else:
|
|
log.warning("Failed to start iperf3 on %s", container)
|
|
|
|
def cleanup_expired(self):
|
|
"""Remove sessions that have completed (based on time)."""
|
|
now = time.time()
|
|
grace = 5 # seconds after expected completion
|
|
still_active = []
|
|
for s in self.active_sessions:
|
|
if now - s.started_at >= s.duration_secs + grace:
|
|
self._collect_result(s)
|
|
else:
|
|
still_active.append(s)
|
|
removed = len(self.active_sessions) - len(still_active)
|
|
self.active_sessions = still_active
|
|
if removed > 0:
|
|
log.debug("Cleaned up %d expired traffic sessions", removed)
|
|
|
|
def _collect_result(self, session: TrafficSession):
|
|
"""Retrieve iperf3 JSON result from container."""
|
|
if not session.result_file:
|
|
return
|
|
if session.client_node in self.down_nodes:
|
|
return
|
|
stdout = docker_exec_quiet(
|
|
session.container,
|
|
f"cat {session.result_file} 2>/dev/null; rm -f {session.result_file}",
|
|
)
|
|
if stdout is None:
|
|
return
|
|
try:
|
|
data = json.loads(stdout.strip())
|
|
except (json.JSONDecodeError, ValueError):
|
|
log.debug("Could not parse iperf3 result for %s -> %s",
|
|
session.client_node, session.server_node)
|
|
return
|
|
data["_meta"] = {
|
|
"client": session.client_node,
|
|
"server": session.server_node,
|
|
"duration_secs": session.duration_secs,
|
|
}
|
|
self.completed_results.append(data)
|
|
|
|
def collect_results(self) -> list[dict]:
|
|
"""Return all completed iperf3 JSON results."""
|
|
# Collect any remaining active sessions that may have finished
|
|
for s in self.active_sessions:
|
|
self._collect_result(s)
|
|
return self.completed_results
|
|
|
|
def stop_all(self):
|
|
"""Kill all iperf3 client processes in running containers."""
|
|
seen = set()
|
|
for session in self.active_sessions:
|
|
if session.container not in seen:
|
|
if session.client_node not in self.down_nodes:
|
|
docker_exec_quiet(
|
|
session.container,
|
|
"killall iperf3 2>/dev/null; true",
|
|
)
|
|
seen.add(session.container)
|
|
# Collect results before clearing (iperf3 writes partial JSON on kill)
|
|
for s in self.active_sessions:
|
|
self._collect_result(s)
|
|
self.active_sessions.clear()
|