Files
lab/labsim/sim-ha-config.py
Michal c729275961
Some checks failed
CI/CD / lint (push) Failing after 24s
CI/CD / typecheck (push) Failing after 22s
CI/CD / test (push) Failing after 23s
CI/CD / build (push) Has been skipped
CI/CD / publish-rpm (push) Has been skipped
CI/CD / publish-deb (push) Has been skipped
labsim: add IPv6 BGP config layer for the Gateway-API public-v6 rehearsal
The sim's eBGP was IPv4-only; extend it to the v6 family so labsim can rehearse
Cilium BGP advertising a public-style v6 LoadBalancer /64 to the VyOS pair before
it touches production (approved plan: public IPv6 via Cilium BGP + Gateway API).

- sim-net-config.py: bgp6 inside bgp() -- ipv6-unicast peer-group K8S6 over
  fd00:2::11/12/13, prefix-list6 (le 128, /128 host routes), K8S-IN6/OUT6 route-maps
  (export deny -- never hand the cluster a default). ULA fd61:1e00::/64 as the sim
  LB range so nothing leaks into the real HE /48. All v6 lines pass the
  sim-net-apply.sh whitelist.
- sim-ha-config.py: VLANS6 = {2: fd00:2/64} -> routers hold fd00:2::252/253 on
  bond0.2 so they can peer the nodes over v6 (no v6 VRRP VIP; BGP peers the per-box
  address, as production).
- k8s-up.sh: nodes get fd00:2::11/12/13 + dual-stack k3s (dual node-ip, dual
  cluster/service CIDR). Peers line up with sim-net-config K8S_NODES_V6.

Config-gen verified for all three. Live chunk (Cilium v6+BGP+Gateway install,
bring-up, datapath/#26847 proof) is next.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DMVzWZgiKW2wquf5z8S1yH
2026-09-10 01:23:03 +01:00

145 lines
6.5 KiB
Python
Executable File

#!/usr/bin/env python3
"""Generate the HA config for the labsim VyOS pair.
Exists to answer one question that cannot be answered on a single router, and
that would otherwise only be discovered at cutover: with kea HA active-passive,
does exactly ONE box answer a DHCP request?
Mirrors the production shape so the answer transfers:
router1 172.31.<v>.252 priority 200 DHCP HA primary
router2 172.31.<v>.253 priority 100 DHCP HA secondary
VIP 172.31.<v>.1 (what clients use as their gateway)
Note the sim's LoT VLAN is a /23 like production, so the VIP prefix differs
there -- getting that wrong produces a config that commits and then behaves
subtly wrongly, which is worse than a failure.
./sim-ha-config.py --role primary > r1.conf
./sim-ha-config.py --role secondary > r2.conf
"""
from __future__ import annotations
import argparse
import importlib.util
import json
import os
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
MIG = os.path.join(HERE, "..", "migration")
# Reuse the DHCP/DNS generator rather than hand-writing subnets: the whole
# point is that what is proven here and what production gets share a code path.
_spec = importlib.util.spec_from_file_location(
"unifi_to_vyos", os.path.join(MIG, "unifi-to-vyos.py"))
unifi_to_vyos = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(unifi_to_vyos)
# vlan -> (prefix, cidr). LoT is a /23 in the sim, matching production.
VLANS = {
1: ("172.31.1", 24),
2: ("172.31.2", 24),
3: ("172.31.3", 24),
9: ("172.31.9", 24),
10: ("172.31.10", 23),
200: ("172.31.200", 24),
}
# VLAN 2 (k8s) also gets a ULA IPv6, so the routers can peer eBGP with the nodes
# over IPv6 (the neighbors in sim-net-config.py K8S_NODES_V6 = fd00:2::1x). Only
# VLAN 2 needs it for the BGP rehearsal; a ULA keeps sim traffic out of the real
# HE /48. Routers hold ::252 / ::253 (no v6 VRRP VIP -- BGP peers the real per-box
# address, exactly as production).
VLANS6 = {2: ("fd00:2", 64)}
DHCP_HA_NAME = "labsim-dhcp-pair" # must not equal either host-name
def group(vlan: int) -> str:
return "native" if vlan == 1 else f"vlan{vlan}"
def build(role: str) -> list[str]:
primary = role == "primary"
self_o, peer_o = (252, 253) if primary else (253, 252)
prio = 200 if primary else 100
out = [f"# labsim VyOS HA -- {role}", ""]
for vlan, (pfx, cidr) in VLANS.items():
g = group(vlan)
# EVERY VLAN is a sub-interface, Management (VLAN 1) included. Putting
# Management on the bare `bond0` is what gives the parent a subnet, and
# kea then answers tagged frames from it as well as from the correct
# sub-interface -- clients on other VLANs get offered a Management
# address (ISC Kea #1117). See NATIVE_VLAN in ovs.sh; proven by
# labsim-vlan-leak-test.sh.
iface = f"bond0 vif {vlan}"
out += [
f"# VLAN {vlan}",
# The node's own address replaces the .1 it used to hold directly;
# .1 becomes the floating VIP, exactly as production will be.
f"delete interfaces bonding {iface} address",
f"set interfaces bonding {iface} address '{pfx}.{self_o}/{cidr}'",
*([f"set interfaces bonding {iface} address '{VLANS6[vlan][0]}::{self_o}/{VLANS6[vlan][1]}'"]
if vlan in VLANS6 else []),
f"set high-availability vrrp group {g} interface bond0.{vlan}",
f"set high-availability vrrp group {g} vrid {vlan}",
f"set high-availability vrrp group {g} address {pfx}.1/{cidr}",
f"set high-availability vrrp group {g} priority {prio}",
f"set high-availability vrrp group {g} hello-source-address {pfx}.{self_o}",
f"set high-availability vrrp group {g} peer-address {pfx}.{peer_o}",
f"set high-availability vrrp group {g} no-preempt",
f"set high-availability vrrp sync-group MAIN member {g}",
"",
]
out += [
"# --- WAN follows VRRP mastership ---",
# These four hooks and the health check existed on both live sim VMs but
# in NEITHER generator, so `sim-net-apply.sh check` reported "in sync"
# while the mechanism under test was pure undetected drift -- exactly
# the failure mode this file was written to end.
#
# The check goes on the SYNC GROUP, not per group: VyOS rejects a
# per-group check while the group is in a sync group ("Only sync group
# health check will be used").
"set high-availability vrrp sync-group MAIN health-check script '/config/vrrp-wan-health'",
"set high-availability vrrp sync-group MAIN health-check interval '5'",
"set high-availability vrrp sync-group MAIN health-check failure-count '3'",
# take and release both exec vrrp-wan-reconcile: one code path, asked at
# different moments. `stop` matters as much as `backup` -- a stopped
# keepalived is a demotion too, and without it the box would keep the
# WAN while holding no VIPs.
"set high-availability vrrp sync-group MAIN transition-script master '/config/vrrp-wan-take'",
"set high-availability vrrp sync-group MAIN transition-script backup '/config/vrrp-wan-release'",
"set high-availability vrrp sync-group MAIN transition-script fault '/config/vrrp-wan-release'",
"set high-availability vrrp sync-group MAIN transition-script stop '/config/vrrp-wan-release'",
"",
"# --- DHCP high-availability ---",
"# The thing under test: active-passive should mean exactly one OFFER.",
"set service dhcp-server high-availability mode active-passive",
f"set service dhcp-server high-availability status {role}",
f"set service dhcp-server high-availability name {DHCP_HA_NAME}",
f"set service dhcp-server high-availability source-address 172.31.10.{self_o}",
f"set service dhcp-server high-availability remote 172.31.10.{peer_o}",
"",
]
inv = json.load(open(os.path.join(MIG, "export", "inventory.json")))
dhcp, stats = unifi_to_vyos.build(inv, "sim")
out += [l for l in dhcp if l.strip() and not l.startswith("#")]
print(f"{role}: {stats['subnets']} subnets, {stats['mappings']} mappings",
file=sys.stderr)
return out
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--role", choices=("primary", "secondary"), required=True)
args = ap.parse_args()
sys.stdout.write("\n".join(build(args.role)) + "\n")
return 0
if __name__ == "__main__":
sys.exit(main())