134 lines
5.4 KiB
Go
134 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
|
|||
|
|
}
|