mirror of
https://github.com/jmcorgan/fips.git
synced 2026-08-12 09:33:23 +00:00
Cost-based parent selection: - Replace depth-only parent selection with effective_depth = depth + link_cost - link_cost computed from locally measured MMP metrics: etx * (1.0 + srtt_ms / 100.0) - Prevents bottleneck subtrees in heterogeneous networks where a LoRa link at depth 1 would otherwise always beat fiber at depth 2 - Configurable hysteresis (default 0.2) prevents marginal parent switches - Configurable hold-down timer (default 30s) suppresses re-evaluation after parent switch - Mandatory switches (parent lost, root change) bypass both safeguards - Link costs passed as HashMap parameter to keep TreeState pure Periodic re-evaluation: - evaluate_parent() was only called on TreeAnnounce receipt or parent loss; after tree stabilization, link degradation went undetected - Added timer-based re-evaluation (reeval_interval_secs, default 60s) that calls evaluate_parent() from the tick handler with current MMP link costs - Respects existing hold-down and hysteresis safeguards - Short-circuits when disabled or <2 peers Design documentation: - Update 7 design docs to reflect cost-based parent selection - Replace depth-only algorithm descriptions with effective_depth model - Replace rejected cumulative path cost spec with local-only design rationale - Rewrite Example 2 (heterogeneous links) for local-only cost model - Update config docs: parent_switch_threshold replaced by parent_hysteresis, hold_down_secs, reeval_interval_secs Chaos simulation enhancements: - fips_overrides with deep merge for per-scenario FIPS config customization - Explicit topology algorithm for deterministic test graphs - Control socket querying via fipsctl for tree/MMP snapshot collection - Edge existence validation in netem manager - Per-link netem policy overrides - 9 new chaos scenarios covering cost avoidance, depth-vs-cost tradeoffs, stability, mixed topologies, periodic re-evaluation, and bottleneck parent 12 new unit tests, 667 total passing, clippy clean.
113 lines
3.4 KiB
Python
113 lines
3.4 KiB
Python
"""FIPS node config generation from template + topology."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from copy import deepcopy
|
|
|
|
import yaml
|
|
|
|
from .topology import SimTopology
|
|
|
|
|
|
def _deep_merge(base: dict, override: dict) -> dict:
|
|
"""Recursively merge override into base (override wins on conflicts)."""
|
|
result = deepcopy(base)
|
|
for key, value in override.items():
|
|
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
|
|
result[key] = _deep_merge(result[key], value)
|
|
else:
|
|
result[key] = deepcopy(value)
|
|
return result
|
|
|
|
# Path to the shared node config template
|
|
_TEMPLATE_PATH = os.path.join(
|
|
os.path.dirname(__file__), "..", "configs", "node.template.yaml"
|
|
)
|
|
|
|
|
|
def _load_template() -> str:
|
|
with open(_TEMPLATE_PATH) as f:
|
|
return f.read()
|
|
|
|
|
|
def generate_peers_block(
|
|
topology: SimTopology, node_id: str, outbound_peers: list[str]
|
|
) -> str:
|
|
"""Generate the YAML peers block for a node.
|
|
|
|
Only includes peers that this node is responsible for connecting to
|
|
(outbound direction). The link is still bidirectional once established.
|
|
"""
|
|
if not outbound_peers:
|
|
return " []"
|
|
|
|
lines = []
|
|
for peer_id in sorted(outbound_peers):
|
|
peer = topology.nodes[peer_id]
|
|
lines.append(f' - npub: "{peer.npub}"')
|
|
lines.append(f' alias: "{peer_id}"')
|
|
lines.append(f" addresses:")
|
|
lines.append(f" - transport: udp")
|
|
lines.append(f' addr: "{peer.docker_ip}:4000"')
|
|
lines.append(f" connect_policy: auto_connect")
|
|
return "\n".join(lines)
|
|
|
|
|
|
def generate_node_config(
|
|
topology: SimTopology,
|
|
node_id: str,
|
|
outbound_peers: list[str],
|
|
fips_overrides: dict | None = None,
|
|
) -> str:
|
|
"""Generate a complete FIPS config YAML for one node."""
|
|
template = _load_template()
|
|
node = topology.nodes[node_id]
|
|
peers_yaml = generate_peers_block(topology, node_id, outbound_peers)
|
|
|
|
config = template
|
|
config = config.replace("{{NODE_NAME}}", node_id.upper())
|
|
config = config.replace("{{TOPOLOGY}}", "sim")
|
|
config = config.replace("{{NPUB}}", node.npub)
|
|
config = config.replace("{{NSEC}}", node.nsec)
|
|
config = config.replace("{{PEERS}}", peers_yaml)
|
|
|
|
if fips_overrides:
|
|
parsed = yaml.safe_load(config)
|
|
merged = _deep_merge(parsed, fips_overrides)
|
|
config = yaml.dump(merged, default_flow_style=False, sort_keys=False)
|
|
|
|
return config
|
|
|
|
|
|
def generate_npubs_env(topology: SimTopology) -> str:
|
|
"""Generate npubs.env content mapping NPUB_<ID>=<npub> for all nodes."""
|
|
lines = []
|
|
for node_id in sorted(topology.nodes):
|
|
node = topology.nodes[node_id]
|
|
env_name = f"NPUB_{node_id.upper()}"
|
|
lines.append(f"{env_name}={node.npub}")
|
|
return "\n".join(lines) + "\n"
|
|
|
|
|
|
def write_configs(
|
|
topology: SimTopology,
|
|
output_dir: str,
|
|
fips_overrides: dict | None = None,
|
|
):
|
|
"""Write all node configs and npubs.env to the output directory."""
|
|
os.makedirs(output_dir, exist_ok=True)
|
|
|
|
outbound = topology.directed_outbound()
|
|
for node_id in topology.nodes:
|
|
config = generate_node_config(
|
|
topology, node_id, outbound[node_id], fips_overrides
|
|
)
|
|
path = os.path.join(output_dir, f"{node_id}.yaml")
|
|
with open(path, "w") as f:
|
|
f.write(config)
|
|
|
|
env_path = os.path.join(output_dir, "npubs.env")
|
|
with open(env_path, "w") as f:
|
|
f.write(generate_npubs_env(topology))
|