Files
ayurishchevandClaude Opus 5 fa389f119b Шаг 5: динамическое изучение MAC/ARP для бэкендов
MAC каждого члена пула больше не статическая константа в topology.env,
а резолвится демоном hcd через обычный ARP ядра — в реальном
окружении MAC бэкенда заранее не известен (сервер ещё не подключён,
NIC может замениться), топология не описывается статически, в отличие
от стенда.

hcif-порты (единственные адреса узла в ядре) уже были настоящими
L3-интерфейсами в тех же сегментах, что и бэкенды — единственное, что
мешало обычному ARP, это permanent-записи ip neigh в entrypoint.sh.
Убрав их и добавив hc/neigh.go (читает ip -json neigh show, точечно
заливает бандл в таблицу 21 на том же тикере, что и health-пробы),
получили резолвер без нового OpenFlow-контроллера.

Таблица 21 стала единственным источником MAC для обоих путей —
маршрутизируемого и коммутируемого (шаг 3): таблица 12 больше не
дублирует MAC инлайново, ct(commit) ведёт сразу в таблицу 21.

Исправлен попутно найденный баг: после apply.sh (replace-flows)
таблица 21 не восстанавливалась, поскольку syncNeighbors сравнивал
MAC с памятью демона, а не с датапасом. Добавлен force-режим по
аналогии с Agent.apply(), плюс регрессионная проверка в verify.sh.

Проверено на живом стенде: MAC всех четырёх членов резолвлен и
совпадает с реальными интерфейсами; смена MAC "железа" обнаружена и
применена без вмешательства за счёт штатного старения ARP ядра.
Регрессия: 94 из 94 проверок.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 12:33:48 +03:00

132 lines
6.4 KiB
Bash
Executable File

#!/bin/bash
# Подъём Open vSwitch внутри контейнера и сборка моста br-lb.
#
# Контейнер работает как чистый OpenFlow-роутер: IP-адреса на интерфейсах не
# настраиваются, сетевой стек ядра в форвардинге не участвует. Вся L3-логика
# (ARP, маршрутизация, DNAT) живёт в таблицах OpenFlow — см. pipeline.sh.
set -euo pipefail
LB_DIR=/opt/lb
# shellcheck disable=SC1091
source "$LB_DIR/topology.env"
log() { echo "[lb-router] $*"; }
die() { echo "[lb-router] ОШИБКА: $*" >&2; exit 1; }
# --- 1. Kernel datapath ------------------------------------------------------
log "загрузка модуля openvswitch"
modprobe openvswitch 2>/dev/null || true
grep -q '^openvswitch ' /proc/modules || die \
"модуль openvswitch не загружен. Выполните на хосте: modprobe openvswitch (см. scripts/host-prereq.sh)"
# --- 2. Запуск ovsdb-server и ovs-vswitchd -----------------------------------
mkdir -p /var/run/openvswitch /var/log/openvswitch /etc/openvswitch
rm -f /var/run/openvswitch/*.pid
if [ ! -f /etc/openvswitch/conf.db ]; then
log "инициализация OVSDB"
ovsdb-tool create /etc/openvswitch/conf.db /usr/share/openvswitch/vswitch.ovsschema
fi
log "запуск ovsdb-server"
ovsdb-server /etc/openvswitch/conf.db \
--remote=punix:/var/run/openvswitch/db.sock \
--remote=db:Open_vSwitch,Open_vSwitch,manager_options \
--pidfile --detach --log-file
ovs-vsctl --no-wait init
log "запуск ovs-vswitchd"
ovs-vswitchd --pidfile --detach --log-file
# --- 3. Мост -----------------------------------------------------------------
log "создание моста $BR"
ovs-vsctl --may-exist add-br "$BR" \
-- set bridge "$BR" fail_mode=secure \
-- set bridge "$BR" protocols=OpenFlow13,OpenFlow15 \
-- set bridge "$BR" other-config:disable-in-band=true
ip link set dev "$BR" up
# --- 4. Перенос интерфейсов Docker в мост ------------------------------------
# Интерфейсы опознаются по MAC (имена eth0/eth1/eth2 Docker раздаёт в
# непредсказуемом порядке), переименовываются в осмысленные и лишаются IP:
# адрес нужен только Docker IPAM, чтобы зарезервировать его за контейнером.
find_dev_by_mac() {
local mac="$1" d
for d in /sys/class/net/*; do
[ -f "$d/address" ] || continue
[ "$(cat "$d/address")" = "$mac" ] && { basename "$d"; return 0; }
done
return 1
}
attach_port() {
local mac="$1" name="$2" ofport="$3" dev
dev=$(find_dev_by_mac "$mac") || die "интерфейс с MAC $mac не найден"
if [ "$dev" != "$name" ]; then
ip link set dev "$dev" down
ip addr flush dev "$dev"
ip link set dev "$dev" name "$name"
else
ip addr flush dev "$name"
fi
ip link set dev "$name" up
ovs-vsctl --may-exist add-port "$BR" "$name" \
-- set Interface "$name" ofport_request="$ofport"
log "порт $name (MAC $mac) -> $BR, ofport $ofport"
}
attach_port "$PUB_MAC" "$PUB_IFNAME" "$PUB_OFPORT"
attach_port "$P2_MAC" "$P2_IFNAME" "$P2_OFPORT"
attach_port "$P3_MAC" "$P3_IFNAME" "$P3_OFPORT"
# --- 4a. Порты-источники health-проб -----------------------------------------
# Единственные адреса, которые узел держит в ядре: с них уходят пробы (§7.1
# дизайна v1 — источник проб должен быть уникальным адресом узла, не VIP).
#
# MAC членов пула здесь больше НЕ прописывается статически (шаг 5): hcif-порты
# — настоящие L3-интерфейсы ядра в том же сегменте, что и бэкенды, и ядро само
# резолвит их MAC обычным ARP. Первая проба после старта чуть медленнее (кадр
# ARP-запроса уходит через br-lb и возвращается ответом реального бэкенда), но
# дальше запись живёт в neigh-таблице ядра обычным старением ARP — именно её
# читает hcd (hc/neigh.go) и заливает в таблицу 21 OpenFlow.
hc_port() {
local name="$1" ofport="$2" mac="$3" ip="$4" net="$5"
ovs-vsctl --may-exist add-port "$BR" "$name" \
-- set Interface "$name" type=internal ofport_request="$ofport" \
-- set Interface "$name" mac="\"$mac\""
ip link set dev "$name" address "$mac"
ip addr replace "$ip/${net##*/}" dev "$name"
ip link set dev "$name" up
log "порт $name ($ip) — источник health-проб"
}
hc_port "$HC2_IFNAME" "$HC2_OFPORT" "$HC2_MAC" "$HC2_IP" "$P2_NET"
hc_port "$HC3_IFNAME" "$HC3_OFPORT" "$HC3_MAC" "$HC3_IP" "$P3_NET"
# Ждём, пока vswitchd реально привяжет порты к датапасу.
for name in "$PUB_IFNAME" "$P2_IFNAME" "$P3_IFNAME"; do
for _ in $(seq 30); do
ofport=$(ovs-vsctl --if-exists get Interface "$name" ofport || echo -1)
[ "$ofport" -gt 0 ] 2>/dev/null && break
sleep 0.5
done
[ "${ofport:-0}" -gt 0 ] 2>/dev/null || die "порт $name не привязался к датапасу: $(ovs-vsctl get Interface "$name" error 2>/dev/null || true)"
done
# --- 5. Пайплайн -------------------------------------------------------------
# Таблица слотов здесь пуста (fail-close) — её наполнит hcd после первых
# успешных проб, поэтому apply.sh на этом этапе демона ещё не дёргает.
"$LB_DIR/apply.sh" --no-reapply
log "готово. Порты: $(ovs-vsctl list-ports "$BR" | tr '\n' ' ')"
# --- 6. Health-check ---------------------------------------------------------
# Демон становится основным процессом контейнера; ovsdb-server и ovs-vswitchd
# работают демонами рядом. Логи проб и переходов видны в docker logs.
CONF=$("$LB_DIR/hc-config.sh")
log "конфигурация health-check: $CONF"
tail -F --pid=$$ /var/log/openvswitch/ovs-vswitchd.log 2>/dev/null &
exec hcd -config "$CONF"