Один скрипт, в шапке — вся конфигурация лабы (ноды, подсети, VNI, AS). Запуск создаёт рядом 4 файла: evpn_setup.sh, test_evpn.sh, deploy.sh, evpn_cleanup.sh (те самые из прошлого шага, но значения уже прошиты из конфига). Поменяли конфиг — перезапустили gen.sh, файлы перегенерировались.
#!/usr/bin/env bash
# gen.sh — генератор скриптов лабы EVPN-VXLAN (ноды 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 (по порядку в NODES)
VNI=10010
BR=br0
MTU_DP=1450 # MTU bridge/veth (underlay 1500 − 50 байт на VXLAN)
SSH_USER="${SSH_USER:-root}" # пользователь для deploy.sh (потом можно переопределить: SSH_USER=adm ./deploy.sh)
# ======================================================================
OUT="$(cd "$(dirname "$0")" && pwd)"
# ---------- таблицы AS и соседей из NODES ----------
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'
# ===================== ШАБЛОН: evpn_setup.sh =====================
read -r -d '' SETUP_TPL <<'TPL' || true
#!/usr/bin/env bash
# evpn_setup.sh — сгенерировано gen.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=@MTU_DP@
@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=$(ip route get "$IP_U" 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="dev"){print $(i+1); exit}}')
[[ -n "$IF_U" ]] || IF_U=eth1
echo ">>> node$NODE (AS $(as_of "$NODE")), underlay: $IF_U = $IP_U/24"
# ---------- sysctl ----------
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
# ---------- underlay ----------
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"
# ---------- bridge + VTEP ----------
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
# ---------- netns + veth ----------
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
# ---------- firewall ----------
if command -v iptables &>/dev/null; then
iptables -C INPUT -p udp --dport 4789 -j ACCEPT 2>/dev/null || iptables -I INPUT 1 -p udp --dport 4789 -j ACCEPT
iptables -C INPUT -p tcp --dport 179 -j ACCEPT 2>/dev/null || iptables -I INPUT 1 -p tcp --dport 179 -j ACCEPT
fi
# ---------- 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
no bgp ebgp-requires-policy
${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
systemctl enable frr >/dev/null 2>&1 || true
systemctl restart frr
echo ">>> итог:"
ip -br addr show "$IF_U"; ip -br link show "$BR" "$VX"
ip netns list | grep "^$NS"
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. Проверка L2 связности netns через 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 "$NS ($NS_IP): BGP EVPN"
vtysh -c 'show bgp l2vpn evpn summary'
section "VNI"
vtysh -c 'show evpn vni'
section "ping из $NS"
for peer in $(ip netns list | awk '{print $1}' | sed -n 's/^ns\([0-9]\+\)$/\1/p' | sort -n); do
[[ "$peer" == "$NODE" ]] && continue
if ip netns exec "$NS" ping -c 3 -W 2 -q "$NS_PREFIX.$peer" >/tmp/p.$$ 2>&1; then
echo " OK $NS_IP -> $NS_PREFIX.$peer ($(grep -o '[0-9]*% packet loss' /tmp/p.$$ | head -1))"
else
echo " FAIL $NS_IP -> $NS_PREFIX.$peer"; tail -3 /tmp/p.$$; RC=1
fi
done
rm -f /tmp/p.$$
section "ARP-кэш $NS"
ip netns exec "$NS" ip neigh
section "MAC-таблица EVPN (VNI $VNI)"
vtysh -c "show evpn mac vni $VNI"
section "remote VTEP в FDB $VX"
bridge fdb show dev $VX | grep dst || echo " (пусто — повторите через пару секунд после ping)"
section "итог"
[[ $RC -eq 0 ]] && echo "ВСЁ ОК" || echo "ЕСТЬ ПРОБЛЕМЫ"
exit $RC
# --- если FAIL: udp/4789 (tcpdump -ni <if> udp port 4789), tcp/179 (BGP),
# firewall, MTU на br0/veth, 'show bgp l2vpn evpn summary' (Established?)
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
h="$SSH_USER@$U_PREFIX.$n"
echo "===== $h: тест ====="
ssh "$h" "$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 на ноде.
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 удалён (frr.conf не тронут; evpn_setup.sh вернёт всё обратно)"
TPL
# ---------- генерация ----------
gen() { # 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//@MTU_DP@/$MTU_DP}"
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" # синтакс-контроль сгенерированного
chmod +x "$OUT/$2"
echo " создан $2"
}
echo ">>> генерирую в $OUT (ноды: ${NODES[*]}, underlay $U_PREFIX.0/24, overlay $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 "Дальше:"
echo " ssh-copy-id $SSH_USER@$U_PREFIX.13{0,1,2} # однократно"
echo " ./deploy.sh # раскатка + автотест"
echo " # или вручную на каждой ноде: sudo ./evpn_setup.sh && ./test_evpn.sh"chmod +x gen.sh && ./gen.sh
# node130 (AS 65001) → ns130 10.0.0.130, node131 (AS 65002) → ns131 10.0.0.131, node132 (AS 65003) → ns132 10.0.0.132
./deploy.shЧто настраивается в шапке gen.sh:
| Переменная | Что задаёт |
|---|---|
NODES |
список нод; номер = последний октет underlay-IP, из него же строятся AS-таблица, соседи, имена netns и их IP |
U_PREFIX / NS_PREFIX |
underlay- и overlay-подсети |
AS_BASE |
база AS (eBGP между нодами) |
VNI, BR, MTU_DP |
сегмент EVPN, имя моста, MTU дата-плейна |
SSH_USER |
пользователь SSH для deploy.sh |
Нюансы:
- генератор перезаписывает файлы при каждом запуске — ручные правки в сгенерированном затрутся, правьте
gen.sh; bash -nпосле генерации ловит синтакс-ошибки в шаблонах сразу;- на узлах автоопределение: номер ноды берётся из её IP
U_PREFIX.x, так что одинevpn_setup.shподходит всем трём ВМ без аргументов.