Files
hpnn-proto/scripts/verify.sh
T

246 lines
14 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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. Контейнеры"
for c in hpnn-lb hpnn-be1 hpnn-be2 hpnn-be3 hpnn-be4; do
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
dp=$($DC exec -T lb-router ovs-appctl dpif/show 2>/dev/null | head -1)
[ -n "$dp" ] && ok "датапас: $dp" || bad "датапас не поднят"
head_ "3. Пайплайн"
for t in 0 5 6 10 11 12 15 16 20 21; do
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
head_ "7. Профиль A: собственный egress бэкенда идёт мимо балансировщика"
dump=$(mktemp)
timeout 12 $DC exec -T lb-router timeout 8 tcpdump -ni p2 -c 2 'host 1.1.1.1' >"$dump" 2>&1 &
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
bad "трафик egress виден на порту p2 балансировщика:"; sed 's/^/ /' "$dump"
else
ok "на порту p2 балансировщика этого трафика нет"
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"
src=$($DC exec -T lb-router timeout 6 tcpdump -ni p2 -c 1 'tcp port 8080 and tcp[tcpflags] & tcp-syn != 0' 2>/dev/null \
| 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)"
echo
echo "Итог: успешно $pass, провалено $fail"
[ "$fail" -eq 0 ]