Skip to content

Instantly share code, notes, and snippets.

@vorlon001
Last active October 24, 2026 07:42
Show Gist options
  • Select an option

  • Save vorlon001/537e2b18b9540ed31c889e50905346dc to your computer and use it in GitHub Desktop.

Select an option

Save vorlon001/537e2b18b9540ed31c889e50905346dc to your computer and use it in GitHub Desktop.
EVPN L2 FRR GLM example v.5
#!/usr/bin/env bash
# gen.sh — генератор скриптов лабы EVPN-VXLAN (3 ноды FRR + netns)
# Создаёт: evpn_setup.sh, test_evpn.sh, deploy.sh, evpn_cleanup.sh
set -euo pipefail

# ==================== КОНФИГ ЛАБЫ (всё меняется только здесь) ====================
NODES=(130 131 132)       # номер ноды = последний октет underlay-IP
U_PREFIX="192.168.200"    # underlay-подсеть
NS_PREFIX="10.0.0"        # overlay-подсеть netns (IP = NS_PREFIX.<нода>)
AS_BASE=65000             # AS i-й ноды = AS_BASE + i + 1
VNI=10010
BR=br0
IF_U="enp1s0.200"         # underlay-порт ВМ (VLAN 200 на enp1s0, создастся при отсутствии)
MTU_U=1500                # MTU underlay; overlay считается сам: MTU_U - 54
EBGP_REQ_POLICY=1         # 1 — для FRR >= 7.3; 0 — для старых (Ubuntu 20.04 / FRR 7.2)
SSH_USER="${SSH_USER:-root}"
# ================================================================================

OUT="$(cd "$(dirname "$0")" && pwd)"

# ---------- таблицы AS и соседей ----------
AS_MAP=""; PEER_MAP=""
for i in "${!NODES[@]}"; do
  n="${NODES[$i]}"; asn=$((AS_BASE + i + 1))
  AS_MAP+="  $n) echo $asn;;"$'\n'
  peers=""
  for m in "${NODES[@]}"; do [[ "$m" == "$n" ]] || peers+=" $m"; done
  PEER_MAP+="  $n) PEERS=\"${peers# }\";;"$'\n'
done
AS_BLOCK='as_of() { case "$1" in
'"$AS_MAP"'  *) return 1;;
esac; }'
PEER_BLOCK='case "$NODE" in
'"$PEER_MAP"'  *) echo "unknown node: $NODE (ожидались: '"${NODES[*]}"')"; exit 1;;
esac'

EBGP_LINE=' no bgp ebgp-requires-policy'
[[ "${EBGP_REQ_POLICY:-1}" == "1" ]] || EBGP_LINE=""

# ===================== ШАБЛОН: evpn_setup.sh =====================
read -r -d '' SETUP_TPL <<'TPL' || true
#!/usr/bin/env bash
# evpn_setup.sh — сгенерировано gen.sh. Ручные правки затрутся при генерации.
# Использование: sudo ./evpn_setup.sh [нода|пусто=авто] [underlay-iface]
set -euo pipefail
[[ $EUID -eq 0 ]] || { echo "запускать от root"; exit 1; }

U_PREFIX="@U_PREFIX@"; NS_PREFIX="@NS_PREFIX@"
VNI=@VNI@; VX="vxlan$VNI"; BR="@BR@"; MTU_U=@MTU_U@; IF_DEF="@IF_U@"

@AS_BLOCK@

NODE="${1:-$(hostname -I | tr ' ' '\n' | sed -n "s|^$U_PREFIX\.\([0-9]\+\)\$|\1|p" | head -1)}"
@PEER_BLOCK@

IP_U="$U_PREFIX.$NODE"; NS="ns$NODE"; NS_IP="$NS_PREFIX.$NODE/24"

# ---------- underlay-интерфейс ----------
IF_U="${2:-$(ip -4 -o addr show | awk -F' +|/' -v ip="$IP_U" '$4==ip{print $2; exit}')}"
[[ -n "$IF_U" ]] || IF_U="$IF_DEF"

PARENT=""; VLAN_ID=""
if [[ "$IF_U" == *.* ]]; then PARENT="${IF_U%%.*}"; VLAN_ID="${IF_U##*.}"; fi
if [[ -n "$VLAN_ID" ]]; then
  modprobe 8021q 2>/dev/null || true
  ip link show "$PARENT" &>/dev/null || { echo "нет родительского интерфейса $PARENT"; exit 1; }
  ip link set "$PARENT" mtu "$MTU_U" 2>/dev/null || true
  ip link set "$PARENT" up
  ip link show "$IF_U" &>/dev/null || ip link add link "$PARENT" name "$IF_U" type vlan id "$VLAN_ID"
fi

MTU=$((MTU_U - 54))   # 50 VXLAN + 4 VLAN
echo ">>> node$NODE (AS $(as_of "$NODE")), underlay: $IF_U = $IP_U/24, MTU overlay: $MTU"

modprobe vxlan 2>/dev/null || true
sysctl -qw net.ipv4.ip_forward=1
sysctl -qw net.ipv4.conf.all.rp_filter=0
sysctl -qw "net.ipv4.conf.$IF_U.rp_filter=0" 2>/dev/null || true

ip link set "$IF_U" mtu "$MTU_U" 2>/dev/null || true
ip link set "$IF_U" up
ip -4 addr show dev "$IF_U" | grep -qw "$IP_U" || ip addr add "$IP_U/24" dev "$IF_U"

ip link add "$BR" type bridge 2>/dev/null || true
ip link set "$BR" mtu $MTU; ip link set "$BR" up
ip link show "$VX" &>/dev/null || ip link add "$VX" type vxlan id "$VNI" local "$IP_U" dstport 4789 nolearning
ip link set "$VX" type vxlan local "$IP_U" dstport 4789 nolearning 2>/dev/null || true
ip link set "$VX" mtu $MTU; ip link set "$VX" master "$BR" 2>/dev/null || true
ip link set "$VX" up

ip netns list | awk '{print $1}' | grep -qx "$NS" || ip netns add "$NS"
if ! ip link show veth_h &>/dev/null; then
  ip link add veth_h type veth peer name veth_ns
  ip link set veth_h mtu $MTU; ip link set veth_h master "$BR"; ip link set veth_h up
  ip link set veth_ns netns "$NS"
fi
ip -n "$NS" link set lo up
ip -n "$NS" link set veth_ns mtu $MTU up
ip -n "$NS" -4 addr show dev veth_ns | grep -qw "$NS_PREFIX.$NODE" || ip -n "$NS" addr add "$NS_IP" dev veth_ns

# ---------- FRR ----------
if ! command -v vtysh &>/dev/null; then
  export DEBIAN_FRONTEND=noninteractive
  apt-get update -qq && apt-get install -y -qq frr
fi
touch /etc/frr/daemons
grep -q '^zebra=' /etc/frr/daemons && sed -i 's/^zebra=.*/zebra=yes/' /etc/frr/daemons || echo 'zebra=yes' >> /etc/frr/daemons
grep -q '^bgpd='  /etc/frr/daemons && sed -i 's/^bgpd=.*/bgpd=yes/'  /etc/frr/daemons || echo 'bgpd=yes'  >> /etc/frr/daemons

NEIGH_CFG=""; NEIGH_ACT=""
for p in $PEERS; do
  NEIGH_CFG+=" neighbor $U_PREFIX.$p remote-as $(as_of "$p")"$'\n'
  NEIGH_ACT+="  neighbor $U_PREFIX.$p activate"$'\n'
done

cat > /etc/frr/frr.conf <<EOF
frr version 8.4
frr defaults traditional
hostname node$NODE
log file /var/log/frr/frr.log
service integrated-vtysh-config
!
ip forwarding
!
router bgp $(as_of "$NODE")
 bgp router-id $IP_U
@EBGP_LINE@
${NEIGH_CFG} address-family l2vpn evpn
${NEIGH_ACT}  advertise-all-vni
 exit-address-family
!
line vty
!
end
EOF
chown frr:frr /etc/frr/frr.conf /etc/frr/daemons 2>/dev/null || true
chmod 640 /etc/frr/frr.conf /etc/frr/daemons 2>/dev/null || true
systemctl enable frr >/dev/null 2>&1 || true
systemctl reset-failed frr.service 2>/dev/null || true
if ! systemctl restart frr; then
  echo "!!! frr.service не запустился — журнал:" >&2
  journalctl -u frr.service -n 40 --no-pager || true
  echo "!!! если 'unknown command ... ebgp-requires-policy' — в gen.sh поставь EBGP_REQ_POLICY=0" >&2
  exit 1
fi

echo ">>> итог:"
ip -br addr show "$IF_U"
ip -br link show "$BR"
ip -br link show "$VX"
ip netns list | grep "^$NS" || true
echo ">>> жду сходимость BGP 5 c..."; sleep 5
vtysh -c 'show bgp l2vpn evpn summary' || true
echo ">>> готово. Тест: ./test_evpn.sh $NODE"
TPL

# ===================== ШАБЛОН: test_evpn.sh =====================
read -r -d '' TEST_TPL <<'TPL' || true
#!/usr/bin/env bash
# test_evpn.sh — сгенерировано gen.sh. Проверка EVPN-связности.
set -uo pipefail
U_PREFIX="@U_PREFIX@"; NS_PREFIX="@NS_PREFIX@"; VNI=@VNI@; VX="vxlan$VNI"

NODE="${1:-$(hostname -I | tr ' ' '\n' | sed -n "s|^$U_PREFIX\.\([0-9]\+\)\$|\1|p" | head -1)}"
[[ -n "$NODE" ]] || { echo "не смог определить ноду — укажи аргументом"; exit 1; }
NS="ns$NODE"; NS_IP="$NS_PREFIX.$NODE"; RC=0
section() { echo; echo "===== $* ====="; }

section "BGP EVPN"
vtysh -c 'show bgp l2vpn evpn summary'

section "VNI"
vtysh -c 'show evpn vni'

PEERS_NETNS=$(ip netns list | awk '{print $1}' | sed -n 's/^ns\([0-9]\+\)$/\1/p' | sort -n)

section "underlay (транк VLAN 200)"
for p in $PEERS_NETNS; do
  [[ "$p" == "$NODE" ]] && continue
  if ping -c1 -W1 -q "$U_PREFIX.$p" &>/dev/null; then echo "  OK   $U_PREFIX.$NODE -> $U_PREFIX.$p"
  else echo "  FAIL $U_PREFIX.$NODE -> $U_PREFIX.$p  (проблема в транке, до EVPN дело не дойдёт)"; RC=1; fi
done

section "overlay: ping из $NS"
for p in $PEERS_NETNS; do
  [[ "$p" == "$NODE" ]] && continue
  if LC_ALL=C ip netns exec "$NS" ping -c3 -W2 -q "$NS_PREFIX.$p" >/tmp/p.$$ 2>&1; then
    echo "  OK   $NS_IP -> $NS_PREFIX.$p ($(grep -o '[0-9]*% packet loss' /tmp/p.$$ | head -1))"
  else
    echo "  FAIL $NS_IP -> $NS_PREFIX.$p"; tail -3 /tmp/p.$$; RC=1
  fi
done
rm -f /tmp/p.$$

section "ARP-кэш $NS"
ip netns exec "$NS" ip neigh || true

section "MAC-таблица EVPN (VNI $VNI)"
vtysh -c "show evpn mac vni $VNI" || true

section "remote VTEP в FDB $VX"
bridge fdb show dev $VX | grep dst || echo "  (пусто — повтори через пару секунд после ping)"

section "итог"
[[ $RC -eq 0 ]] && echo "ВСЁ ОК" || echo "ЕСТЬ ПРОБЛЕМЫ (см. FAIL выше: underlay = транк/маршрутизация, overlay = EVPN)"
exit $RC
TPL

# ===================== ШАБЛОН: deploy.sh =====================
read -r -d '' DEPLOY_TPL <<'TPL' || true
#!/usr/bin/env bash
# deploy.sh — сгенерировано gen.sh. Раскатка и тест всех нод по SSH.
set -euo pipefail
NODES=(@NODES@)
U_PREFIX="@U_PREFIX@"
SSH_USER="${SSH_USER:-@SSH_USER@}"
DIR="$(cd "$(dirname "$0")" && pwd)"
RUN='bash'; [[ "$SSH_USER" == "root" ]] || RUN='sudo bash'

for n in "${NODES[@]}"; do
  h="$SSH_USER@$U_PREFIX.$n"
  echo "===== $h: копирование ====="
  scp -q "$DIR/evpn_setup.sh" "$DIR/test_evpn.sh" "$h:/tmp/"
  echo "===== $h: настройка ====="
  ssh "$h" "$RUN /tmp/evpn_setup.sh"
done

echo "===== сходимость BGP: 10 c ====="; sleep 10
FAIL=0
for n in "${NODES[@]}"; do
  echo "===== $SSH_USER@$U_PREFIX.$n: тест ====="
  ssh "$SSH_USER@$U_PREFIX.$n" "$RUN /tmp/test_evpn.sh" || FAIL=1
done
exit $FAIL
TPL

# ===================== ШАБЛОН: evpn_cleanup.sh =====================
read -r -d '' CLEAN_TPL <<'TPL' || true
#!/usr/bin/env bash
# evpn_cleanup.sh — сгенерировано gen.sh. Откат dataplane (frr.conf не трогает).
set -euo pipefail
U_PREFIX="@U_PREFIX@"; NS_PREFIX="@NS_PREFIX@"; VNI=@VNI@; VX="vxlan$VNI"; BR="@BR@"
NODE="${1:-$(hostname -I | tr ' ' '\n' | sed -n "s|^$U_PREFIX\.\([0-9]\+\)\$|\1|p" | head -1)}"
ip netns del "ns$NODE" 2>/dev/null || true
ip link del veth_h 2>/dev/null || true
ip link del "$VX" 2>/dev/null || true
ip link del "$BR" 2>/dev/null || true
echo ">>> dataplane удалён (enp1s0.200 не тронут; evpn_setup.sh вернёт всё обратно)"
TPL

# ---------- генерация ----------
gen() {
  local s="${!1}"
  s="${s//@U_PREFIX@/$U_PREFIX}"; s="${s//@NS_PREFIX@/$NS_PREFIX}"
  s="${s//@VNI@/$VNI}";           s="${s//@BR@/$BR}"
  s="${s//@IF_U@/$IF_U}";         s="${s//@MTU_U@/$MTU_U}"
  s="${s//@EBGP_LINE@/$EBGP_LINE}"
  s="${s//@NODES@/${NODES[*]}}";  s="${s//@SSH_USER@/$SSH_USER}"
  s="${s//@AS_BLOCK@/$AS_BLOCK}"; s="${s//@PEER_BLOCK@/$PEER_BLOCK}"
  printf '%s\n' "$s" > "$OUT/$2"
  bash -n "$OUT/$2" && echo "  создан $2 (синтаксис OK)"
}

echo ">>> генерирую в $OUT (ноды ${NODES[*]}, $U_PREFIX.0/24@$IF_U, $NS_PREFIX.0/24, VNI $VNI):"
gen SETUP_TPL  evpn_setup.sh
gen TEST_TPL   test_evpn.sh
gen DEPLOY_TPL deploy.sh
gen CLEAN_TPL  evpn_cleanup.sh
echo
echo "Дальше: ./deploy.sh  — или вручную на каждой ноде: sudo ./evpn_setup.sh, затем ./test_evpn.sh"

Запуск:

chmod +x gen.sh && ./gen.sh
# на каждой ноде (130, 131, 132):
sudo bash evpn_setup.sh
sudo bash test_evpn.sh

Гарантии по порядку отладки зашиты в тест: сначала underlay (пинг 192.168.200.x через транк), потом overlay (пинг 10.0.0.x через EVPN). Если падает underlay — транк/VLAN; если underlay OK, а overlay FAIL — пришли вывод test_evpn.sh с любой ноды, там будет сразу видно, какой слой отвалился.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment