| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789 |
- package dynamicrouting
- import (
- "encoding/json"
- "fmt"
- "log"
- "os"
- "os/exec"
- "path/filepath"
- "strings"
- "sync"
- "time"
- )
- const (
- defaultConfigPath = "/etc/proxmox-router/routing/dynamic.json"
- frrConfigPath = "/etc/frr/frr.conf"
- )
- type Manager struct {
- mu sync.RWMutex
- configPath string
- config DynamicConfig
- }
- func NewManager(configPath string) *Manager {
- if configPath == "" {
- configPath = defaultConfigPath
- }
- m := &Manager{
- configPath: configPath,
- }
- if err := m.load(); err != nil {
- log.Printf("[DynamicRouting] Config load warning: %v, initializing defaults", err)
- m.initDefaults()
- _ = m.save()
- }
- // Apply initial configuration to FRR
- if err := m.applyFRRConfig(); err != nil {
- log.Printf("[DynamicRouting] Initial FRR apply warning: %v", err)
- }
- return m
- }
- func (m *Manager) initDefaults() {
- m.config = DynamicConfig{
- BGPInstance: BGPInstance{
- Enabled: false,
- AS: 65001,
- RouterID: "192.168.131.56",
- HoldTime: 180,
- KeepaliveTime: 60,
- RedistributeConnected: false,
- RedistributeStatic: false,
- RedistributeOSPF: false,
- },
- BGPPeers: []BGPPeer{},
- BGPNetworks: []BGPNetwork{},
- OSPFInstance: OSPFInstance{
- Enabled: false,
- RouterID: "192.168.131.56",
- RedistributeConnected: false,
- RedistributeStatic: false,
- RedistributeBGP: false,
- DefaultMetric: 10,
- },
- OSPFAreas: []OSPFArea{
- {
- ID: "area-backbone",
- Name: "backbone",
- AreaID: "0.0.0.0",
- AreaType: "default",
- Comment: "Default OSPF Backbone Area",
- },
- },
- OSPFInterfaces: []OSPFInterface{},
- }
- }
- func (m *Manager) load() error {
- m.mu.Lock()
- defer m.mu.Unlock()
- data, err := os.ReadFile(m.configPath)
- if err != nil {
- return err
- }
- return json.Unmarshal(data, &m.config)
- }
- func (m *Manager) save() error {
- dir := filepath.Dir(m.configPath)
- if err := os.MkdirAll(dir, 0755); err != nil {
- return err
- }
- data, err := json.MarshalIndent(m.config, "", " ")
- if err != nil {
- return err
- }
- return os.WriteFile(m.configPath, data, 0644)
- }
- // applyFRRConfig generates /etc/frr/frr.conf and reloads FRR daemons
- func (m *Manager) applyFRRConfig() error {
- var sb strings.Builder
- sb.WriteString("frr version 10.3\n")
- sb.WriteString("frr defaults traditional\n")
- sb.WriteString("hostname WAN-Balancer\n")
- sb.WriteString("log syslog informational\n")
- sb.WriteString("service integrated-vtysh-config\n!\n")
- // 1. Interfaces section for OSPF (cost, timers, authentication)
- interfaceConfigs := make(map[string]bool)
- if m.config.OSPFInstance.Enabled {
- for _, iface := range m.config.OSPFInterfaces {
- if iface.Disabled || iface.Interface == "" || interfaceConfigs[iface.Interface] {
- continue
- }
- interfaceConfigs[iface.Interface] = true
- sb.WriteString(fmt.Sprintf("interface %s\n", iface.Interface))
- if iface.Cost > 0 {
- sb.WriteString(fmt.Sprintf(" ip ospf cost %d\n", iface.Cost))
- }
- if iface.Priority > 0 {
- sb.WriteString(fmt.Sprintf(" ip ospf priority %d\n", iface.Priority))
- }
- if iface.HelloInterval > 0 {
- sb.WriteString(fmt.Sprintf(" ip ospf hello-interval %d\n", iface.HelloInterval))
- }
- if iface.DeadInterval > 0 {
- sb.WriteString(fmt.Sprintf(" ip ospf dead-interval %d\n", iface.DeadInterval))
- }
- if iface.AuthType == "md5" && iface.AuthKey != "" {
- sb.WriteString(" ip ospf authentication message-digest\n")
- sb.WriteString(fmt.Sprintf(" ip ospf message-digest-key 1 md5 %s\n", iface.AuthKey))
- } else if iface.AuthType == "simple" && iface.AuthKey != "" {
- sb.WriteString(" ip ospf authentication\n")
- sb.WriteString(fmt.Sprintf(" ip ospf authentication-key %s\n", iface.AuthKey))
- }
- sb.WriteString("!\n")
- }
- }
- // 2. BGP Router Section
- if m.config.BGPInstance.Enabled && m.config.BGPInstance.AS > 0 {
- inst := m.config.BGPInstance
- sb.WriteString(fmt.Sprintf("router bgp %d\n", inst.AS))
- if inst.RouterID != "" {
- sb.WriteString(fmt.Sprintf(" bgp router-id %s\n", inst.RouterID))
- }
- keepalive := 60
- hold := 180
- if inst.KeepaliveTime > 0 {
- keepalive = inst.KeepaliveTime
- }
- if inst.HoldTime > 0 {
- hold = inst.HoldTime
- }
- sb.WriteString(fmt.Sprintf(" timers bgp %d %d\n", keepalive, hold))
- // Peers definition
- for _, peer := range m.config.BGPPeers {
- if peer.Disabled || peer.RemoteAddress == "" || peer.RemoteAS == 0 {
- continue
- }
- sb.WriteString(fmt.Sprintf(" neighbor %s remote-as %d\n", peer.RemoteAddress, peer.RemoteAS))
- if peer.Name != "" {
- sb.WriteString(fmt.Sprintf(" neighbor %s description \"%s\"\n", peer.RemoteAddress, peer.Name))
- }
- if peer.Multihop {
- sb.WriteString(fmt.Sprintf(" neighbor %s ebgp-multihop 255\n", peer.RemoteAddress))
- }
- if peer.Passive {
- sb.WriteString(fmt.Sprintf(" neighbor %s passive\n", peer.RemoteAddress))
- }
- if peer.Password != "" {
- sb.WriteString(fmt.Sprintf(" neighbor %s password %s\n", peer.RemoteAddress, peer.Password))
- }
- if peer.LocalAddress != "" {
- sb.WriteString(fmt.Sprintf(" neighbor %s update-source %s\n", peer.RemoteAddress, peer.LocalAddress))
- }
- }
- // Address Family IPv4 Unicast
- sb.WriteString(" address-family ipv4 unicast\n")
- // Advertised Networks
- for _, net := range m.config.BGPNetworks {
- if !net.Disabled && net.Network != "" {
- sb.WriteString(fmt.Sprintf(" network %s\n", net.Network))
- }
- }
- // Activate peers in IPv4 unicast
- for _, peer := range m.config.BGPPeers {
- if peer.Disabled || peer.RemoteAddress == "" || peer.RemoteAS == 0 {
- continue
- }
- sb.WriteString(fmt.Sprintf(" neighbor %s activate\n", peer.RemoteAddress))
- if peer.NexthopSelf {
- sb.WriteString(fmt.Sprintf(" neighbor %s next-hop-self\n", peer.RemoteAddress))
- }
- }
- // Redistribute
- if inst.RedistributeConnected {
- sb.WriteString(" redistribute connected\n")
- }
- if inst.RedistributeStatic {
- sb.WriteString(" redistribute static\n")
- }
- if inst.RedistributeOSPF {
- sb.WriteString(" redistribute ospf\n")
- }
- sb.WriteString(" exit-address-family\n!\n")
- }
- // 3. OSPF Router Section
- if m.config.OSPFInstance.Enabled {
- inst := m.config.OSPFInstance
- sb.WriteString("router ospf\n")
- if inst.RouterID != "" {
- sb.WriteString(fmt.Sprintf(" ospf router-id %s\n", inst.RouterID))
- }
- if inst.DefaultMetric > 0 {
- sb.WriteString(fmt.Sprintf(" default-metric %d\n", inst.DefaultMetric))
- }
- if inst.RedistributeConnected {
- sb.WriteString(" redistribute connected\n")
- }
- if inst.RedistributeStatic {
- sb.WriteString(" redistribute static\n")
- }
- if inst.RedistributeBGP {
- sb.WriteString(" redistribute bgp\n")
- }
- for _, iface := range m.config.OSPFInterfaces {
- if iface.Disabled || iface.Network == "" {
- continue
- }
- area := iface.Area
- if area == "" {
- area = "0.0.0.0"
- }
- sb.WriteString(fmt.Sprintf(" network %s area %s\n", iface.Network, area))
- if iface.Passive && iface.Interface != "" {
- sb.WriteString(fmt.Sprintf(" passive-interface %s\n", iface.Interface))
- }
- }
- sb.WriteString("!\n")
- }
- sb.WriteString("end\n")
- // Write frr.conf
- if err := os.WriteFile(frrConfigPath, []byte(sb.String()), 0640); err != nil {
- return fmt.Errorf("failed to write %s: %w", frrConfigPath, err)
- }
- // Change ownership to frr:frr
- _ = exec.Command("chown", "frr:frr", frrConfigPath).Run()
- // Reload FRR using vtysh or systemctl
- out, err := exec.Command("vtysh", "-f", frrConfigPath).CombinedOutput()
- if err != nil {
- log.Printf("[DynamicRouting] vtysh -f reload warning: %v, output: %s", err, string(out))
- // fallback to systemctl reload frr
- _ = exec.Command("systemctl", "reload", "frr").Run()
- }
- return nil
- }
- func vtyshExec(cmd string) ([]byte, error) {
- return exec.Command("vtysh", "-c", cmd).Output()
- }
- // -------------------------------------------------------------
- // BGP API & Operations
- // -------------------------------------------------------------
- func (m *Manager) GetBGPInstance() BGPInstance {
- m.mu.RLock()
- defer m.mu.RUnlock()
- inst := m.config.BGPInstance
- // Check if BGP is currently running in FRR
- out, err := vtyshExec("show ip bgp summary json")
- if err == nil && len(out) > 5 && !strings.Contains(string(out), "warning") {
- inst.Running = true
- }
- return inst
- }
- func (m *Manager) UpdateBGPInstance(inst BGPInstance) error {
- m.mu.Lock()
- m.config.BGPInstance = inst
- _ = m.save()
- m.mu.Unlock()
- return m.applyFRRConfig()
- }
- func (m *Manager) ListBGPPeers() []BGPPeer {
- m.mu.RLock()
- defer m.mu.RUnlock()
- // Make a copy of configured peers
- peers := make([]BGPPeer, len(m.config.BGPPeers))
- copy(peers, m.config.BGPPeers)
- // Fetch live peer telemetry from vtysh
- out, err := vtyshExec("show ip bgp summary json")
- if err == nil {
- var summary struct {
- IPv4Unicast struct {
- Peers map[string]struct {
- RemoteAs uint32 `json:"remoteAs"`
- State string `json:"state"`
- PeerUptime string `json:"peerUptime"`
- PfxRcd int `json:"pfxRcd"`
- PfxSnt int `json:"pfxSnt"`
- } `json:"peers"`
- } `json:"ipv4Unicast"`
- }
- if err := json.Unmarshal(out, &summary); err == nil && summary.IPv4Unicast.Peers != nil {
- for i := range peers {
- if live, ok := summary.IPv4Unicast.Peers[peers[i].RemoteAddress]; ok {
- peers[i].State = live.State
- peers[i].Uptime = live.PeerUptime
- peers[i].PrefixReceived = live.PfxRcd
- peers[i].PrefixSent = live.PfxSnt
- } else if peers[i].Disabled {
- peers[i].State = "Disabled"
- } else {
- peers[i].State = "Idle"
- }
- }
- }
- }
- return peers
- }
- func (m *Manager) SaveBGPPeer(peer BGPPeer) error {
- m.mu.Lock()
- defer m.mu.Unlock()
- if peer.ID == "" {
- peer.ID = fmt.Sprintf("bgp-peer-%d", time.Now().UnixNano())
- }
- found := false
- for i, p := range m.config.BGPPeers {
- if p.ID == peer.ID {
- m.config.BGPPeers[i] = peer
- found = true
- break
- }
- }
- if !found {
- m.config.BGPPeers = append(m.config.BGPPeers, peer)
- }
- if err := m.save(); err != nil {
- return err
- }
- return m.applyFRRConfig()
- }
- func (m *Manager) DeleteBGPPeer(id string) error {
- m.mu.Lock()
- defer m.mu.Unlock()
- newPeers := make([]BGPPeer, 0, len(m.config.BGPPeers))
- for _, p := range m.config.BGPPeers {
- if p.ID != id {
- newPeers = append(newPeers, p)
- }
- }
- m.config.BGPPeers = newPeers
- if err := m.save(); err != nil {
- return err
- }
- return m.applyFRRConfig()
- }
- func (m *Manager) SetBGPPeerState(id string, disabled bool) error {
- m.mu.Lock()
- defer m.mu.Unlock()
- for i := range m.config.BGPPeers {
- if m.config.BGPPeers[i].ID == id {
- m.config.BGPPeers[i].Disabled = disabled
- break
- }
- }
- if err := m.save(); err != nil {
- return err
- }
- return m.applyFRRConfig()
- }
- func (m *Manager) ListBGPNetworks() []BGPNetwork {
- m.mu.RLock()
- defer m.mu.RUnlock()
- return m.config.BGPNetworks
- }
- func (m *Manager) SaveBGPNetwork(net BGPNetwork) error {
- m.mu.Lock()
- defer m.mu.Unlock()
- if net.ID == "" {
- net.ID = fmt.Sprintf("bgp-net-%d", time.Now().UnixNano())
- }
- found := false
- for i, n := range m.config.BGPNetworks {
- if n.ID == net.ID {
- m.config.BGPNetworks[i] = net
- found = true
- break
- }
- }
- if !found {
- m.config.BGPNetworks = append(m.config.BGPNetworks, net)
- }
- if err := m.save(); err != nil {
- return err
- }
- return m.applyFRRConfig()
- }
- func (m *Manager) DeleteBGPNetwork(id string) error {
- m.mu.Lock()
- defer m.mu.Unlock()
- newNets := make([]BGPNetwork, 0, len(m.config.BGPNetworks))
- for _, n := range m.config.BGPNetworks {
- if n.ID != id {
- newNets = append(newNets, n)
- }
- }
- m.config.BGPNetworks = newNets
- if err := m.save(); err != nil {
- return err
- }
- return m.applyFRRConfig()
- }
- func (m *Manager) GetBGPRoutes() []BGPRoute {
- var routes []BGPRoute
- out, err := vtyshExec("show ip bgp json")
- if err != nil {
- return routes
- }
- var parsed struct {
- Routes map[string][]struct {
- Metric int `json:"metric"`
- Weight int `json:"weight"`
- Path string `json:"path"`
- Origin string `json:"origin"`
- Nexthops []struct {
- IP string `json:"ip"`
- Used bool `json:"used"`
- } `json:"nexthops"`
- } `json:"routes"`
- }
- if err := json.Unmarshal(out, &parsed); err == nil && parsed.Routes != nil {
- for prefix, items := range parsed.Routes {
- for _, item := range items {
- nh := ""
- if len(item.Nexthops) > 0 {
- nh = item.Nexthops[0].IP
- }
- routes = append(routes, BGPRoute{
- Prefix: prefix,
- NextHop: nh,
- Metric: item.Metric,
- Weight: item.Weight,
- Path: item.Path,
- Origin: item.Origin,
- Best: true,
- })
- }
- }
- }
- return routes
- }
- // -------------------------------------------------------------
- // OSPF API & Operations
- // -------------------------------------------------------------
- func (m *Manager) GetOSPFInstance() OSPFInstance {
- m.mu.RLock()
- defer m.mu.RUnlock()
- inst := m.config.OSPFInstance
- out, err := vtyshExec("show ip ospf json")
- if err == nil && len(out) > 5 && !strings.Contains(string(out), "warning") {
- inst.Running = true
- }
- return inst
- }
- func (m *Manager) UpdateOSPFInstance(inst OSPFInstance) error {
- m.mu.Lock()
- m.config.OSPFInstance = inst
- _ = m.save()
- m.mu.Unlock()
- return m.applyFRRConfig()
- }
- func (m *Manager) ListOSPFAreas() []OSPFArea {
- m.mu.RLock()
- defer m.mu.RUnlock()
- return m.config.OSPFAreas
- }
- func (m *Manager) SaveOSPFArea(area OSPFArea) error {
- m.mu.Lock()
- defer m.mu.Unlock()
- if area.ID == "" {
- area.ID = fmt.Sprintf("area-%d", time.Now().UnixNano())
- }
- found := false
- for i, a := range m.config.OSPFAreas {
- if a.ID == area.ID {
- m.config.OSPFAreas[i] = area
- found = true
- break
- }
- }
- if !found {
- m.config.OSPFAreas = append(m.config.OSPFAreas, area)
- }
- if err := m.save(); err != nil {
- return err
- }
- return m.applyFRRConfig()
- }
- func (m *Manager) DeleteOSPFArea(id string) error {
- m.mu.Lock()
- defer m.mu.Unlock()
- newAreas := make([]OSPFArea, 0, len(m.config.OSPFAreas))
- for _, a := range m.config.OSPFAreas {
- if a.ID != id {
- newAreas = append(newAreas, a)
- }
- }
- m.config.OSPFAreas = newAreas
- if err := m.save(); err != nil {
- return err
- }
- return m.applyFRRConfig()
- }
- func (m *Manager) ListOSPFInterfaces() []OSPFInterface {
- m.mu.RLock()
- defer m.mu.RUnlock()
- return m.config.OSPFInterfaces
- }
- func (m *Manager) SaveOSPFInterface(iface OSPFInterface) error {
- m.mu.Lock()
- defer m.mu.Unlock()
- if iface.ID == "" {
- iface.ID = fmt.Sprintf("ospf-iface-%d", time.Now().UnixNano())
- }
- if iface.Cost <= 0 {
- iface.Cost = 10
- }
- if iface.Priority <= 0 {
- iface.Priority = 1
- }
- if iface.HelloInterval <= 0 {
- iface.HelloInterval = 10
- }
- if iface.DeadInterval <= 0 {
- iface.DeadInterval = 40
- }
- found := false
- for i, item := range m.config.OSPFInterfaces {
- if item.ID == iface.ID {
- m.config.OSPFInterfaces[i] = iface
- found = true
- break
- }
- }
- if !found {
- m.config.OSPFInterfaces = append(m.config.OSPFInterfaces, iface)
- }
- if err := m.save(); err != nil {
- return err
- }
- return m.applyFRRConfig()
- }
- func (m *Manager) DeleteOSPFInterface(id string) error {
- m.mu.Lock()
- defer m.mu.Unlock()
- newInterfaces := make([]OSPFInterface, 0, len(m.config.OSPFInterfaces))
- for _, item := range m.config.OSPFInterfaces {
- if item.ID != id {
- newInterfaces = append(newInterfaces, item)
- }
- }
- m.config.OSPFInterfaces = newInterfaces
- if err := m.save(); err != nil {
- return err
- }
- return m.applyFRRConfig()
- }
- func (m *Manager) SetOSPFInterfaceState(id string, disabled bool) error {
- m.mu.Lock()
- defer m.mu.Unlock()
- for i := range m.config.OSPFInterfaces {
- if m.config.OSPFInterfaces[i].ID == id {
- m.config.OSPFInterfaces[i].Disabled = disabled
- break
- }
- }
- if err := m.save(); err != nil {
- return err
- }
- return m.applyFRRConfig()
- }
- func (m *Manager) GetOSPFNeighbors() []OSPFNeighbor {
- var neighbors []OSPFNeighbor
- out, err := vtyshExec("show ip ospf neighbor json")
- if err != nil {
- return neighbors
- }
- var parsed struct {
- Neighbors map[string][]struct {
- NbrState string `json:"nbrState"`
- Address string `json:"address"`
- IfaceName string `json:"ifaceName"`
- Area string `json:"area"`
- DeadTime string `json:"deadTime"`
- } `json:"neighbors"`
- }
- if err := json.Unmarshal(out, &parsed); err == nil && parsed.Neighbors != nil {
- for nbrID, entries := range parsed.Neighbors {
- for _, entry := range entries {
- neighbors = append(neighbors, OSPFNeighbor{
- NeighborID: nbrID,
- Address: entry.Address,
- Interface: entry.IfaceName,
- Area: entry.Area,
- State: entry.NbrState,
- DeadTimer: entry.DeadTime,
- })
- }
- }
- }
- return neighbors
- }
- func (m *Manager) GetOSPFRoutes() []OSPFRoute {
- var routes []OSPFRoute
- out, err := vtyshExec("show ip ospf route json")
- if err != nil {
- return routes
- }
- var parsed map[string]struct {
- RouteType string `json:"routeType"`
- Cost int `json:"cost"`
- Area string `json:"area"`
- Nexthops []struct {
- IP string `json:"ip"`
- DirectlyAttachedTo string `json:"directlyAttachedTo"`
- } `json:"nexthops"`
- }
- if err := json.Unmarshal(out, &parsed); err == nil {
- for prefix, r := range parsed {
- nh := ""
- dev := ""
- if len(r.Nexthops) > 0 {
- nh = strings.TrimSpace(r.Nexthops[0].IP)
- dev = r.Nexthops[0].DirectlyAttachedTo
- }
- routes = append(routes, OSPFRoute{
- Prefix: prefix,
- RouteType: r.RouteType,
- Cost: r.Cost,
- Area: r.Area,
- NextHop: nh,
- Interface: dev,
- })
- }
- }
- return routes
- }
- // -------------------------------------------------------------
- // Summary Telemetry
- // -------------------------------------------------------------
- func (m *Manager) GetSummary() DynamicRoutingSummary {
- m.mu.RLock()
- defer m.mu.RUnlock()
- summary := DynamicRoutingSummary{
- BGPEnabled: m.config.BGPInstance.Enabled,
- OSPFEnabled: m.config.OSPFInstance.Enabled,
- }
- // Check BGP status
- if summary.BGPEnabled {
- out, err := vtyshExec("show ip bgp summary json")
- if err == nil && len(out) > 5 && !strings.Contains(string(out), "warning") {
- summary.BGPRunning = true
- var bgpParsed struct {
- IPv4Unicast struct {
- PeerCount int `json:"peerCount"`
- Peers map[string]struct {
- State string `json:"state"`
- } `json:"peers"`
- } `json:"ipv4Unicast"`
- }
- if err := json.Unmarshal(out, &bgpParsed); err == nil {
- summary.BGPPeerCount = len(m.config.BGPPeers)
- for _, p := range bgpParsed.IPv4Unicast.Peers {
- if p.State == "Established" {
- summary.BGPEstablishedCount++
- }
- }
- }
- }
- }
- // Check OSPF status
- if summary.OSPFEnabled {
- out, err := vtyshExec("show ip ospf json")
- if err == nil && len(out) > 5 && !strings.Contains(string(out), "warning") {
- summary.OSPFRunning = true
- }
- nbrs := m.GetOSPFNeighbors()
- summary.OSPFNeighborCount = len(nbrs)
- for _, n := range nbrs {
- if strings.HasPrefix(n.State, "Full") {
- summary.OSPFAdjacentCount++
- }
- }
- }
- return summary
- }
|