Skip to content

Peer templates: share policy chains and BGP options across many peers - #21

Open
floreabogdan wants to merge 6 commits into
mainfrom
feat/peer-templates
Open

Peer templates: share policy chains and BGP options across many peers#21
floreabogdan wants to merge 6 commits into
mainfrom
feat/peer-templates

Conversation

@floreabogdan

@floreabogdan floreabogdan commented Aug 23, 2026

Copy link
Copy Markdown
Owner

What and why

Fixes #20.

At an IX you can have thirty peers that need the same import/export chains and the same BGP options; today each is set by hand and a policy change is repeated thirty times. This adds peer templates — Cisco's peer groups, fitted to how birdy already works.

A template is a peer without an identity: the role, policy chains, import limit, transport safeguards and export transforms a kind of session should have, kept once under Peers → Templates. Peers link to it and take that shape; save the template and every linked peer is rewritten in one transaction, then reviewed on the Changes page behind bird -p, lint and the armed auto-revert like any other edit. Each peer keeps its own name, neighbor, AS, password and the enabled/drain switches.

The split is exactly the one Clone a peer already makes (and the peer_templates table PLAN.md designed and never built).

Design decisions

  • Inheritance is decided in the model and expressed in bird.conf. Linked peer rows hold a full copy of the governed columns and peer_policies; saving a template fans out inside the transaction, so the renderer, lint, previews, the policies page's "in use" counts and the delete guards keep reading plain peer rows. In the rendered config a template in use is BIRD's own template bgp NAME { … } carrying the shared session options (multihop, passive, BFD, GTSM, graceful restart, RFC 9234 role, route reflection), and each linked peer is protocol bgp X from NAME. Filters, identity and the channel stay per peer — the filters embed the peer's ASN and transforms, the channel's family follows the neighbor, and its import limit is the one override. A template nobody links to is not written. Unlinked peers render byte-for-byte as before.
  • Separate peer_templates table rather than a flag on peers — every SELECT … FROM peers (poller, dashboard, seed) stays a peer query.
  • One override allow-list column (peers.template_overrides) with the import limit as its only entry: the one thing that genuinely differs between thirty otherwise identical IX peers.

What's in the PR

  • Store (schema v38): peer_templates, template_policies, peers.template_id (nullable FK, RESTRICT), peers.template_overrides. PeerTemplate.ApplyTo / TemplateFromPeer, fan-out on UpdatePeerTemplate, LinkPeerToTemplate / DetachPeer, delete guards that name what stands in the way. Peer.Validate's shape checks move to a shared validateShape. A reflection test fails whenever a peer field is added without being classified as identity or shape.
  • Render: template bgp blocks (filed as birdy.d/08-templates-*.conf, ahead of the peers), from declarations, fallbacks for unused/missing templates; the per-peer preview is assembled from sections (template block first).
  • Templates UI: /peers/templates list with chains, limit, safeguard chips and usage; an editor that reuses the peer form minus the identity card, previews on a sample neighbor and states how many peers a save rewrites. On the peer form a Template picker fills and greys the governed controls ("Set by the template"); None detaches and leaves the values in place. The server re-applies the template on every save and preview. Save as template captures an existing peer (optionally linking it); Add peer on a template row starts a session already linked; the peers list carries a chip and a ?template= filter.
  • Import-limit override, bulk attach/detach from the peers list, a template picker on Import from BIRD (adopted sessions arrive with chains), and lint folding (IX_PEERS (30 peers)).
  • Guard: peer names new, seed, preview, templates, attach are refused.
  • Fix (pre-existing): the peer form's other-role checkbox group was visible greyed out instead of hidden.
  • Docs: USAGE §7 "Peer templates", README bullet + two screenshots (from the TestPreview scaffold, documentation ranges), CHANGELOG, PLAN.md note.

Verified end to end on real BIRD

Docker lab (alpine:3.20 bird 2.15.1, the same package the image bundles): two birdy-managed routers and four plain BIRD peers on one bridge.

  • Adopt a hand-written router with four live sessions → Import from BIRD onto two templates → apply/confirm: bird -p accepts the templated config; the three IX sessions do not flap; routes flow both ways; on disk 08-templates-IX_PEERS.conf and protocol bgp rs1 from IX_PEERS.
  • Template edit fans out: prepend ×2 on IX_PEERS → all three peers see 65551 65551 65551, transit untouched.
  • Override: rs2 keeps limit 1/warn through a template save; BIRD reports Import limit: 1 [HIT] / warn while rs1 keeps 100.
  • Bulk detach/attach with applies in between — the session stays Established.
  • Lint folding on Changes: IX_PEERS (2 peers) No import limit… (the overridden peer stays out of the fold).
  • Safety net: GTSM on the template (peers don't send TTL 255) → the three IX sessions sit in OpenSent until rollback restores them; restoring the template then reads "Already applied: the running config already matches".
  • Second router under its own birdy, peered via templates on both sides: RFC 9234 roles negotiated provider/customer from the two templates, the customer prefix tagged FROM_CUSTOMER and re-exported to the IX peers, the full table down to the customer; A observes B through the multi-instance dashboard.

Checklist

  • gofmt -l . prints nothing, go vet ./... and golangci-lint run pass
  • go test ./... passes; behavior changes come with tests (no C toolchain on the dev box for -race; CI runs it)
  • Fits birdy's opinionated, single-router scope (or was discussed in an issue first)
  • No new dependency, or the PR justifies it
  • No secrets logged or rendered to the browser

https://claude.ai/code/session_01C7cpvZpZxb2GYEj6jBHKQX

A peer template is a peer without an identity: role, policy chains, import
limit, transport safeguards and export transforms, captured once and linked
from many peers. Linked peers carry a full copy of the governed columns and
a template save rewrites every one of them in the same transaction, so the
renderer, the linter and every "in use" count keep reading plain peer rows.

Schema version 38: peer_templates and template_policies, plus a nullable
template_id and a template_overrides list on peers. Existing peers are
untouched and unlinked.

- PeerTemplate.ApplyTo / TemplateFromPeer are the two halves of the split
  clone already makes; a reflection test fails when a peer field is added
  without being classified as identity or shape.
- Peer.Validate's shape checks move to validateShape so a template is held
  to exactly the rules a peer is.
- DeletePolicy also refuses while a template chains the policy; the chain
  helpers are generalised over the two chain tables.
- Peer names new/seed/preview/templates are refused: they are pages under
  /peers/ the router serves before the name wildcard.

Part of #20.

Claude-Session: https://claude.ai/code/session_01C7cpvZpZxb2GYEj6jBHKQX
Peers → Templates lists every template with its chains, limit and how many
peers link to it. The editor is the peer form with the identity card
removed; it previews the shape on a sample neighbor and says how many peers
a save rewrites. On the peer form a Template picker fills the governed
controls from the template and greys them out; choosing None detaches the
peer and leaves the values in place. The server re-applies the template on
every save and preview, so what the form posts for a governed field never
matters. "Save as template" captures an existing peer's shape and can link
that peer in the same step; "Add peer" on a template's row starts a session
already linked; the peers list carries a chip and a ?template= filter.

A linked peer's protocol block carries a comment naming its template;
unlinked peers render byte-for-byte as before.

Closes #20.

Claude-Session: https://claude.ai/code/session_01C7cpvZpZxb2GYEj6jBHKQX
…tach, lint folding

- A linked peer can keep its own import limit: "Use this peer's own import
  limit" on the form writes the importLimit override the store already
  honoured, so the one oversized IX peer needs no template of its own.
  An unlinked peer carries no overrides; Validate clears the list.
- Import from BIRD offers a template per row (plus one control to set every
  checked row). A session imported with a template arrives with its role,
  shape and chains — a plain seed never had chains, since the socket says
  nothing about what the filters were meant to do.
- The peers list gains a bulk bar: tick peers, attach them to a template or
  detach them. Store.DetachPeer keeps the values and drops the link.
- Lint folds identical findings about peers of one template into a single
  line attributed to the template; per-peer and unlinked findings stay.
- The peer form's other-role checkbox group was visible greyed out instead
  of hidden (.checks' flex beat the hidden attribute) — pre-existing, fixed.
- "attach" joins the reserved peer names.

Claude-Session: https://claude.ai/code/session_01C7cpvZpZxb2GYEj6jBHKQX
A template at least one peer links to now renders as a template bgp block
carrying the session options its peers share — multihop, passive, BFD,
GTSM, graceful restart, the RFC 9234 role, route reflection — and each
linked peer is declared "protocol bgp X from TEMPLATE". What stays in the
peer's block is everything peers differ in: neighbor, password, the
filters (they embed the peer's own ASN and transforms) and the channel,
whose family follows the neighbor and whose import limit is the one thing
a peer may override. A template nobody links to is not written, so
creating one changes nothing until a peer uses it; a linked peer whose
template is missing from the input renders in full. Unlinked peers are
byte-for-byte unchanged.

The per-peer preview is assembled from sections now (the template block
first, then the peer's own), which also retires the marker-based slicing
that skipped iBGP filters. Template files sit with the policies in
birdy.d/ (08-templates-*.conf), ahead of the peers that inherit them.

Verified against BIRD 2.15.1 in a Docker lab: bird -p accepts the output,
an apply over four live sessions caused no flap, template edits fan out
to every linked peer on the daemon, and the armed revert restores a
template change that breaks its sessions.

Claude-Session: https://claude.ai/code/session_01C7cpvZpZxb2GYEj6jBHKQX
- Templates list shows each template's safeguards as chips (first AS,
  origin only, RFC 9234, GTSM, BFD, passive, multihop, prepend).
- A linked peer's Role says "Set by the template"; the template editor's
  "N peers linked" note is informational rather than a warning, its lint
  findings are attributed to the template, and its chain headings speak
  of "these peers".
- README gains a screenshot row (templates list, linked peer), USAGE and
  the CHANGELOG describe the native template bgp rendering, and the
  TestPreview scaffold renders the new pages from fixture data.

Claude-Session: https://claude.ai/code/session_01C7cpvZpZxb2GYEj6jBHKQX
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

BGP Templates

1 participant