# CHIMERA architecture & codebase plan Status: design document. No code yet. This file defines the repo layout, the data model, and the engine behavior we build toward. ## Design principles 1. **Desired state lives in git, never in the router.** A small YAML inventory describes each RR *cluster* (cluster-id, routers, peer-groups, peers). CHIMERA compiles it into per-RR deltas. Archived configs in `CiscoConfigs/` are only an *import source*, not the runtime store. 2. **Never wholesale-replace `frr.conf`.** RRs carry hand-tuned OSPF, interface and system stanzas. CHIMERA only emits targeted `configure terminal` commands (vtysh), then `write memory` per box. Whole-file templating is a v3 maybe, gated by a `file-mode` flag, not the default. 3. **Peer-groups are first-class.** All bulk operations (add/modify a group across *all* RRs of a cluster) are peer-group-scoped; FRR propagates group attributes to members, and per-peer overrides win over group settings. Single-peer edits only when explicitly asked. 4. **Mesh-aware generation.** The RR-PEERS group is derived per-RR by subtracting self; the inventory lists the cluster's RR loopbacks once and each RR's neighbor list is generated (this is why the four MUYU/FOX/WOLF/… files are near-duplicates with self-exclusion). 5. **Plan → diff → apply, always.** Every mutating command renders a delta first (`--diff` is default output). `apply` takes the same plan object. 6. **Drift detection is a first-class verb** (`status`), not an aftercare: parse live `vtysh -c "show running-config bgpd"` against inventory. 7. **Serial, per-cluster rollout with a canary.** Apply to one RR first, verify, then the rest. Most peer-group attribute changes don't tear sessions down; anything that *would* reset sessions (remote-as change, password, shutdown, un-resourced peer-group deletion) gets flagged in the plan with a loud warning. ## Execution model (FRR on Debian/LXC) Transport: SSH (paramiko or plain `ssh` subprocess) → `vtysh -c "show running-config bgpd"` for reads (integrated-vtysh-config is turned ON on every RR, so this is literally `/etc/frr/frr.conf` in bgpd view). Apply = one batch of `vtysh -c` commands per box: ``` vtysh -c "configure terminal" -c "router bgp 64572" -c "address-family l2vpn evpn" -c "neighbor CLIENTS send-community both" vtysh -c "configure terminal" ... # FRR's integrated config applies atomically per invocation vtysh -c "write memory" ``` Command sequencing rules the compiler must respect (from the FRR 10.x bgpd docs): - `neighbor peer-group` and `neighbor remote-as` before attaching any member to the group. - Global BGP params (`bgp cluster-id`, `bgp router-id`) live under `router bgp `, not under AF sub-modes. - AF-scoped ops (`route-reflector-client`, `activate`, `send-community`) require entering the right `address-family` block first. - Deleting an AF block with `no ...` implicitly kills its neighbor/AF lines — plan must expand that into explicit removals when diffing, so dry-run output is truthful. - Configs contain duplicated/merged AF stanzas (see BGP-NETWORK.md quirks): the parser normalizes them, the writer emits canonical single-block form. First apply of a normalize-only diff is a safe no-op-semantics change we can land quietly. Rollback: before touching a box, `vtysh -c "show running-config bgpd"` → `/var/backups/chimera//.conf` plus `cp /etc/frr/frr.conf` (root or `sudo -n`); `chimera rollback [--from ]` re-applies the snapshot through the same vtysh path. Optional per-box pre/post hooks (`hooks:` in inventory) to run `systemctl reload frr`-style recovery, though normally unnecessary. ## Data model Inventory YAML (one file per cluster, e.g. `inventory/hq-evpn.yml`): ```yaml cluster: name: hq-evpn as: 64572 cluster_id: 6.7.6.7 # keep notation as configured address_family_default: [ipv4_unicast, l2vpn_evpn] routers: - { name: MUYU-RR, ssh: muyu-rr.hq.panchain.org, loopback: 10.255.254.246 } - { name: FOX-RR, ssh: fox-rr.hq.panchain.org, loopback: 10.255.254.247 } - { name: WOLF-RR, ssh: wolf-rr.hq.panchain.org, loopback: 10.255.254.245 } - { name: NYATER, ssh: nyater.hq.panchain.org, loopback: 10.255.254.255, role: rr+switch } peer_groups: RR-PEERS: remote_as: 64572 update_source: lo af: ipv4_unicast: { activate: true, send_community: both } l2vpn_evpn: { activate: true, send_community: both } CLIENTS: remote_as: 64572 update_source: lo af: ipv4_unicast: { activate: true, send_community: both, route_reflector_client: true } l2vpn_evpn: { activate: true, send_community: both, route_reflector_client: true } peers: # RR-PEERS members: declared once; generated per-RR minus self - { group: RR-PEERS, loopback_ref: [MUYU-RR, FOX-RR, WOLF-RR, NYATER], mesh: true } # clients: - { group: CLIENTS, ip: 10.255.254.250, desc: URAKAZE } - { group: CLIENTS, ip: 10.255.254.234, desc: VICTORIA } ... ``` Core objects (Python, `attrs`/`dataclasses`): - `RouterReflector` — host, ssh target, loopback, role (rr | rr-switch), parsed snapshot of its running bgp config. - `BgpCluster` — AS, cluster_id, list of RRs, peer-groups, peers; owns the `validate()` rules (cluster_id must be identical on every RR; RRs never in an RCC group; AS consistency). - `PeerGroup` — name, attrs (remote_as, update_source, password, timers…), per-AF settings (`activate`, `send_community`, `route_reflector_client`, `allowas_in`, route-maps, … as encountered). - `Peer` — ip, group membership, per-peer overrides dict. - `RunningBgpConfig` — normalized parse of one RR's bgpd view (`router bgp` → globals → peer-groups → peers → AF blocks). - `Delta` — ordered list of `BgpCommand`s with `resets_session: bool` flag. `chimera.cli` and the future web layer are **thin callers of the core**; the core must not import anything from cli/web (so a `chimera.core` import in a notebook works too). ## Repo layout ``` CHIMERA/ pyproject.toml # package = chimera; console_script = chimera src/chimera/ core/ models.py # dataclasses above inventory.py # YAML load/validate (pydantic or jsonschema) normalize.py # parse quirks → canonical model planner.py # desired × live → Delta; sequencing rules commands.py # BgpCommand rendering (vtysh arg list) transport/ ssh.py # paramiko/openssh subprocess runner frr.py # vtysh read/apply/write/rollback vyos.py # (later) set-syntax provider for VyOS transit parse/ bgp_grammar.py # line-based bgpd config parser cli/ main.py # typer app: the verbs below web/ app.py # FastAPI, same core; read-only pages first templates/ # Jinja + htmx; no SPA needed tests/ unit/ e2e/ inventory/ # git-tracked desired state hq-evpn.yml nekos-evpn.yml hq-transit.yml docs/ # this file, BGP-NETWORK.md, … docker/ rr-lab/ # frr:10.3 containers, 4-RR full mesh: e2e lab ``` ## CLI verbs (target surface) ``` chimera init # generate inventory stubs chimera import --live # bootstrap inventory from running RRs chimera status [cluster] # per-RR drift report: group attrs, RCC, cluster_id chimera plan peergroup edit --cluster --af l2vpn-evpn --send-community both chimera peer add 10.255.254.243 --group CLIENTS --cluster hq-evpn chimera peergroup add --cluster --remote-as 64572 ... chimera cluster set-id # all RRs; refuses until all agree chimera apply [--canary-first] [--only ] [--yes] chimera rollback [--from ] ``` Mutating verbs print the Delta by default and exit; `--yes` applies. `chimera cluster set-id` refuses to emit until `status` shows every RR reachable — changing cluster-id on a subset restarts the TCP-BFD-less RR cluster's ORF/count logic mid-reflection; we want one plan for all boxes. ## Web UI plan (phase 2, after CLI core is solid) - Pages: cluster overview (4 RRs × sessions table), drift report, desired-state editor (validated YAML), plan preview with rendered vtysh commands, apply progress log. - Auth: local/single-user token first; Tailscale-gated in practice. - Same core as CLI — no duplicated business logic. ## Testing - Unit: parser golden files — the actual MUYU/YUUKA/REIMU configs copied into `tests/parse/golden/` (they contain the weird nested-AF stanzas — regression suite for the normalizer). - Property test: model → render → re-parse == normalize(parse) (round-trip). - E2e lab: `docker/rr-lab` = 4× `frr:10.3` containers, loopback on dummy0, full-mesh RR + 2 fake clients; CHIMERA apply against them in CI. - Fake transport mode (`--transport fake`) that shells out to a stub — keeps e2e cheap without docker. ## Roadmap 1. **M0** parser + normalizer + golden tests (no SSH yet; parse the archived `.ios` files). ~ This unlocks everything else. 2. **M1** inventory + `status` (read-only drift report over SSH). 3. **M2** planner + commands + `apply`/`rollback`, canary rollout. 4. **M3** verbs: peergroup add/edit, peer add, cluster set-id. 5. **M4** web read-only dashboard; apply via UI with plan-preview gate. 6. **M5** VyOS provider (HQ transit NOZOMI/SHUN) after FRR flow is proven — the transit RR quirk (commented-out RCC) is a `status` warning fixture. ## Open questions - Which FRR version is on the live RRs exactly? Repo headers say `frr version 10.3`; docs target 10.7.1 — verify with `vtysh -c "show version"` during M1, pin grammar to what runs. - HQ's 4 RR members are 3 LXC + NYATER switch: does orchestration treat NYATER's bgpd (switch platform, multicast AF) identically? Assumed yes — it speaks the same bgpd grammar. - Is there a management network/VRF for SSH to the RRs (`vrf` flag on ssh runner may be needed)? - Do we want route-map/route-reflector-set-extra-community knobs in the data model now, or keep minimal until encountered in the configs?