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>
135 lines
5.4 KiB
Go
135 lines
5.4 KiB
Go
package main
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"log"
|
||
"os"
|
||
"os/exec"
|
||
"time"
|
||
)
|
||
|
||
// Резолв MAC членов пула через neigh-таблицу ядра — шаг 5.
|
||
//
|
||
// Адреса членов известны из конфигурации заранее, их MAC — нет (в реальном
|
||
// окружении сервер ещё не подключён, NIC может замениться). hcif-порты —
|
||
// настоящие L3-интерфейсы ядра в том же сегменте, что и члены пула, поэтому
|
||
// ядро само резолвит их MAC обычным ARP (entrypoint.sh больше не подсовывает
|
||
// статические permanent-записи). hcd лишь читает то, что уже узнало ядро, и
|
||
// отражает это в единственном месте датапаса, где применяется MAC следующего
|
||
// перехода, — в таблице 21 (см. pipeline.sh).
|
||
//
|
||
// Для клиентов (адрес заранее не известен и не enumerable) резолв остаётся
|
||
// пассивным действием learn в самом OpenFlow — здесь не участвует.
|
||
|
||
// neighEntry — одна запись `ip -json neigh show`.
|
||
type neighEntry struct {
|
||
Dst string `json:"dst"`
|
||
Dev string `json:"dev"`
|
||
Lladdr string `json:"lladdr"`
|
||
State []string `json:"state"`
|
||
}
|
||
|
||
// resolvedStates — состояния ARP, при которых MAC считается годным к
|
||
// использованию. FAILED и INCOMPLETE (или отсутствие записи) означают, что
|
||
// адрес пока не резолвлен.
|
||
var resolvedStates = map[string]bool{
|
||
"REACHABLE": true,
|
||
"STALE": true,
|
||
"DELAY": true,
|
||
"PROBE": true,
|
||
"PERMANENT": true,
|
||
}
|
||
|
||
// resolveMAC возвращает MAC адреса ip на интерфейсе iface, если ядро его уже
|
||
// резолвило. Пустая строка — адрес пока не резолвлен либо недостижим.
|
||
func resolveMAC(iface, ip string) (string, error) {
|
||
out, err := exec.Command("ip", "-json", "neigh", "show", "dev", iface, "to", ip).Output()
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
var entries []neighEntry
|
||
if err := json.Unmarshal(out, &entries); err != nil {
|
||
return "", fmt.Errorf("разбор ip -json neigh: %w", err)
|
||
}
|
||
for _, e := range entries {
|
||
if e.Lladdr == "" {
|
||
continue
|
||
}
|
||
for _, s := range e.State {
|
||
if resolvedStates[s] {
|
||
return e.Lladdr, nil
|
||
}
|
||
}
|
||
}
|
||
return "", nil
|
||
}
|
||
|
||
// syncNeighbors проходит по всем членам, резолвит их MAC через ядро и для
|
||
// каждого изменения атомарно обновляет его правило в таблице adjacency —
|
||
// точечным бандлом, не трогая записи остальных членов. Вызывается на том же
|
||
// тикере, что и health-пробы.
|
||
//
|
||
// force заливает правило заново, даже если MAC не изменился с прошлого раза:
|
||
// нужно после apply.sh (replace-flows стирает весь пайплайн, включая уже
|
||
// залитые сюда правила, а resolveMAC() в следующий раз вернёт тот же MAC и
|
||
// без force заливка не повторится). Тот же приём, что у Agent.apply(force).
|
||
func (a *Agent) syncNeighbors(force bool) {
|
||
for _, m := range a.cfg.Members {
|
||
mac, err := resolveMAC(m.Iface, m.Address)
|
||
if err != nil {
|
||
log.Printf("neigh: %s (%s@%s): %v", m.Name, m.Address, m.Iface, err)
|
||
continue
|
||
}
|
||
|
||
m.mu.Lock()
|
||
changed := force || mac != m.mac
|
||
if mac != m.mac {
|
||
m.macSince = time.Now()
|
||
}
|
||
m.mac = mac
|
||
m.mu.Unlock()
|
||
|
||
if !changed {
|
||
continue
|
||
}
|
||
if mac == "" {
|
||
log.Printf("neigh: %s (%s) MAC утерян — снимаю запись adjacency", m.Name, m.Address)
|
||
if err := a.applyNeigh(m); err != nil {
|
||
log.Printf("neigh: %s: ошибка снятия записи: %v", m.Name, err)
|
||
}
|
||
continue
|
||
}
|
||
log.Printf("neigh: %s (%s@%s) -> %s", m.Name, m.Address, m.Iface, mac)
|
||
if err := a.applyNeigh(m); err != nil {
|
||
log.Printf("neigh: %s: ошибка применения: %v", m.Name, err)
|
||
}
|
||
}
|
||
}
|
||
|
||
// applyNeigh заливает точечный бандл для ОДНОГО члена: снимает прежнее
|
||
// правило adjacency (если было) и, если MAC резолвлен, ставит новое. Формат
|
||
// файла бандла — тот же, что и для таблицы слотов (FlowBundle в maglev.go).
|
||
func (a *Agent) applyNeigh(m *Member) error {
|
||
m.mu.Lock()
|
||
mac := m.mac
|
||
m.mu.Unlock()
|
||
|
||
table := a.cfg.AdjTable
|
||
out := fmt.Sprintf("flow delete table=%d,ip,nw_dst=%s\n", table, m.Address)
|
||
if mac != "" {
|
||
out += fmt.Sprintf("flow add table=%d,priority=100,ip,nw_dst=%s actions=mod_dl_dst:%s,output:%d\n",
|
||
table, m.Address, mac, m.OFPort)
|
||
}
|
||
|
||
path := fmt.Sprintf("/var/run/openvswitch/neigh-%s.bundle", m.Name)
|
||
if err := os.WriteFile(path, []byte(out), 0o644); err != nil {
|
||
return err
|
||
}
|
||
res, err := exec.Command("ovs-ofctl", "-O", a.cfg.OFVersion, "bundle", a.cfg.Bridge, path).CombinedOutput()
|
||
if err != nil {
|
||
return fmt.Errorf("ovs-ofctl bundle: %v: %s", err, res)
|
||
}
|
||
return nil
|
||
}
|