mirror of
https://github.com/kernelkit/infix.git
synced 2026-08-01 21:33:02 +02:00
yangerd: fix stp status
This commit is contained in:
@@ -503,6 +503,19 @@ func lagAugment(iplink map[string]any) map[string]any {
|
||||
return bond
|
||||
}
|
||||
|
||||
func bridgePortSTP(info map[string]any) map[string]any {
|
||||
state := getString(info, "state")
|
||||
if state == "" {
|
||||
return map[string]any{}
|
||||
}
|
||||
|
||||
return map[string]any{
|
||||
"cist": map[string]any{
|
||||
"state": state,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func bridgePortLower(iplink map[string]any) map[string]any {
|
||||
master := getString(iplink, "master")
|
||||
if master == "" {
|
||||
@@ -526,7 +539,7 @@ func bridgePortLower(iplink map[string]any) map[string]any {
|
||||
"fast-leave": getBool(info, "fastleave"),
|
||||
"router": bridgeRouterMode(getIntOrZero(info, "multicast_router")),
|
||||
},
|
||||
"stp": map[string]any{},
|
||||
"stp": bridgePortSTP(info),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/kernelkit/infix/src/yangerd/internal/bridgebatch"
|
||||
"github.com/kernelkit/infix/src/yangerd/internal/iface"
|
||||
"github.com/kernelkit/infix/src/yangerd/internal/ipbatch"
|
||||
"github.com/kernelkit/infix/src/yangerd/internal/stpquery"
|
||||
"github.com/kernelkit/infix/src/yangerd/internal/tree"
|
||||
"github.com/vishvananda/netlink"
|
||||
"github.com/vishvananda/netlink/nl"
|
||||
@@ -386,20 +387,26 @@ func (m *NLMonitor) refreshInterface(name string) {
|
||||
// Caller must NOT hold m.mu.
|
||||
func (m *NLMonitor) rebuild() {
|
||||
m.mu.Lock()
|
||||
linksCopy := append(json.RawMessage{}, m.links...)
|
||||
doc := iface.Transform(m.links, m.addrs, m.links, m.fc)
|
||||
eth := copyStringMap(m.ethernet)
|
||||
wfi := copyStringMap(m.wifi)
|
||||
fdb := copyStringMap(m.fdb)
|
||||
mdb := copyStringMap(m.mdb)
|
||||
doc = mergeAugments(doc, eth, wfi, fdb, mdb)
|
||||
m.tree.Set(treeKey, doc)
|
||||
m.mu.Unlock()
|
||||
|
||||
var brSTP, ptSTP map[string]json.RawMessage
|
||||
resolver := stpquery.NewLinksIfIndexResolver(linksCopy)
|
||||
brSTP, ptSTP = stpquery.Query(linksCopy, resolver)
|
||||
|
||||
doc = mergeAugments(doc, eth, wfi, fdb, mdb, brSTP, ptSTP)
|
||||
m.tree.Set(treeKey, doc)
|
||||
}
|
||||
|
||||
// mergeAugments adds ethernet, wifi, and bridge data into the
|
||||
// complete ietf-interfaces document produced by iface.Transform().
|
||||
func mergeAugments(doc json.RawMessage, ethernet, wifi, fdb, mdb map[string]json.RawMessage) json.RawMessage {
|
||||
if len(ethernet) == 0 && len(wifi) == 0 && len(fdb) == 0 && len(mdb) == 0 {
|
||||
func mergeAugments(doc json.RawMessage, ethernet, wifi, fdb, mdb, bridgeSTP, portSTP map[string]json.RawMessage) json.RawMessage {
|
||||
if len(ethernet) == 0 && len(wifi) == 0 && len(fdb) == 0 && len(mdb) == 0 && len(bridgeSTP) == 0 && len(portSTP) == 0 {
|
||||
return doc
|
||||
}
|
||||
|
||||
@@ -457,6 +464,22 @@ func mergeAugments(doc json.RawMessage, ethernet, wifi, fdb, mdb map[string]json
|
||||
}
|
||||
}
|
||||
|
||||
if stpData, ok := bridgeSTP[name]; ok {
|
||||
bridgeObj := ensureBridgeAugment(ifaceObj)
|
||||
var stpObj any
|
||||
if err := json.Unmarshal(stpData, &stpObj); err == nil {
|
||||
bridgeObj["stp"] = stpObj
|
||||
}
|
||||
}
|
||||
|
||||
if stpData, ok := portSTP[name]; ok {
|
||||
bpObj := ensureBridgePortAugment(ifaceObj)
|
||||
var stpObj any
|
||||
if err := json.Unmarshal(stpData, &stpObj); err == nil {
|
||||
deepMergeSTP(bpObj, stpObj)
|
||||
}
|
||||
}
|
||||
|
||||
ifaceArr[i] = ifaceObj
|
||||
}
|
||||
|
||||
@@ -481,6 +504,52 @@ func ensureBridgeAugment(ifaceObj map[string]any) map[string]any {
|
||||
return bridgeObj
|
||||
}
|
||||
|
||||
func ensureBridgePortAugment(ifaceObj map[string]any) map[string]any {
|
||||
key := "infix-interfaces:bridge-port"
|
||||
if existing, ok := ifaceObj[key]; ok {
|
||||
if m, ok := existing.(map[string]any); ok {
|
||||
return m
|
||||
}
|
||||
}
|
||||
obj := map[string]any{}
|
||||
ifaceObj[key] = obj
|
||||
return obj
|
||||
}
|
||||
|
||||
// deepMergeSTP merges mstpd STP data into the bridge-port augment.
|
||||
// The kernel already provides stp.cist.state via iface.Transform;
|
||||
// mstpd adds role, port-id, designated, etc. We deep-merge to
|
||||
// preserve the kernel state field while adding mstpd fields.
|
||||
func deepMergeSTP(bpObj map[string]any, stpData any) {
|
||||
stpMap, ok := stpData.(map[string]any)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
existing, _ := bpObj["stp"].(map[string]any)
|
||||
if existing == nil {
|
||||
bpObj["stp"] = stpMap
|
||||
return
|
||||
}
|
||||
|
||||
if newCist, ok := stpMap["cist"].(map[string]any); ok {
|
||||
existingCist, _ := existing["cist"].(map[string]any)
|
||||
if existingCist == nil {
|
||||
existing["cist"] = newCist
|
||||
} else {
|
||||
for k, v := range newCist {
|
||||
existingCist[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for k, v := range stpMap {
|
||||
if k != "cist" {
|
||||
existing[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func copyStringMap(m map[string]json.RawMessage) map[string]json.RawMessage {
|
||||
if len(m) == 0 {
|
||||
return nil
|
||||
|
||||
@@ -344,7 +344,7 @@ func TestMergeAugments(t *testing.T) {
|
||||
"br0": json.RawMessage(`[{"mac":"00:11:22:33:44:55"}]`),
|
||||
}
|
||||
|
||||
got := mergeAugments(doc, eth, nil, fdb, nil)
|
||||
got := mergeAugments(doc, eth, nil, fdb, nil, nil, nil)
|
||||
|
||||
var root map[string]any
|
||||
if err := json.Unmarshal(got, &root); err != nil {
|
||||
@@ -370,7 +370,7 @@ func TestMergeAugments(t *testing.T) {
|
||||
|
||||
func TestMergeAugmentsNoOp(t *testing.T) {
|
||||
doc := json.RawMessage(`{"interface":[{"name":"lo"}]}`)
|
||||
got := mergeAugments(doc, nil, nil, nil, nil)
|
||||
got := mergeAugments(doc, nil, nil, nil, nil, nil, nil)
|
||||
if string(got) != string(doc) {
|
||||
t.Fatalf("expected no-op, got %s", string(got))
|
||||
}
|
||||
@@ -379,7 +379,7 @@ func TestMergeAugmentsNoOp(t *testing.T) {
|
||||
func TestMergeAugmentsInvalidDoc(t *testing.T) {
|
||||
doc := json.RawMessage(`{invalid`)
|
||||
eth := map[string]json.RawMessage{"eth0": json.RawMessage(`{}`)}
|
||||
got := mergeAugments(doc, eth, nil, nil, nil)
|
||||
got := mergeAugments(doc, eth, nil, nil, nil, nil, nil)
|
||||
if string(got) != string(doc) {
|
||||
t.Fatalf("expected passthrough on invalid doc, got %s", string(got))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,636 @@
|
||||
// Package stpquery provides a native Go client for querying mstpd's
|
||||
// operational data over its abstract Unix datagram socket. It decodes
|
||||
// the binary wire protocol directly — no subprocess, no CGo.
|
||||
package stpquery
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
cmdGetCISTBridgeStatus = 101
|
||||
cmdGetCISTPortStatus = 105
|
||||
serverSocketName = ".mstp_server"
|
||||
)
|
||||
|
||||
type ctlMsgHdr struct {
|
||||
Cmd, Lin, Lout, Llog, Res int32
|
||||
}
|
||||
|
||||
const hdrSize = 20
|
||||
|
||||
type Client struct {
|
||||
fd int
|
||||
}
|
||||
|
||||
// sockaddrUN is the full-size struct sockaddr_un used by mstpd.
|
||||
// mstpd passes sizeof(struct sockaddr_un) to bind/connect, so the
|
||||
// abstract name is zero-padded to fill the entire sun_path[108].
|
||||
// Go's net package uses minimal length, which produces a different
|
||||
// abstract socket name. We must match mstpd's behavior exactly.
|
||||
type sockaddrUN struct {
|
||||
Family uint16
|
||||
Path [108]byte
|
||||
}
|
||||
|
||||
func setSockAddr(sa *sockaddrUN, name string) {
|
||||
sa.Family = syscall.AF_UNIX
|
||||
copy(sa.Path[1:], name)
|
||||
}
|
||||
|
||||
var connSeq atomic.Uint64
|
||||
|
||||
func New() (*Client, error) {
|
||||
fd, err := syscall.Socket(syscall.AF_UNIX, syscall.SOCK_DGRAM, 0)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("socket: %w", err)
|
||||
}
|
||||
|
||||
seq := connSeq.Add(1)
|
||||
var local sockaddrUN
|
||||
setSockAddr(&local, fmt.Sprintf("MSTPCTL_%d_%d", os.Getpid(), seq))
|
||||
_, _, errno := syscall.Syscall(syscall.SYS_BIND, uintptr(fd),
|
||||
uintptr(unsafe.Pointer(&local)), unsafe.Sizeof(local))
|
||||
if errno != 0 {
|
||||
syscall.Close(fd)
|
||||
return nil, fmt.Errorf("bind: %w", errno)
|
||||
}
|
||||
|
||||
var remote sockaddrUN
|
||||
setSockAddr(&remote, serverSocketName)
|
||||
_, _, errno = syscall.Syscall(syscall.SYS_CONNECT, uintptr(fd),
|
||||
uintptr(unsafe.Pointer(&remote)), unsafe.Sizeof(remote))
|
||||
if errno != 0 {
|
||||
syscall.Close(fd)
|
||||
return nil, fmt.Errorf("connect to mstpd: %w", errno)
|
||||
}
|
||||
|
||||
return &Client{fd: fd}, nil
|
||||
}
|
||||
|
||||
func (c *Client) Close() error {
|
||||
if c.fd >= 0 {
|
||||
err := syscall.Close(c.fd)
|
||||
c.fd = -1
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) roundTrip(cmd int32, in []byte, outSize int) ([]byte, error) {
|
||||
hdr := ctlMsgHdr{
|
||||
Cmd: cmd,
|
||||
Lin: int32(len(in)),
|
||||
Lout: int32(outSize),
|
||||
}
|
||||
|
||||
buf := make([]byte, hdrSize+len(in))
|
||||
binary.NativeEndian.PutUint32(buf[0:4], uint32(hdr.Cmd))
|
||||
binary.NativeEndian.PutUint32(buf[4:8], uint32(hdr.Lin))
|
||||
binary.NativeEndian.PutUint32(buf[8:12], uint32(hdr.Lout))
|
||||
binary.NativeEndian.PutUint32(buf[12:16], uint32(hdr.Llog))
|
||||
binary.NativeEndian.PutUint32(buf[16:20], uint32(hdr.Res))
|
||||
copy(buf[hdrSize:], in)
|
||||
|
||||
tv := syscall.Timeval{Sec: 5}
|
||||
syscall.SetsockoptTimeval(c.fd, syscall.SOL_SOCKET, syscall.SO_SNDTIMEO, &tv)
|
||||
syscall.SetsockoptTimeval(c.fd, syscall.SOL_SOCKET, syscall.SO_RCVTIMEO, &tv)
|
||||
|
||||
if err := syscall.Sendmsg(c.fd, buf, nil, nil, 0); err != nil {
|
||||
return nil, fmt.Errorf("write to mstpd: %w", err)
|
||||
}
|
||||
|
||||
resp := make([]byte, hdrSize+outSize+4096)
|
||||
n, _, _, _, err := syscall.Recvmsg(c.fd, resp, nil, 0)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read from mstpd: %w", err)
|
||||
}
|
||||
if n < hdrSize {
|
||||
return nil, fmt.Errorf("mstpd response too short: %d bytes", n)
|
||||
}
|
||||
|
||||
resCode := int32(binary.NativeEndian.Uint32(resp[16:20]))
|
||||
if resCode != 0 {
|
||||
return nil, fmt.Errorf("mstpd error: res=%d", resCode)
|
||||
}
|
||||
|
||||
lout := int(int32(binary.NativeEndian.Uint32(resp[8:12])))
|
||||
if n < hdrSize+lout {
|
||||
return nil, fmt.Errorf("mstpd response truncated: got %d, need %d", n, hdrSize+lout)
|
||||
}
|
||||
|
||||
return resp[hdrSize : hdrSize+lout], nil
|
||||
}
|
||||
|
||||
// CISTBridgeStatus holds decoded bridge-level STP data from mstpd.
|
||||
type CISTBridgeStatus struct {
|
||||
BridgeID BridgeID
|
||||
TimeSinceTopologyChange uint32
|
||||
TopologyChangeCount uint32
|
||||
TopologyChange bool
|
||||
TopologyChangePort string // max 16 chars
|
||||
LastTopologyChangePort string // max 16 chars
|
||||
DesignatedRoot BridgeID
|
||||
RootPathCost uint32
|
||||
RootPortID PortID
|
||||
RootMaxAge uint8
|
||||
RootForwardDelay uint8
|
||||
BridgeMaxAge uint8
|
||||
BridgeForwardDelay uint8
|
||||
TxHoldCount uint32
|
||||
ProtocolVersion uint32
|
||||
RegionalRoot BridgeID
|
||||
InternalPathCost uint32
|
||||
Enabled bool
|
||||
AgeingTime uint32
|
||||
MaxHops uint8
|
||||
BridgeHelloTime uint8
|
||||
RootPortName string // from get_cist_bridge_status_OUT tail
|
||||
}
|
||||
|
||||
// CISTPortStatus holds decoded port-level STP data from mstpd.
|
||||
type CISTPortStatus struct {
|
||||
Uptime uint32
|
||||
State uint32
|
||||
PortID PortID
|
||||
AdminExternalPortPathCost uint32
|
||||
ExternalPortPathCost uint32
|
||||
DesignatedRoot BridgeID
|
||||
DesignatedExternalCost uint32
|
||||
DesignatedBridge BridgeID
|
||||
DesignatedPort PortID
|
||||
TcAck bool
|
||||
PortHelloTime uint8
|
||||
AdminEdgePort bool
|
||||
AutoEdgePort bool
|
||||
OperEdgePort bool
|
||||
Enabled bool
|
||||
AdminP2P uint32
|
||||
OperP2P bool
|
||||
RestrictedRole bool
|
||||
RestrictedTCN bool
|
||||
Role uint32
|
||||
Disputed bool
|
||||
DesignatedRegionalRoot BridgeID
|
||||
DesignatedInternalCost uint32
|
||||
AdminInternalPortPathCost uint32
|
||||
InternalPortPathCost uint32
|
||||
BPDUGuardPort bool
|
||||
BPDUGuardError bool
|
||||
BPDUFilterPort bool
|
||||
NetworkPort bool
|
||||
BAInconsistent bool
|
||||
NumRxBPDUFiltered uint32
|
||||
NumRxBPDU uint32
|
||||
NumRxTCN uint32
|
||||
NumTxBPDU uint32
|
||||
NumTxTCN uint32
|
||||
NumTransFwd uint32
|
||||
NumTransBlk uint32
|
||||
RcvdBpdu bool
|
||||
RcvdRSTP bool
|
||||
RcvdSTP bool
|
||||
RcvdTcAck bool
|
||||
RcvdTcn bool
|
||||
SendRSTP bool
|
||||
}
|
||||
|
||||
// BridgeID is an 8-byte STP bridge identifier.
|
||||
type BridgeID [8]byte
|
||||
|
||||
// Priority returns the 4-bit priority value (0-15).
|
||||
func (b BridgeID) Priority() int {
|
||||
return int(b[0]) >> 4
|
||||
}
|
||||
|
||||
// SystemID returns the 12-bit system extension.
|
||||
func (b BridgeID) SystemID() int {
|
||||
return (int(b[0])&0x0f)<<8 | int(b[1])
|
||||
}
|
||||
|
||||
// Address returns the 6-byte MAC address as a colon-separated string.
|
||||
func (b BridgeID) Address() string {
|
||||
return fmt.Sprintf("%02x:%02x:%02x:%02x:%02x:%02x", b[2], b[3], b[4], b[5], b[6], b[7])
|
||||
}
|
||||
|
||||
// PortID is a 2-byte STP port identifier (big-endian on wire).
|
||||
type PortID [2]byte
|
||||
|
||||
// Priority returns the 4-bit port priority (0-15).
|
||||
func (p PortID) Priority() int {
|
||||
return int(p[0]) >> 4
|
||||
}
|
||||
|
||||
// Number returns the 12-bit port number.
|
||||
func (p PortID) Number() int {
|
||||
return (int(p[0])&0x0f)<<8 | int(p[1])
|
||||
}
|
||||
|
||||
// GetBridgeStatus queries mstpd for CIST bridge status.
|
||||
// brIndex is the kernel interface index of the bridge.
|
||||
func (c *Client) GetBridgeStatus(brIndex int) (*CISTBridgeStatus, error) {
|
||||
// Input: 4-byte int32 br_index
|
||||
in := make([]byte, 4)
|
||||
binary.NativeEndian.PutUint32(in, uint32(int32(brIndex)))
|
||||
|
||||
// Output: 128 bytes = 112 (CIST_BridgeStatus) + 16 (root_port_name)
|
||||
out, err := c.roundTrip(cmdGetCISTBridgeStatus, in, 128)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(out) < 128 {
|
||||
return nil, fmt.Errorf("bridge status response too short: %d", len(out))
|
||||
}
|
||||
|
||||
s := &CISTBridgeStatus{}
|
||||
copy(s.BridgeID[:], out[0:8])
|
||||
s.TimeSinceTopologyChange = binary.NativeEndian.Uint32(out[8:12])
|
||||
s.TopologyChangeCount = binary.NativeEndian.Uint32(out[12:16])
|
||||
s.TopologyChange = out[16] != 0
|
||||
s.TopologyChangePort = cString(out[17:33])
|
||||
s.LastTopologyChangePort = cString(out[33:49])
|
||||
copy(s.DesignatedRoot[:], out[56:64])
|
||||
s.RootPathCost = binary.NativeEndian.Uint32(out[64:68])
|
||||
s.RootPortID = PortID{out[68], out[69]}
|
||||
s.RootMaxAge = out[70]
|
||||
s.RootForwardDelay = out[71]
|
||||
s.BridgeMaxAge = out[72]
|
||||
s.BridgeForwardDelay = out[73]
|
||||
s.TxHoldCount = binary.NativeEndian.Uint32(out[76:80])
|
||||
s.ProtocolVersion = binary.NativeEndian.Uint32(out[80:84])
|
||||
copy(s.RegionalRoot[:], out[88:96])
|
||||
s.InternalPathCost = binary.NativeEndian.Uint32(out[96:100])
|
||||
s.Enabled = out[100] != 0
|
||||
s.AgeingTime = binary.NativeEndian.Uint32(out[104:108])
|
||||
s.MaxHops = out[108]
|
||||
s.BridgeHelloTime = out[109]
|
||||
// Bytes 112..127 = root_port_name[16]
|
||||
s.RootPortName = cString(out[112:128])
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// GetPortStatus queries mstpd for CIST port status.
|
||||
// brIndex and portIndex are kernel interface indices.
|
||||
func (c *Client) GetPortStatus(brIndex, portIndex int) (*CISTPortStatus, error) {
|
||||
// Input: 8 bytes = 2x int32
|
||||
in := make([]byte, 8)
|
||||
binary.NativeEndian.PutUint32(in[0:4], uint32(int32(brIndex)))
|
||||
binary.NativeEndian.PutUint32(in[4:8], uint32(int32(portIndex)))
|
||||
|
||||
// Output: 136 bytes (CIST_PortStatus)
|
||||
out, err := c.roundTrip(cmdGetCISTPortStatus, in, 136)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(out) < 136 {
|
||||
return nil, fmt.Errorf("port status response too short: %d", len(out))
|
||||
}
|
||||
|
||||
s := &CISTPortStatus{}
|
||||
s.Uptime = binary.NativeEndian.Uint32(out[0:4])
|
||||
s.State = binary.NativeEndian.Uint32(out[4:8])
|
||||
s.PortID = PortID{out[8], out[9]}
|
||||
s.AdminExternalPortPathCost = binary.NativeEndian.Uint32(out[12:16])
|
||||
s.ExternalPortPathCost = binary.NativeEndian.Uint32(out[16:20])
|
||||
copy(s.DesignatedRoot[:], out[24:32])
|
||||
s.DesignatedExternalCost = binary.NativeEndian.Uint32(out[32:36])
|
||||
copy(s.DesignatedBridge[:], out[40:48])
|
||||
s.DesignatedPort = PortID{out[48], out[49]}
|
||||
s.TcAck = out[50] != 0
|
||||
s.PortHelloTime = out[51]
|
||||
s.AdminEdgePort = out[52] != 0
|
||||
s.AutoEdgePort = out[53] != 0
|
||||
s.OperEdgePort = out[54] != 0
|
||||
s.Enabled = out[55] != 0
|
||||
s.AdminP2P = binary.NativeEndian.Uint32(out[56:60])
|
||||
s.OperP2P = out[60] != 0
|
||||
s.RestrictedRole = out[61] != 0
|
||||
s.RestrictedTCN = out[62] != 0
|
||||
s.Role = binary.NativeEndian.Uint32(out[64:68])
|
||||
s.Disputed = out[68] != 0
|
||||
copy(s.DesignatedRegionalRoot[:], out[72:80])
|
||||
s.DesignatedInternalCost = binary.NativeEndian.Uint32(out[80:84])
|
||||
s.AdminInternalPortPathCost = binary.NativeEndian.Uint32(out[84:88])
|
||||
s.InternalPortPathCost = binary.NativeEndian.Uint32(out[88:92])
|
||||
s.BPDUGuardPort = out[92] != 0
|
||||
s.BPDUGuardError = out[93] != 0
|
||||
s.BPDUFilterPort = out[94] != 0
|
||||
s.NetworkPort = out[95] != 0
|
||||
s.BAInconsistent = out[96] != 0
|
||||
s.NumRxBPDUFiltered = binary.NativeEndian.Uint32(out[100:104])
|
||||
s.NumRxBPDU = binary.NativeEndian.Uint32(out[104:108])
|
||||
s.NumRxTCN = binary.NativeEndian.Uint32(out[108:112])
|
||||
s.NumTxBPDU = binary.NativeEndian.Uint32(out[112:116])
|
||||
s.NumTxTCN = binary.NativeEndian.Uint32(out[116:120])
|
||||
s.NumTransFwd = binary.NativeEndian.Uint32(out[120:124])
|
||||
s.NumTransBlk = binary.NativeEndian.Uint32(out[124:128])
|
||||
s.RcvdBpdu = out[128] != 0
|
||||
s.RcvdRSTP = out[129] != 0
|
||||
s.RcvdSTP = out[130] != 0
|
||||
s.RcvdTcAck = out[131] != 0
|
||||
s.RcvdTcn = out[132] != 0
|
||||
s.SendRSTP = out[133] != 0
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// cString extracts a NUL-terminated C string from a byte slice.
|
||||
func cString(b []byte) string {
|
||||
for i, c := range b {
|
||||
if c == 0 {
|
||||
return string(b[:i])
|
||||
}
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// protocolName maps mstpd protocol_version to YANG force-protocol value.
|
||||
func protocolName(v uint32) string {
|
||||
switch v {
|
||||
case 0:
|
||||
return "stp"
|
||||
case 2:
|
||||
return "rstp"
|
||||
default:
|
||||
return "rstp"
|
||||
}
|
||||
}
|
||||
|
||||
// roleName maps mstpd port role to YANG role value.
|
||||
func roleName(v uint32) string {
|
||||
switch v {
|
||||
case 0:
|
||||
return "disabled"
|
||||
case 1:
|
||||
return "root"
|
||||
case 2:
|
||||
return "designated"
|
||||
case 3:
|
||||
return "alternate"
|
||||
case 4:
|
||||
return "backup"
|
||||
case 5:
|
||||
return "master"
|
||||
default:
|
||||
return "disabled"
|
||||
}
|
||||
}
|
||||
|
||||
// bridgeIDMap returns a YANG bridge-id object.
|
||||
func bridgeIDMap(b BridgeID) map[string]any {
|
||||
return map[string]any{
|
||||
"priority": b.Priority(),
|
||||
"system-id": b.SystemID(),
|
||||
"address": b.Address(),
|
||||
}
|
||||
}
|
||||
|
||||
// portIDMap returns a YANG port-id object.
|
||||
func portIDMap(p PortID) map[string]any {
|
||||
return map[string]any{
|
||||
"priority": p.Priority(),
|
||||
"port-id": p.Number(),
|
||||
}
|
||||
}
|
||||
|
||||
// IfIndexResolver looks up kernel interface indices by name.
|
||||
type IfIndexResolver interface {
|
||||
IfIndex(name string) (int, bool)
|
||||
}
|
||||
|
||||
// Query queries mstpd for STP data on all bridges found in the ip-json
|
||||
// links data. Returns per-bridge and per-port STP JSON fragments ready
|
||||
// for merging into the YANG interface tree.
|
||||
//
|
||||
// Query connects to mstpd, queries STP data for all bridges in links,
|
||||
// and returns per-bridge and per-port STP JSON fragments. A fresh
|
||||
// connection is established per call so that late-starting or restarted
|
||||
// mstpd instances are handled gracefully.
|
||||
func Query(links json.RawMessage, resolver IfIndexResolver) (bridgeSTP, portSTP map[string]json.RawMessage) {
|
||||
brs := findBridges(links)
|
||||
if len(brs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
client, err := New()
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
bridgeSTP = make(map[string]json.RawMessage)
|
||||
portSTP = make(map[string]json.RawMessage)
|
||||
|
||||
for _, br := range brs {
|
||||
brIdx, ok := resolver.IfIndex(br.name)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
bs, err := client.GetBridgeStatus(brIdx)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
stp := buildBridgeSTP(bs)
|
||||
if data, err := json.Marshal(stp); err == nil {
|
||||
bridgeSTP[br.name] = data
|
||||
}
|
||||
|
||||
for _, port := range br.ports {
|
||||
portIdx, ok := resolver.IfIndex(port)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
ps, err := client.GetPortStatus(brIdx, portIdx)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
pstp := buildPortSTP(ps)
|
||||
if data, err := json.Marshal(pstp); err == nil {
|
||||
portSTP[port] = data
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return bridgeSTP, portSTP
|
||||
}
|
||||
|
||||
func buildBridgeSTP(bs *CISTBridgeStatus) map[string]any {
|
||||
cist := map[string]any{
|
||||
"bridge-id": bridgeIDMap(bs.BridgeID),
|
||||
"root-id": bridgeIDMap(bs.DesignatedRoot),
|
||||
}
|
||||
|
||||
bid := bridgeIDMap(bs.BridgeID)
|
||||
if prio, ok := bid["priority"]; ok {
|
||||
cist["priority"] = prio
|
||||
}
|
||||
|
||||
if bs.RootPortName != "" {
|
||||
cist["root-port"] = bs.RootPortName
|
||||
}
|
||||
|
||||
if bs.TopologyChangeCount > 0 {
|
||||
tc := map[string]any{
|
||||
"count": bs.TopologyChangeCount,
|
||||
"in-progress": bs.TopologyChange,
|
||||
}
|
||||
if bs.TopologyChangePort != "" {
|
||||
tc["port"] = bs.TopologyChangePort
|
||||
}
|
||||
if bs.TimeSinceTopologyChange > 0 {
|
||||
tc["time"] = time.Now().UTC().Add(-time.Duration(bs.TimeSinceTopologyChange) * time.Second).Format(time.RFC3339)
|
||||
}
|
||||
cist["topology-change"] = tc
|
||||
}
|
||||
|
||||
stp := map[string]any{
|
||||
"force-protocol": protocolName(bs.ProtocolVersion),
|
||||
"hello-time": int(bs.BridgeHelloTime),
|
||||
"forward-delay": int(bs.BridgeForwardDelay),
|
||||
"max-age": int(bs.BridgeMaxAge),
|
||||
"transmit-hold-count": int(bs.TxHoldCount),
|
||||
"max-hops": int(bs.MaxHops),
|
||||
"cist": cist,
|
||||
}
|
||||
|
||||
return stp
|
||||
}
|
||||
|
||||
func buildPortSTP(ps *CISTPortStatus) map[string]any {
|
||||
cist := map[string]any{
|
||||
"port-id": portIDMap(ps.PortID),
|
||||
"role": roleName(ps.Role),
|
||||
"disputed": ps.Disputed,
|
||||
"external-path-cost": int(ps.ExternalPortPathCost),
|
||||
"designated": map[string]any{
|
||||
"bridge-id": bridgeIDMap(ps.DesignatedBridge),
|
||||
"port-id": portIDMap(ps.DesignatedPort),
|
||||
},
|
||||
}
|
||||
|
||||
stp := map[string]any{
|
||||
"edge": ps.OperEdgePort,
|
||||
"cist": cist,
|
||||
"statistics": map[string]any{
|
||||
"in-bpdus": strconv.FormatUint(uint64(ps.NumRxBPDU), 10),
|
||||
"in-bpdus-filtered": strconv.FormatUint(uint64(ps.NumRxBPDUFiltered), 10),
|
||||
"in-tcns": strconv.FormatUint(uint64(ps.NumRxTCN), 10),
|
||||
"out-bpdus": strconv.FormatUint(uint64(ps.NumTxBPDU), 10),
|
||||
"out-tcns": strconv.FormatUint(uint64(ps.NumTxTCN), 10),
|
||||
"to-blocking": strconv.FormatUint(uint64(ps.NumTransBlk), 10),
|
||||
"to-forwarding": strconv.FormatUint(uint64(ps.NumTransFwd), 10),
|
||||
},
|
||||
}
|
||||
|
||||
return stp
|
||||
}
|
||||
|
||||
type bridgeInfo struct {
|
||||
name string
|
||||
ports []string
|
||||
}
|
||||
|
||||
func findBridges(links json.RawMessage) []bridgeInfo {
|
||||
var ifaces []map[string]any
|
||||
if json.Unmarshal(links, &ifaces) != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
bridges := make(map[string]*bridgeInfo)
|
||||
for _, iface := range ifaces {
|
||||
linkinfo, _ := iface["linkinfo"].(map[string]any)
|
||||
if linkinfo == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
name, _ := iface["ifname"].(string)
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if kind, _ := linkinfo["info_kind"].(string); kind == "bridge" {
|
||||
if bridges[name] == nil {
|
||||
bridges[name] = &bridgeInfo{name: name}
|
||||
}
|
||||
}
|
||||
|
||||
if master, _ := iface["master"].(string); master != "" {
|
||||
br := bridges[master]
|
||||
if br == nil {
|
||||
br = &bridgeInfo{name: master}
|
||||
bridges[master] = br
|
||||
}
|
||||
br.ports = append(br.ports, name)
|
||||
}
|
||||
}
|
||||
|
||||
var result []bridgeInfo
|
||||
for _, br := range bridges {
|
||||
if len(br.ports) > 0 {
|
||||
result = append(result, *br)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// LinksIfIndexResolver resolves interface names to indices from ip-json link data.
|
||||
type LinksIfIndexResolver struct {
|
||||
idx map[string]int
|
||||
}
|
||||
|
||||
// NewLinksIfIndexResolver builds a resolver from ip-json link data.
|
||||
func NewLinksIfIndexResolver(links json.RawMessage) *LinksIfIndexResolver {
|
||||
r := &LinksIfIndexResolver{idx: make(map[string]int)}
|
||||
var ifaces []map[string]any
|
||||
if json.Unmarshal(links, &ifaces) != nil {
|
||||
return r
|
||||
}
|
||||
for _, iface := range ifaces {
|
||||
name, _ := iface["ifname"].(string)
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
switch v := iface["ifindex"].(type) {
|
||||
case float64:
|
||||
r.idx[name] = int(v)
|
||||
case int:
|
||||
r.idx[name] = v
|
||||
}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// IfIndex returns the kernel interface index for the given name.
|
||||
func (r *LinksIfIndexResolver) IfIndex(name string) (int, bool) {
|
||||
idx, ok := r.idx[name]
|
||||
return idx, ok
|
||||
}
|
||||
|
||||
// FindBridges is exported for testing. It extracts bridge info from
|
||||
// ip-json link data.
|
||||
func FindBridges(links json.RawMessage) []struct {
|
||||
Name string
|
||||
Ports []string
|
||||
} {
|
||||
brs := findBridges(links)
|
||||
out := make([]struct {
|
||||
Name string
|
||||
Ports []string
|
||||
}, len(brs))
|
||||
for i, br := range brs {
|
||||
out[i].Name = br.name
|
||||
out[i].Ports = br.ports
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -169,10 +169,10 @@ with infamy.Test() as test:
|
||||
with test.step("Verify that A, B, C and D agrees on A being the root bridge"):
|
||||
a_id = bridge_id(a, "bridge-id")
|
||||
print(f"A's bridge-id: {a_id}")
|
||||
until(lambda: all(map(lambda n: bridge_id(n, "root-id") == a_id, (a, b, c, d))), 60)
|
||||
until(lambda: all(map(lambda n: bridge_id(n, "root-id") == a_id, (a, b, c, d))), 120)
|
||||
|
||||
with test.step("Verify that B, C and D all use their direct connection to on A"):
|
||||
until(lambda: all(map(lambda n: port_role(n, n["a"]) == "root", (b, c, d))), 60)
|
||||
until(lambda: all(map(lambda n: port_role(n, n["a"]) == "root", (b, c, d))), 120)
|
||||
|
||||
with test.step("Verify that host:a can reach host:{b,c,d}"):
|
||||
parallel(lambda: ns["a"].must_reach("10.0.0.2"),
|
||||
|
||||
Reference in New Issue
Block a user