2026-08-16 21:43:02 +03:00
|
|
|
|
#!/bin/bash
|
|
|
|
|
|
# Проверки стенда, выполнимые с хоста. Клиентские шаги (ping/curl из
|
|
|
|
|
|
# 192.168.5.0/24) описаны в README и выполняются вручную; если поднят
|
|
|
|
|
|
# macvlan-shim (host-prereq.sh --shim), скрипт прогоняет и их.
|
|
|
|
|
|
set -uo pipefail
|
|
|
|
|
|
|
|
|
|
|
|
cd "$(dirname "$0")/.."
|
|
|
|
|
|
DC="docker compose"
|
|
|
|
|
|
VIP=192.168.5.21
|
|
|
|
|
|
LB_IP=192.168.5.20
|
|
|
|
|
|
SHIM=hpnn-shim
|
|
|
|
|
|
|
|
|
|
|
|
pass=0
|
|
|
|
|
|
fail=0
|
|
|
|
|
|
ok() { echo -e " \033[32mOK\033[0m $*"; pass=$((pass + 1)); }
|
|
|
|
|
|
bad() { echo -e " \033[31mПРОВАЛ\033[0m $*"; fail=$((fail + 1)); }
|
|
|
|
|
|
skip() { echo -e " \033[33mПРОПУСК\033[0m $*"; }
|
|
|
|
|
|
head_() { echo; echo "== $*"; }
|
|
|
|
|
|
|
|
|
|
|
|
head_ "1. Контейнеры"
|
2026-08-17 16:59:16 +03:00
|
|
|
|
for c in hpnn-lb hpnn-be1 hpnn-be2 hpnn-be3 hpnn-be4 hpnn-cli2 hpnn-cli3; do
|
2026-08-16 21:43:02 +03:00
|
|
|
|
state=$(docker inspect -f '{{.State.Status}}' "$c" 2>/dev/null || echo "нет")
|
|
|
|
|
|
[ "$state" = "running" ] && ok "$c: $state" || bad "$c: $state"
|
|
|
|
|
|
done
|
|
|
|
|
|
|
|
|
|
|
|
head_ "2. Мост br-lb и порты"
|
|
|
|
|
|
ports=$($DC exec -T lb-router ovs-vsctl list-ports br-lb 2>/dev/null | tr '\n' ' ')
|
|
|
|
|
|
for p in pub0 p2 p3; do
|
|
|
|
|
|
echo "$ports" | grep -qw "$p" && ok "порт $p в мосту" || bad "порт $p отсутствует (есть: $ports)"
|
|
|
|
|
|
done
|
2026-08-17 16:59:16 +03:00
|
|
|
|
# Шаг 3: каждая ВМ сегмента — отдельный порт моста. Без этого балансировщик не
|
|
|
|
|
|
# увидит ответ бэкенда клиенту-соседу и приватный листенер работать не сможет.
|
|
|
|
|
|
for p in be1 be3 cli2 be2 be4 cli3; do
|
|
|
|
|
|
echo "$ports" | grep -qw "$p" && ok "ВМ $p подключена портом OVS" \
|
|
|
|
|
|
|| bad "порт $p отсутствует — выполните make attach"
|
|
|
|
|
|
done
|
2026-08-16 21:43:02 +03:00
|
|
|
|
dp=$($DC exec -T lb-router ovs-appctl dpif/show 2>/dev/null | head -1)
|
|
|
|
|
|
[ -n "$dp" ] && ok "датапас: $dp" || bad "датапас не поднят"
|
|
|
|
|
|
|
|
|
|
|
|
head_ "3. Пайплайн"
|
2026-08-17 16:59:16 +03:00
|
|
|
|
for t in 0 1 5 6 10 11 12 15 16 20 21 24 25; do
|
2026-08-16 21:43:02 +03:00
|
|
|
|
n=$($DC exec -T lb-router ovs-ofctl -O OpenFlow15 dump-flows br-lb "table=$t" 2>/dev/null | grep -c 'table=')
|
|
|
|
|
|
[ "${n:-0}" -gt 0 ] && ok "таблица $t: $n правил" || bad "таблица $t пуста"
|
|
|
|
|
|
done
|
|
|
|
|
|
for p in hcif-p2 hcif-p3; do
|
|
|
|
|
|
echo "$ports" | grep -qw "$p" && ok "порт-источник проб $p в мосту" || bad "порт $p отсутствует"
|
|
|
|
|
|
done
|
|
|
|
|
|
|
|
|
|
|
|
head_ "4. Маршруты бэкендов (профиль A: default мимо LB)"
|
|
|
|
|
|
for be in be1:10.20.0.1:10.30.0.0/24 be2:10.30.0.1:10.20.0.0/24 \
|
|
|
|
|
|
be3:10.20.0.1:10.30.0.0/24 be4:10.30.0.1:10.20.0.0/24; do
|
|
|
|
|
|
name=${be%%:*}; rest=${be#*:}; gw=${rest%%:*}; peer=${rest#*:}
|
|
|
|
|
|
r=$($DC exec -T "$name" ip route 2>/dev/null)
|
|
|
|
|
|
echo "$r" | grep -q "192.168.5.0/24 via $gw" && ok "$name: клиентский префикс via $gw" || bad "$name: нет маршрута на 192.168.5.0/24"
|
|
|
|
|
|
echo "$r" | grep -q "$peer via $gw" && ok "$name: соседний сегмент via $gw" || bad "$name: нет маршрута на $peer"
|
|
|
|
|
|
echo "$r" | grep -q "^default via .*\.254" && ok "$name: default мимо LB" || bad "$name: default не на шлюзе Docker"
|
|
|
|
|
|
done
|
|
|
|
|
|
|
|
|
|
|
|
head_ "5. Маршрутизация между приватными сегментами"
|
|
|
|
|
|
for pair in be1:10.30.0.2:be2 be3:10.30.0.3:be4 be2:10.20.0.2:be1 be4:10.20.0.3:be3; do
|
|
|
|
|
|
from=${pair%%:*}; rest=${pair#*:}; dst=${rest%%:*}; to=${rest#*:}
|
|
|
|
|
|
out=$($DC exec -T "$from" curl -s --max-time 5 "http://$dst:8080/" 2>/dev/null)
|
|
|
|
|
|
echo "$out" | grep -q "backend=$to" && ok "$from -> $to через OVS" \
|
|
|
|
|
|
|| bad "$from -> $to недоступен (ответ: ${out:-пусто})"
|
|
|
|
|
|
done
|
|
|
|
|
|
|
|
|
|
|
|
head_ "6. Балансировка (требует macvlan-shim на хосте)"
|
|
|
|
|
|
if ip link show "$SHIM" >/dev/null 2>&1; then
|
|
|
|
|
|
ping -c1 -W2 "$LB_IP" >/dev/null 2>&1 && ok "ping $LB_IP (ARP+ICMP-респондер OVS)" || bad "ping $LB_IP не проходит"
|
|
|
|
|
|
ping -c1 -W2 "$VIP" >/dev/null 2>&1 && ok "ping $VIP (VIP)" || bad "ping $VIP не проходит"
|
|
|
|
|
|
|
|
|
|
|
|
declare -A hits=()
|
|
|
|
|
|
client=""
|
|
|
|
|
|
for _ in $(seq 20); do
|
|
|
|
|
|
line=$(curl -s --max-time 5 "http://$VIP/" 2>/dev/null) || continue
|
|
|
|
|
|
b=$(echo "$line" | sed -n 's/.*backend=\([a-z0-9]*\).*/\1/p')
|
|
|
|
|
|
c=$(echo "$line" | sed -n 's/.*client=\([0-9.]*\):.*/\1/p')
|
|
|
|
|
|
[ -n "$b" ] && hits[$b]=$(( ${hits[$b]:-0} + 1 ))
|
|
|
|
|
|
[ -n "$c" ] && client=$c
|
|
|
|
|
|
done
|
|
|
|
|
|
total=0; for k in "${!hits[@]}"; do total=$((total + hits[$k])); done
|
|
|
|
|
|
echo " распределение: $(for k in "${!hits[@]}"; do printf '%s=%s ' "$k" "${hits[$k]}"; done)(всего $total)"
|
|
|
|
|
|
# На 20 запросах и четырёх членах пропуск одного бэкенда возможен, но
|
|
|
|
|
|
# маловероятен; порог в три различных имени отделяет статистику от дефекта.
|
|
|
|
|
|
[ "${#hits[@]}" -ge 3 ] && ok "трафик распределён между ${#hits[@]} бэкендами из 4" \
|
|
|
|
|
|
|| bad "задействовано лишь ${#hits[@]} бэкендов"
|
|
|
|
|
|
if [ -n "$client" ]; then
|
|
|
|
|
|
case "$client" in
|
|
|
|
|
|
192.168.5.*) ok "бэкенд видит реальный IP клиента: $client (DNAT без SNAT)" ;;
|
|
|
|
|
|
*) bad "бэкенд видит адрес $client вместо клиентского" ;;
|
|
|
|
|
|
esac
|
|
|
|
|
|
else
|
|
|
|
|
|
bad "не удалось получить адрес клиента из ответа"
|
|
|
|
|
|
fi
|
|
|
|
|
|
else
|
|
|
|
|
|
skip "shim не поднят — выполните: sudo scripts/host-prereq.sh --shim"
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
2026-08-17 16:59:16 +03:00
|
|
|
|
head_ "7. Профиль A: собственный egress бэкенда не маршрутизируется балансировщиком"
|
|
|
|
|
|
# С шага 3 br-lb коммутирует сегмент, поэтому egress физически через него
|
|
|
|
|
|
# проходит — но только как через коммутатор. Признак: кадр адресован MAC
|
|
|
|
|
|
# docker-шлюза, а не MAC маршрутизатора сегмента (02:42:0a:14:00:01).
|
2026-08-16 21:43:02 +03:00
|
|
|
|
dump=$(mktemp)
|
2026-08-17 16:59:16 +03:00
|
|
|
|
timeout 12 $DC exec -T lb-router timeout 8 tcpdump -eni be1 -c 2 'host 1.1.1.1' >"$dump" 2>&1 &
|
2026-08-16 21:43:02 +03:00
|
|
|
|
tcpdump_pid=$!
|
|
|
|
|
|
sleep 2
|
|
|
|
|
|
code=$($DC exec -T be1 curl -s -o /dev/null -w '%{http_code}' --max-time 6 http://1.1.1.1/ 2>/dev/null)
|
|
|
|
|
|
wait $tcpdump_pid 2>/dev/null
|
|
|
|
|
|
[ "${code:-000}" != "000" ] && ok "be1 достучался наружу (HTTP $code) через шлюз Docker" \
|
|
|
|
|
|
|| bad "be1 не имеет выхода наружу (HTTP ${code:-000})"
|
|
|
|
|
|
if grep -q '^[0-9][0-9]:' "$dump"; then
|
2026-08-17 16:59:16 +03:00
|
|
|
|
if grep -q '> 02:42:0a:14:00:01' "$dump"; then
|
|
|
|
|
|
bad "egress адресован маршрутизатору сегмента — default ушёл на балансировщик"
|
|
|
|
|
|
else
|
|
|
|
|
|
ok "egress адресован шлюзу Docker, а не маршрутизатору сегмента"
|
|
|
|
|
|
fi
|
2026-08-16 21:43:02 +03:00
|
|
|
|
else
|
2026-08-17 16:59:16 +03:00
|
|
|
|
bad "трафик egress не наблюдается на порту ВМ"
|
2026-08-16 21:43:02 +03:00
|
|
|
|
fi
|
|
|
|
|
|
rm -f "$dump"
|
|
|
|
|
|
|
|
|
|
|
|
head_ "8. Health-check: состояние пула"
|
|
|
|
|
|
hc() { $DC exec -T lb-router curl -fsS --max-time 5 "http://127.0.0.1:9111$1" 2>/dev/null; }
|
|
|
|
|
|
state() { hc /status | sed -n "s/.*\"name\":\"$1\"[^}]*\"state\":\"\([a-z]*\)\".*/\1/p"; }
|
|
|
|
|
|
digest() { hc /status | sed -n 's/.*"slot_table_digest":"\([^"]*\)".*/\1/p'; }
|
|
|
|
|
|
# Раскладка из датапаса: пары «слот -> член». reg1=0 OVS печатает без 0x.
|
|
|
|
|
|
snapshot() {
|
|
|
|
|
|
$DC exec -T lb-router ovs-ofctl -O OpenFlow15 dump-flows br-lb table=11 2>/dev/null \
|
|
|
|
|
|
| sed -n 's/.*reg1=\(0x[0-9a-f]*\|[0-9]\+\).*set_field:\(0x[0-9a-f]*\)->reg2.*/\1 \2/p' | sort
|
|
|
|
|
|
}
|
|
|
|
|
|
slots_of() { snapshot | grep -c " $1\$"; }
|
|
|
|
|
|
# wait_state <член> <состояние> <секунд>
|
|
|
|
|
|
wait_state() {
|
|
|
|
|
|
for _ in $(seq "$3"); do
|
|
|
|
|
|
[ "$(hc /status | sed -n "s/.*\"name\":\"$1\"[^}]*\"state\":\"\([a-z]*\)\".*/\1/p")" = "$2" ] && return 0
|
|
|
|
|
|
sleep 1
|
|
|
|
|
|
done
|
|
|
|
|
|
return 1
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for m in be1 be2 be3 be4; do
|
|
|
|
|
|
[ "$(state $m)" = "up" ] && ok "член $m: up" || bad "член $m: $(state $m)"
|
|
|
|
|
|
done
|
|
|
|
|
|
probes=$(hc /status | sed -n 's/.*"probes":\([0-9]*\).*/\1/p' | head -1)
|
|
|
|
|
|
[ "${probes:-0}" -gt 0 ] && ok "пробы идут (у be1 их $probes)" || bad "проб не было"
|
|
|
|
|
|
|
|
|
|
|
|
total_slots=$(snapshot | wc -l)
|
|
|
|
|
|
[ "$total_slots" -eq 1024 ] && ok "таблица слотов заполнена: $total_slots слотов" \
|
|
|
|
|
|
|| bad "слотов $total_slots вместо 1024"
|
|
|
|
|
|
# Идеальная доля при четырёх равновесных членах — 256 слотов; Maglev
|
|
|
|
|
|
# гарантирует отклонение в пределах ±1, порог взят с большим запасом.
|
|
|
|
|
|
uneven=0; dist=""
|
|
|
|
|
|
for id in 0x1 0x2 0x3 0x4; do
|
|
|
|
|
|
n=$(slots_of $id); dist="$dist $id=$n"
|
|
|
|
|
|
[ "$n" -lt 230 ] || [ "$n" -gt 280 ] && uneven=1
|
|
|
|
|
|
done
|
|
|
|
|
|
[ "$uneven" -eq 0 ] && ok "слоты поделены поровну:$dist" || bad "неравномерная раскладка:$dist"
|
|
|
|
|
|
|
|
|
|
|
|
head_ "9. Источник health-проб — уникальный адрес узла, не VIP"
|
2026-08-17 16:59:16 +03:00
|
|
|
|
# Слушаем порт самой ВМ: с шага 3 проба идёт коммутацией сегмента, а не через
|
|
|
|
|
|
# аплинк p2, поэтому на аплинке её больше не видно.
|
|
|
|
|
|
src=$($DC exec -T lb-router timeout 6 tcpdump -ni be1 -c 1 'tcp port 8080 and tcp[tcpflags] & tcp-syn != 0' 2>/dev/null \
|
2026-08-16 21:43:02 +03:00
|
|
|
|
| sed -n 's/.*IP \([0-9.]*\)\.[0-9]* > .*/\1/p' | head -1)
|
|
|
|
|
|
[ "$src" = "10.20.0.253" ] && ok "проба к be1 уходит с $src" || bad "проба уходит с ${src:-неизвестно}, ожидался 10.20.0.253"
|
|
|
|
|
|
|
|
|
|
|
|
head_ "10. Минимальное возмущение раскладки при выводе члена"
|
|
|
|
|
|
before=$(mktemp); after=$(mktemp)
|
|
|
|
|
|
snapshot > "$before"
|
|
|
|
|
|
digest_full=$(digest)
|
|
|
|
|
|
$DC exec -T lb-router lbctl drain be1 >/dev/null 2>&1
|
|
|
|
|
|
sleep 1
|
|
|
|
|
|
snapshot > "$after"
|
|
|
|
|
|
# Слоты выбывшего члена обязаны переехать — это его 25 % таблицы. Смысл
|
|
|
|
|
|
# проверки в другом: сколько слотов сменило владельца у ОСТАВШИХСЯ членов.
|
|
|
|
|
|
# Классический Maglev гарантирует малое, а не нулевое возмущение, поэтому
|
|
|
|
|
|
# проверяется порог. Измеренное на этом стенде значение — 8 слотов (0,8 %).
|
|
|
|
|
|
own=$(join "$before" "$after" | awk '$2=="0x1"' | wc -l)
|
|
|
|
|
|
foreign=$(join "$before" "$after" | awk '$2!="0x1" && $2!=$3' | wc -l)
|
|
|
|
|
|
if [ "$foreign" -le 20 ]; then
|
|
|
|
|
|
ok "возмущение минимально: переехали $own слотов выбывшего be1 и лишь $foreign чужих (порог 20)"
|
|
|
|
|
|
else
|
|
|
|
|
|
bad "у оставшихся членов переехало $foreign слотов — возмущение выше ожидаемого"
|
|
|
|
|
|
fi
|
|
|
|
|
|
$DC exec -T lb-router lbctl enable be1 >/dev/null 2>&1
|
|
|
|
|
|
sleep 1
|
|
|
|
|
|
[ "$(digest)" = "$digest_full" ] && ok "после возврата члена дайджест прежний: $digest_full" \
|
|
|
|
|
|
|| bad "дайджест не восстановился: $(digest) вместо $digest_full"
|
|
|
|
|
|
rm -f "$before" "$after"
|
|
|
|
|
|
|
|
|
|
|
|
head_ "11. Отказ и восстановление члена пула"
|
|
|
|
|
|
$DC pause be1 >/dev/null 2>&1
|
|
|
|
|
|
if wait_state be1 down 20; then
|
|
|
|
|
|
ok "be1 переведён в down по данным проб"
|
|
|
|
|
|
[ "$(slots_of 0x1)" -eq 0 ] && ok "у be1 не осталось слотов" || bad "у be1 ещё $(slots_of 0x1) слотов"
|
|
|
|
|
|
[ "$(snapshot | wc -l)" -eq 1024 ] && ok "его слоты разошлись по трём оставшимся членам" \
|
|
|
|
|
|
|| bad "в таблице $(snapshot | wc -l) слотов вместо 1024"
|
|
|
|
|
|
nodead=1
|
|
|
|
|
|
for _ in $(seq 8); do
|
|
|
|
|
|
line=$(curl -s --max-time 5 "http://$VIP/" 2>/dev/null)
|
|
|
|
|
|
echo "$line" | grep -qE 'backend=(be2|be3|be4)' || nodead=0
|
|
|
|
|
|
done
|
|
|
|
|
|
[ "$nodead" -eq 1 ] && ok "весь трафик на VIP обслуживают живые члены, ошибок нет" \
|
|
|
|
|
|
|| bad "часть запросов не обслужена или ушла на мёртвый член"
|
|
|
|
|
|
else
|
|
|
|
|
|
bad "be1 не перешёл в down за 20 с"
|
|
|
|
|
|
fi
|
|
|
|
|
|
$DC unpause be1 >/dev/null 2>&1
|
|
|
|
|
|
wait_state be1 up 20 && ok "be1 вернулся в up после восстановления" || bad "be1 не вернулся в up"
|
|
|
|
|
|
|
|
|
|
|
|
head_ "12. Fail-close при полном отказе пула"
|
|
|
|
|
|
$DC pause be1 be2 be3 be4 >/dev/null 2>&1
|
|
|
|
|
|
if wait_state be1 down 20 && wait_state be2 down 20 \
|
|
|
|
|
|
&& wait_state be3 down 20 && wait_state be4 down 20; then
|
|
|
|
|
|
[ "$(snapshot | wc -l)" -eq 0 ] && ok "таблица слотов пуста" || bad "в таблице остались слоты"
|
|
|
|
|
|
d0=$($DC exec -T lb-router ovs-ofctl -O OpenFlow15 dump-flows br-lb table=11 2>/dev/null \
|
|
|
|
|
|
| sed -n 's/.*n_packets=\([0-9]*\).*priority=0.*/\1/p')
|
|
|
|
|
|
curl -s --max-time 4 "http://$VIP/" >/dev/null 2>&1 && bad "VIP ответил при пустом пуле" || ok "клиент получает таймаут"
|
|
|
|
|
|
d1=$($DC exec -T lb-router ovs-ofctl -O OpenFlow15 dump-flows br-lb table=11 2>/dev/null \
|
|
|
|
|
|
| sed -n 's/.*n_packets=\([0-9]*\).*priority=0.*/\1/p')
|
|
|
|
|
|
[ "${d1:-0}" -gt "${d0:-0}" ] && ok "счётчик правила fail-close вырос: ${d0:-0} -> ${d1:-0}" \
|
|
|
|
|
|
|| bad "счётчик fail-close не изменился"
|
|
|
|
|
|
else
|
|
|
|
|
|
bad "члены не перешли в down"
|
|
|
|
|
|
fi
|
|
|
|
|
|
$DC unpause be1 be2 be3 be4 >/dev/null 2>&1
|
|
|
|
|
|
wait_state be1 up 20 && wait_state be2 up 20 && wait_state be3 up 20 && wait_state be4 up 20 \
|
|
|
|
|
|
&& ok "пул восстановлен" || bad "пул не восстановился"
|
|
|
|
|
|
|
|
|
|
|
|
head_ "13. Установленная сессия переживает смену состава пула"
|
|
|
|
|
|
if ip link show "$SHIM" >/dev/null 2>&1; then
|
|
|
|
|
|
slow=$(mktemp)
|
|
|
|
|
|
(curl -sN --max-time 22 "http://$VIP/slow?seconds=14" > "$slow" 2>&1 &)
|
|
|
|
|
|
sleep 4
|
|
|
|
|
|
served=$(sed -n 's/backend=\([a-z0-9]*\).*/\1/p' "$slow" | head -1)
|
|
|
|
|
|
if [ -n "$served" ]; then
|
|
|
|
|
|
$DC exec -T lb-router lbctl drain "$served" >/dev/null 2>&1
|
|
|
|
|
|
sleep 12
|
|
|
|
|
|
$DC exec -T lb-router lbctl enable "$served" >/dev/null 2>&1
|
|
|
|
|
|
ticks=$(grep -c '^backend=' "$slow")
|
|
|
|
|
|
[ "$ticks" -eq 14 ] && ok "сессия к $served не порвалась при его выводе из пула ($ticks/14 тиков)" \
|
|
|
|
|
|
|| bad "сессия оборвалась: $ticks из 14 тиков"
|
|
|
|
|
|
else
|
|
|
|
|
|
bad "не удалось начать длинную сессию"
|
|
|
|
|
|
fi
|
|
|
|
|
|
rm -f "$slow"
|
|
|
|
|
|
else
|
|
|
|
|
|
skip "shim не поднят"
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
head_ "14. Идемпотентность: перезаливка пайплайна не теряет раскладку"
|
|
|
|
|
|
d_before=$(digest)
|
|
|
|
|
|
$DC exec -T lb-router /opt/lb/apply.sh >/dev/null 2>&1
|
|
|
|
|
|
sleep 1
|
|
|
|
|
|
[ "$(digest)" = "$d_before" ] && [ "$(snapshot | wc -l)" -eq 1024 ] \
|
|
|
|
|
|
&& ok "после apply.sh раскладка та же: $d_before" \
|
|
|
|
|
|
|| bad "раскладка изменилась: $(digest), слотов $(snapshot | wc -l)"
|
|
|
|
|
|
|
2026-08-17 16:59:16 +03:00
|
|
|
|
head_ "15. Листенер в приватном сегменте (шаг 3)"
|
|
|
|
|
|
# Клиент cli2 живёт в одном сегменте с be1 и be3 — тот самый случай, ради
|
|
|
|
|
|
# которого сегмент переведён на порты OVS. В его ОС не настроено ничего, кроме
|
|
|
|
|
|
# адреса: VIP находится в его собственной подсети.
|
|
|
|
|
|
P2_VIP=10.20.0.100
|
|
|
|
|
|
routes=$($DC exec -T cli2 ip route 2>/dev/null | grep -c 'via')
|
|
|
|
|
|
[ "${routes:-1}" -eq 0 ] && ok "в ОС клиента нет ни одного маршрута — только адрес" \
|
|
|
|
|
|
|| bad "у клиента появились маршруты: их быть не должно"
|
|
|
|
|
|
|
|
|
|
|
|
$DC exec -T cli2 ping -c1 -W2 "$P2_VIP" >/dev/null 2>&1 \
|
|
|
|
|
|
&& ok "ping $P2_VIP из сегмента (ARP- и ICMP-респондеры OVS)" \
|
|
|
|
|
|
|| bad "VIP приватного листенера недоступен"
|
|
|
|
|
|
|
|
|
|
|
|
declare -A phits=()
|
|
|
|
|
|
pclient=""; pserved=""; pown=0
|
|
|
|
|
|
for _ in $(seq 24); do
|
|
|
|
|
|
line=$($DC exec -T cli2 curl -s --max-time 5 "http://$P2_VIP/" 2>/dev/null) || continue
|
|
|
|
|
|
b=$(echo "$line" | sed -n 's/.*backend=\([a-z0-9]*\).*/\1/p')
|
|
|
|
|
|
c=$(echo "$line" | sed -n 's/.*client=\([0-9.]*\):.*/\1/p')
|
|
|
|
|
|
sv=$(echo "$line" | sed -n 's/.*served=\([0-9.]*:[0-9]*\).*/\1/p')
|
|
|
|
|
|
[ -n "$b" ] && phits[$b]=$(( ${phits[$b]:-0} + 1 ))
|
|
|
|
|
|
[ -n "$c" ] && pclient=$c
|
|
|
|
|
|
case "$b" in be1|be3) pown=$((pown + 1)); pserved=$sv ;; esac
|
|
|
|
|
|
done
|
|
|
|
|
|
echo " распределение: $(for k in "${!phits[@]}"; do printf '%s=%s ' "$k" "${phits[$k]}"; done)"
|
|
|
|
|
|
[ "${#phits[@]}" -ge 3 ] && ok "приватный листенер балансирует между ${#phits[@]} членами из 4" \
|
|
|
|
|
|
|| bad "задействовано лишь ${#phits[@]} членов"
|
|
|
|
|
|
|
|
|
|
|
|
# Ключевая проверка шага: бэкенд видит исходный адрес клиента.
|
|
|
|
|
|
[ "$pclient" = "10.20.0.10" ] && ok "бэкенд видит исходный IP клиента: $pclient (SNAT не выполняется)" \
|
|
|
|
|
|
|| bad "бэкенд видит $pclient вместо 10.20.0.10"
|
|
|
|
|
|
|
|
|
|
|
|
# Тот самый случай: член пула в одном сегменте с клиентом. Ответ уходит по L2
|
|
|
|
|
|
# напрямую, но проходит через br-lb, где таблица 24 снимает трансляцию.
|
|
|
|
|
|
[ "$pown" -gt 0 ] && ok "члены из сегмента клиента обслужили $pown запросов" \
|
|
|
|
|
|
|| bad "ни один запрос не попал на be1/be3 — коммутируемый путь не проверен"
|
|
|
|
|
|
case "$pserved" in
|
|
|
|
|
|
10.20.0.[23]:8080) ok "трансляция порта выполнена: листенер 80 -> член $pserved" ;;
|
|
|
|
|
|
"") bad "не удалось определить адрес обслуживания" ;;
|
|
|
|
|
|
*) bad "неожиданный адрес обслуживания: $pserved" ;;
|
|
|
|
|
|
esac
|
|
|
|
|
|
|
|
|
|
|
|
# Путь остался коммутируемым: TTL ответа от VIP не уменьшен, значит клиент и
|
|
|
|
|
|
# бэкенд по-прежнему L2-соседи и балансировщик не стал для них L3-хопом.
|
2026-08-19 12:33:48 +03:00
|
|
|
|
# Захватываем с запасом: пул из 4 членов, capture -c должен гарантированно
|
|
|
|
|
|
# застать хотя бы один ответ от соседа по сегменту (be1/be3), а не только от
|
|
|
|
|
|
# кросс-сегментных (be2/be4) — иначе проверка становится статистически
|
|
|
|
|
|
# хрупкой на малой выборке.
|
2026-08-17 16:59:16 +03:00
|
|
|
|
ttls=$(mktemp)
|
2026-08-19 12:33:48 +03:00
|
|
|
|
timeout 20 $DC exec -T lb-router timeout 15 tcpdump -ni cli2 -c 16 -v \
|
2026-08-17 16:59:16 +03:00
|
|
|
|
"tcp and src host $P2_VIP and tcp[tcpflags] & tcp-syn != 0" >"$ttls" 2>/dev/null &
|
|
|
|
|
|
tpid=$!
|
|
|
|
|
|
sleep 1
|
2026-08-19 12:33:48 +03:00
|
|
|
|
for _ in $(seq 32); do $DC exec -T cli2 curl -s --max-time 3 -o /dev/null "http://$P2_VIP/" 2>/dev/null; done
|
2026-08-17 16:59:16 +03:00
|
|
|
|
wait $tpid 2>/dev/null
|
|
|
|
|
|
grep -q 'ttl 64' "$ttls" && ok "ответ от VIP приходит с TTL 64 — путь коммутируемый, не маршрутный" \
|
|
|
|
|
|
|| bad "TTL ответа уменьшен: путь стал маршрутизируемым"
|
|
|
|
|
|
rm -f "$ttls"
|
|
|
|
|
|
|
|
|
|
|
|
head_ "16. Внутрисегментный трафик не блокирован"
|
|
|
|
|
|
out=$($DC exec -T cli2 curl -s --max-time 5 http://10.20.0.2:8080/ 2>/dev/null)
|
|
|
|
|
|
echo "$out" | grep -q 'backend=be1' && ok "клиент обращается к бэкенду напрямую, минуя VIP" \
|
|
|
|
|
|
|| bad "прямое обращение клиента к бэкенду не работает"
|
|
|
|
|
|
out=$($DC exec -T be1 curl -s --max-time 5 http://10.20.0.3:8080/ 2>/dev/null)
|
|
|
|
|
|
echo "$out" | grep -q 'backend=be3' && ok "be1 -> be3 внутри сегмента" \
|
|
|
|
|
|
|| bad "связность между бэкендами сегмента нарушена"
|
|
|
|
|
|
$DC exec -T be1 ping -c1 -W2 10.20.0.254 >/dev/null 2>&1 \
|
|
|
|
|
|
&& ok "бэкенд достаёт шлюз Docker через аплинк сегмента" \
|
|
|
|
|
|
|| bad "шлюз сегмента недоступен"
|
|
|
|
|
|
$DC exec -T cli3 curl -s --max-time 5 http://10.30.0.3:8080/ 2>/dev/null | grep -q 'backend=be4' \
|
|
|
|
|
|
&& ok "cli3 -> be4 во втором сегменте" || bad "связность второго сегмента нарушена"
|
|
|
|
|
|
|
2026-08-19 12:33:48 +03:00
|
|
|
|
head_ "17. Динамическое изучение MAC/ARP (шаг 5)"
|
|
|
|
|
|
# MAC членов пула больше не статичен: hcd резолвит его через обычный ARP на
|
|
|
|
|
|
# hcif-портах (ядро) и сам заливает таблицу 21. Проверяем, что резолвленный
|
|
|
|
|
|
# MAC совпадает с реальным MAC интерфейса ВМ, а не с константой topology.env.
|
|
|
|
|
|
neigh=$($DC exec -T lb-router lbctl neigh 2>/dev/null)
|
|
|
|
|
|
for be in be1:10.20.0.2 be2:10.30.0.2 be3:10.20.0.3 be4:10.30.0.3; do
|
|
|
|
|
|
name=${be%%:*}; ip=${be#*:}
|
|
|
|
|
|
real_mac=$($DC exec -T "$name" ip -o link show eth0 2>/dev/null | sed -n 's/.*link\/ether \([0-9a-f:]*\).*/\1/p')
|
|
|
|
|
|
resolved=$(echo "$neigh" | awk -v n="$name" '$1==n{print $4}')
|
|
|
|
|
|
if [ -n "$real_mac" ] && [ "$resolved" = "$real_mac" ]; then
|
|
|
|
|
|
ok "$name: hcd резолвил реальный MAC $real_mac (не из topology.env)"
|
|
|
|
|
|
else
|
|
|
|
|
|
bad "$name: резолвлен $resolved, реальный MAC интерфейса $real_mac"
|
|
|
|
|
|
fi
|
|
|
|
|
|
done
|
|
|
|
|
|
|
|
|
|
|
|
# Таблица 21 — единственное место с MAC следующего перехода; правило члена
|
|
|
|
|
|
# должно быть приоритета 100 (заливка hcd), а не 90 (пассивный learn клиента).
|
|
|
|
|
|
# Проверка заодно закрывает регрессию force-реапплая: apply.sh уже вызывался
|
|
|
|
|
|
# в блоке 14 и стирал весь пайплайн вместе с таблицей 21 — если бы /reapply
|
|
|
|
|
|
# не восстанавливал её принудительно, этих правил здесь бы уже не было.
|
|
|
|
|
|
t21=$($DC exec -T lb-router ovs-ofctl -O OpenFlow15 dump-flows br-lb table=21 2>/dev/null)
|
|
|
|
|
|
for ip in 10.20.0.2 10.30.0.2 10.20.0.3 10.30.0.3; do
|
|
|
|
|
|
echo "$t21" | grep -q "priority=100,ip,nw_dst=$ip " \
|
|
|
|
|
|
&& ok "таблица 21: приоритет-100 правило для $ip пережило apply.sh" \
|
|
|
|
|
|
|| bad "таблица 21: нет приоритет-100 правила для $ip"
|
|
|
|
|
|
done
|
|
|
|
|
|
|
2026-08-16 21:43:02 +03:00
|
|
|
|
echo
|
|
|
|
|
|
echo "Итог: успешно $pass, провалено $fail"
|
|
|
|
|
|
[ "$fail" -eq 0 ]
|