manager.go 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789
  1. package dynamicrouting
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "log"
  6. "os"
  7. "os/exec"
  8. "path/filepath"
  9. "strings"
  10. "sync"
  11. "time"
  12. )
  13. const (
  14. defaultConfigPath = "/etc/proxmox-router/routing/dynamic.json"
  15. frrConfigPath = "/etc/frr/frr.conf"
  16. )
  17. type Manager struct {
  18. mu sync.RWMutex
  19. configPath string
  20. config DynamicConfig
  21. }
  22. func NewManager(configPath string) *Manager {
  23. if configPath == "" {
  24. configPath = defaultConfigPath
  25. }
  26. m := &Manager{
  27. configPath: configPath,
  28. }
  29. if err := m.load(); err != nil {
  30. log.Printf("[DynamicRouting] Config load warning: %v, initializing defaults", err)
  31. m.initDefaults()
  32. _ = m.save()
  33. }
  34. // Apply initial configuration to FRR
  35. if err := m.applyFRRConfig(); err != nil {
  36. log.Printf("[DynamicRouting] Initial FRR apply warning: %v", err)
  37. }
  38. return m
  39. }
  40. func (m *Manager) initDefaults() {
  41. m.config = DynamicConfig{
  42. BGPInstance: BGPInstance{
  43. Enabled: false,
  44. AS: 65001,
  45. RouterID: "192.168.131.56",
  46. HoldTime: 180,
  47. KeepaliveTime: 60,
  48. RedistributeConnected: false,
  49. RedistributeStatic: false,
  50. RedistributeOSPF: false,
  51. },
  52. BGPPeers: []BGPPeer{},
  53. BGPNetworks: []BGPNetwork{},
  54. OSPFInstance: OSPFInstance{
  55. Enabled: false,
  56. RouterID: "192.168.131.56",
  57. RedistributeConnected: false,
  58. RedistributeStatic: false,
  59. RedistributeBGP: false,
  60. DefaultMetric: 10,
  61. },
  62. OSPFAreas: []OSPFArea{
  63. {
  64. ID: "area-backbone",
  65. Name: "backbone",
  66. AreaID: "0.0.0.0",
  67. AreaType: "default",
  68. Comment: "Default OSPF Backbone Area",
  69. },
  70. },
  71. OSPFInterfaces: []OSPFInterface{},
  72. }
  73. }
  74. func (m *Manager) load() error {
  75. m.mu.Lock()
  76. defer m.mu.Unlock()
  77. data, err := os.ReadFile(m.configPath)
  78. if err != nil {
  79. return err
  80. }
  81. return json.Unmarshal(data, &m.config)
  82. }
  83. func (m *Manager) save() error {
  84. dir := filepath.Dir(m.configPath)
  85. if err := os.MkdirAll(dir, 0755); err != nil {
  86. return err
  87. }
  88. data, err := json.MarshalIndent(m.config, "", " ")
  89. if err != nil {
  90. return err
  91. }
  92. return os.WriteFile(m.configPath, data, 0644)
  93. }
  94. // applyFRRConfig generates /etc/frr/frr.conf and reloads FRR daemons
  95. func (m *Manager) applyFRRConfig() error {
  96. var sb strings.Builder
  97. sb.WriteString("frr version 10.3\n")
  98. sb.WriteString("frr defaults traditional\n")
  99. sb.WriteString("hostname WAN-Balancer\n")
  100. sb.WriteString("log syslog informational\n")
  101. sb.WriteString("service integrated-vtysh-config\n!\n")
  102. // 1. Interfaces section for OSPF (cost, timers, authentication)
  103. interfaceConfigs := make(map[string]bool)
  104. if m.config.OSPFInstance.Enabled {
  105. for _, iface := range m.config.OSPFInterfaces {
  106. if iface.Disabled || iface.Interface == "" || interfaceConfigs[iface.Interface] {
  107. continue
  108. }
  109. interfaceConfigs[iface.Interface] = true
  110. sb.WriteString(fmt.Sprintf("interface %s\n", iface.Interface))
  111. if iface.Cost > 0 {
  112. sb.WriteString(fmt.Sprintf(" ip ospf cost %d\n", iface.Cost))
  113. }
  114. if iface.Priority > 0 {
  115. sb.WriteString(fmt.Sprintf(" ip ospf priority %d\n", iface.Priority))
  116. }
  117. if iface.HelloInterval > 0 {
  118. sb.WriteString(fmt.Sprintf(" ip ospf hello-interval %d\n", iface.HelloInterval))
  119. }
  120. if iface.DeadInterval > 0 {
  121. sb.WriteString(fmt.Sprintf(" ip ospf dead-interval %d\n", iface.DeadInterval))
  122. }
  123. if iface.AuthType == "md5" && iface.AuthKey != "" {
  124. sb.WriteString(" ip ospf authentication message-digest\n")
  125. sb.WriteString(fmt.Sprintf(" ip ospf message-digest-key 1 md5 %s\n", iface.AuthKey))
  126. } else if iface.AuthType == "simple" && iface.AuthKey != "" {
  127. sb.WriteString(" ip ospf authentication\n")
  128. sb.WriteString(fmt.Sprintf(" ip ospf authentication-key %s\n", iface.AuthKey))
  129. }
  130. sb.WriteString("!\n")
  131. }
  132. }
  133. // 2. BGP Router Section
  134. if m.config.BGPInstance.Enabled && m.config.BGPInstance.AS > 0 {
  135. inst := m.config.BGPInstance
  136. sb.WriteString(fmt.Sprintf("router bgp %d\n", inst.AS))
  137. if inst.RouterID != "" {
  138. sb.WriteString(fmt.Sprintf(" bgp router-id %s\n", inst.RouterID))
  139. }
  140. keepalive := 60
  141. hold := 180
  142. if inst.KeepaliveTime > 0 {
  143. keepalive = inst.KeepaliveTime
  144. }
  145. if inst.HoldTime > 0 {
  146. hold = inst.HoldTime
  147. }
  148. sb.WriteString(fmt.Sprintf(" timers bgp %d %d\n", keepalive, hold))
  149. // Peers definition
  150. for _, peer := range m.config.BGPPeers {
  151. if peer.Disabled || peer.RemoteAddress == "" || peer.RemoteAS == 0 {
  152. continue
  153. }
  154. sb.WriteString(fmt.Sprintf(" neighbor %s remote-as %d\n", peer.RemoteAddress, peer.RemoteAS))
  155. if peer.Name != "" {
  156. sb.WriteString(fmt.Sprintf(" neighbor %s description \"%s\"\n", peer.RemoteAddress, peer.Name))
  157. }
  158. if peer.Multihop {
  159. sb.WriteString(fmt.Sprintf(" neighbor %s ebgp-multihop 255\n", peer.RemoteAddress))
  160. }
  161. if peer.Passive {
  162. sb.WriteString(fmt.Sprintf(" neighbor %s passive\n", peer.RemoteAddress))
  163. }
  164. if peer.Password != "" {
  165. sb.WriteString(fmt.Sprintf(" neighbor %s password %s\n", peer.RemoteAddress, peer.Password))
  166. }
  167. if peer.LocalAddress != "" {
  168. sb.WriteString(fmt.Sprintf(" neighbor %s update-source %s\n", peer.RemoteAddress, peer.LocalAddress))
  169. }
  170. }
  171. // Address Family IPv4 Unicast
  172. sb.WriteString(" address-family ipv4 unicast\n")
  173. // Advertised Networks
  174. for _, net := range m.config.BGPNetworks {
  175. if !net.Disabled && net.Network != "" {
  176. sb.WriteString(fmt.Sprintf(" network %s\n", net.Network))
  177. }
  178. }
  179. // Activate peers in IPv4 unicast
  180. for _, peer := range m.config.BGPPeers {
  181. if peer.Disabled || peer.RemoteAddress == "" || peer.RemoteAS == 0 {
  182. continue
  183. }
  184. sb.WriteString(fmt.Sprintf(" neighbor %s activate\n", peer.RemoteAddress))
  185. if peer.NexthopSelf {
  186. sb.WriteString(fmt.Sprintf(" neighbor %s next-hop-self\n", peer.RemoteAddress))
  187. }
  188. }
  189. // Redistribute
  190. if inst.RedistributeConnected {
  191. sb.WriteString(" redistribute connected\n")
  192. }
  193. if inst.RedistributeStatic {
  194. sb.WriteString(" redistribute static\n")
  195. }
  196. if inst.RedistributeOSPF {
  197. sb.WriteString(" redistribute ospf\n")
  198. }
  199. sb.WriteString(" exit-address-family\n!\n")
  200. }
  201. // 3. OSPF Router Section
  202. if m.config.OSPFInstance.Enabled {
  203. inst := m.config.OSPFInstance
  204. sb.WriteString("router ospf\n")
  205. if inst.RouterID != "" {
  206. sb.WriteString(fmt.Sprintf(" ospf router-id %s\n", inst.RouterID))
  207. }
  208. if inst.DefaultMetric > 0 {
  209. sb.WriteString(fmt.Sprintf(" default-metric %d\n", inst.DefaultMetric))
  210. }
  211. if inst.RedistributeConnected {
  212. sb.WriteString(" redistribute connected\n")
  213. }
  214. if inst.RedistributeStatic {
  215. sb.WriteString(" redistribute static\n")
  216. }
  217. if inst.RedistributeBGP {
  218. sb.WriteString(" redistribute bgp\n")
  219. }
  220. for _, iface := range m.config.OSPFInterfaces {
  221. if iface.Disabled || iface.Network == "" {
  222. continue
  223. }
  224. area := iface.Area
  225. if area == "" {
  226. area = "0.0.0.0"
  227. }
  228. sb.WriteString(fmt.Sprintf(" network %s area %s\n", iface.Network, area))
  229. if iface.Passive && iface.Interface != "" {
  230. sb.WriteString(fmt.Sprintf(" passive-interface %s\n", iface.Interface))
  231. }
  232. }
  233. sb.WriteString("!\n")
  234. }
  235. sb.WriteString("end\n")
  236. // Write frr.conf
  237. if err := os.WriteFile(frrConfigPath, []byte(sb.String()), 0640); err != nil {
  238. return fmt.Errorf("failed to write %s: %w", frrConfigPath, err)
  239. }
  240. // Change ownership to frr:frr
  241. _ = exec.Command("chown", "frr:frr", frrConfigPath).Run()
  242. // Reload FRR using vtysh or systemctl
  243. out, err := exec.Command("vtysh", "-f", frrConfigPath).CombinedOutput()
  244. if err != nil {
  245. log.Printf("[DynamicRouting] vtysh -f reload warning: %v, output: %s", err, string(out))
  246. // fallback to systemctl reload frr
  247. _ = exec.Command("systemctl", "reload", "frr").Run()
  248. }
  249. return nil
  250. }
  251. func vtyshExec(cmd string) ([]byte, error) {
  252. return exec.Command("vtysh", "-c", cmd).Output()
  253. }
  254. // -------------------------------------------------------------
  255. // BGP API & Operations
  256. // -------------------------------------------------------------
  257. func (m *Manager) GetBGPInstance() BGPInstance {
  258. m.mu.RLock()
  259. defer m.mu.RUnlock()
  260. inst := m.config.BGPInstance
  261. // Check if BGP is currently running in FRR
  262. out, err := vtyshExec("show ip bgp summary json")
  263. if err == nil && len(out) > 5 && !strings.Contains(string(out), "warning") {
  264. inst.Running = true
  265. }
  266. return inst
  267. }
  268. func (m *Manager) UpdateBGPInstance(inst BGPInstance) error {
  269. m.mu.Lock()
  270. m.config.BGPInstance = inst
  271. _ = m.save()
  272. m.mu.Unlock()
  273. return m.applyFRRConfig()
  274. }
  275. func (m *Manager) ListBGPPeers() []BGPPeer {
  276. m.mu.RLock()
  277. defer m.mu.RUnlock()
  278. // Make a copy of configured peers
  279. peers := make([]BGPPeer, len(m.config.BGPPeers))
  280. copy(peers, m.config.BGPPeers)
  281. // Fetch live peer telemetry from vtysh
  282. out, err := vtyshExec("show ip bgp summary json")
  283. if err == nil {
  284. var summary struct {
  285. IPv4Unicast struct {
  286. Peers map[string]struct {
  287. RemoteAs uint32 `json:"remoteAs"`
  288. State string `json:"state"`
  289. PeerUptime string `json:"peerUptime"`
  290. PfxRcd int `json:"pfxRcd"`
  291. PfxSnt int `json:"pfxSnt"`
  292. } `json:"peers"`
  293. } `json:"ipv4Unicast"`
  294. }
  295. if err := json.Unmarshal(out, &summary); err == nil && summary.IPv4Unicast.Peers != nil {
  296. for i := range peers {
  297. if live, ok := summary.IPv4Unicast.Peers[peers[i].RemoteAddress]; ok {
  298. peers[i].State = live.State
  299. peers[i].Uptime = live.PeerUptime
  300. peers[i].PrefixReceived = live.PfxRcd
  301. peers[i].PrefixSent = live.PfxSnt
  302. } else if peers[i].Disabled {
  303. peers[i].State = "Disabled"
  304. } else {
  305. peers[i].State = "Idle"
  306. }
  307. }
  308. }
  309. }
  310. return peers
  311. }
  312. func (m *Manager) SaveBGPPeer(peer BGPPeer) error {
  313. m.mu.Lock()
  314. defer m.mu.Unlock()
  315. if peer.ID == "" {
  316. peer.ID = fmt.Sprintf("bgp-peer-%d", time.Now().UnixNano())
  317. }
  318. found := false
  319. for i, p := range m.config.BGPPeers {
  320. if p.ID == peer.ID {
  321. m.config.BGPPeers[i] = peer
  322. found = true
  323. break
  324. }
  325. }
  326. if !found {
  327. m.config.BGPPeers = append(m.config.BGPPeers, peer)
  328. }
  329. if err := m.save(); err != nil {
  330. return err
  331. }
  332. return m.applyFRRConfig()
  333. }
  334. func (m *Manager) DeleteBGPPeer(id string) error {
  335. m.mu.Lock()
  336. defer m.mu.Unlock()
  337. newPeers := make([]BGPPeer, 0, len(m.config.BGPPeers))
  338. for _, p := range m.config.BGPPeers {
  339. if p.ID != id {
  340. newPeers = append(newPeers, p)
  341. }
  342. }
  343. m.config.BGPPeers = newPeers
  344. if err := m.save(); err != nil {
  345. return err
  346. }
  347. return m.applyFRRConfig()
  348. }
  349. func (m *Manager) SetBGPPeerState(id string, disabled bool) error {
  350. m.mu.Lock()
  351. defer m.mu.Unlock()
  352. for i := range m.config.BGPPeers {
  353. if m.config.BGPPeers[i].ID == id {
  354. m.config.BGPPeers[i].Disabled = disabled
  355. break
  356. }
  357. }
  358. if err := m.save(); err != nil {
  359. return err
  360. }
  361. return m.applyFRRConfig()
  362. }
  363. func (m *Manager) ListBGPNetworks() []BGPNetwork {
  364. m.mu.RLock()
  365. defer m.mu.RUnlock()
  366. return m.config.BGPNetworks
  367. }
  368. func (m *Manager) SaveBGPNetwork(net BGPNetwork) error {
  369. m.mu.Lock()
  370. defer m.mu.Unlock()
  371. if net.ID == "" {
  372. net.ID = fmt.Sprintf("bgp-net-%d", time.Now().UnixNano())
  373. }
  374. found := false
  375. for i, n := range m.config.BGPNetworks {
  376. if n.ID == net.ID {
  377. m.config.BGPNetworks[i] = net
  378. found = true
  379. break
  380. }
  381. }
  382. if !found {
  383. m.config.BGPNetworks = append(m.config.BGPNetworks, net)
  384. }
  385. if err := m.save(); err != nil {
  386. return err
  387. }
  388. return m.applyFRRConfig()
  389. }
  390. func (m *Manager) DeleteBGPNetwork(id string) error {
  391. m.mu.Lock()
  392. defer m.mu.Unlock()
  393. newNets := make([]BGPNetwork, 0, len(m.config.BGPNetworks))
  394. for _, n := range m.config.BGPNetworks {
  395. if n.ID != id {
  396. newNets = append(newNets, n)
  397. }
  398. }
  399. m.config.BGPNetworks = newNets
  400. if err := m.save(); err != nil {
  401. return err
  402. }
  403. return m.applyFRRConfig()
  404. }
  405. func (m *Manager) GetBGPRoutes() []BGPRoute {
  406. var routes []BGPRoute
  407. out, err := vtyshExec("show ip bgp json")
  408. if err != nil {
  409. return routes
  410. }
  411. var parsed struct {
  412. Routes map[string][]struct {
  413. Metric int `json:"metric"`
  414. Weight int `json:"weight"`
  415. Path string `json:"path"`
  416. Origin string `json:"origin"`
  417. Nexthops []struct {
  418. IP string `json:"ip"`
  419. Used bool `json:"used"`
  420. } `json:"nexthops"`
  421. } `json:"routes"`
  422. }
  423. if err := json.Unmarshal(out, &parsed); err == nil && parsed.Routes != nil {
  424. for prefix, items := range parsed.Routes {
  425. for _, item := range items {
  426. nh := ""
  427. if len(item.Nexthops) > 0 {
  428. nh = item.Nexthops[0].IP
  429. }
  430. routes = append(routes, BGPRoute{
  431. Prefix: prefix,
  432. NextHop: nh,
  433. Metric: item.Metric,
  434. Weight: item.Weight,
  435. Path: item.Path,
  436. Origin: item.Origin,
  437. Best: true,
  438. })
  439. }
  440. }
  441. }
  442. return routes
  443. }
  444. // -------------------------------------------------------------
  445. // OSPF API & Operations
  446. // -------------------------------------------------------------
  447. func (m *Manager) GetOSPFInstance() OSPFInstance {
  448. m.mu.RLock()
  449. defer m.mu.RUnlock()
  450. inst := m.config.OSPFInstance
  451. out, err := vtyshExec("show ip ospf json")
  452. if err == nil && len(out) > 5 && !strings.Contains(string(out), "warning") {
  453. inst.Running = true
  454. }
  455. return inst
  456. }
  457. func (m *Manager) UpdateOSPFInstance(inst OSPFInstance) error {
  458. m.mu.Lock()
  459. m.config.OSPFInstance = inst
  460. _ = m.save()
  461. m.mu.Unlock()
  462. return m.applyFRRConfig()
  463. }
  464. func (m *Manager) ListOSPFAreas() []OSPFArea {
  465. m.mu.RLock()
  466. defer m.mu.RUnlock()
  467. return m.config.OSPFAreas
  468. }
  469. func (m *Manager) SaveOSPFArea(area OSPFArea) error {
  470. m.mu.Lock()
  471. defer m.mu.Unlock()
  472. if area.ID == "" {
  473. area.ID = fmt.Sprintf("area-%d", time.Now().UnixNano())
  474. }
  475. found := false
  476. for i, a := range m.config.OSPFAreas {
  477. if a.ID == area.ID {
  478. m.config.OSPFAreas[i] = area
  479. found = true
  480. break
  481. }
  482. }
  483. if !found {
  484. m.config.OSPFAreas = append(m.config.OSPFAreas, area)
  485. }
  486. if err := m.save(); err != nil {
  487. return err
  488. }
  489. return m.applyFRRConfig()
  490. }
  491. func (m *Manager) DeleteOSPFArea(id string) error {
  492. m.mu.Lock()
  493. defer m.mu.Unlock()
  494. newAreas := make([]OSPFArea, 0, len(m.config.OSPFAreas))
  495. for _, a := range m.config.OSPFAreas {
  496. if a.ID != id {
  497. newAreas = append(newAreas, a)
  498. }
  499. }
  500. m.config.OSPFAreas = newAreas
  501. if err := m.save(); err != nil {
  502. return err
  503. }
  504. return m.applyFRRConfig()
  505. }
  506. func (m *Manager) ListOSPFInterfaces() []OSPFInterface {
  507. m.mu.RLock()
  508. defer m.mu.RUnlock()
  509. return m.config.OSPFInterfaces
  510. }
  511. func (m *Manager) SaveOSPFInterface(iface OSPFInterface) error {
  512. m.mu.Lock()
  513. defer m.mu.Unlock()
  514. if iface.ID == "" {
  515. iface.ID = fmt.Sprintf("ospf-iface-%d", time.Now().UnixNano())
  516. }
  517. if iface.Cost <= 0 {
  518. iface.Cost = 10
  519. }
  520. if iface.Priority <= 0 {
  521. iface.Priority = 1
  522. }
  523. if iface.HelloInterval <= 0 {
  524. iface.HelloInterval = 10
  525. }
  526. if iface.DeadInterval <= 0 {
  527. iface.DeadInterval = 40
  528. }
  529. found := false
  530. for i, item := range m.config.OSPFInterfaces {
  531. if item.ID == iface.ID {
  532. m.config.OSPFInterfaces[i] = iface
  533. found = true
  534. break
  535. }
  536. }
  537. if !found {
  538. m.config.OSPFInterfaces = append(m.config.OSPFInterfaces, iface)
  539. }
  540. if err := m.save(); err != nil {
  541. return err
  542. }
  543. return m.applyFRRConfig()
  544. }
  545. func (m *Manager) DeleteOSPFInterface(id string) error {
  546. m.mu.Lock()
  547. defer m.mu.Unlock()
  548. newInterfaces := make([]OSPFInterface, 0, len(m.config.OSPFInterfaces))
  549. for _, item := range m.config.OSPFInterfaces {
  550. if item.ID != id {
  551. newInterfaces = append(newInterfaces, item)
  552. }
  553. }
  554. m.config.OSPFInterfaces = newInterfaces
  555. if err := m.save(); err != nil {
  556. return err
  557. }
  558. return m.applyFRRConfig()
  559. }
  560. func (m *Manager) SetOSPFInterfaceState(id string, disabled bool) error {
  561. m.mu.Lock()
  562. defer m.mu.Unlock()
  563. for i := range m.config.OSPFInterfaces {
  564. if m.config.OSPFInterfaces[i].ID == id {
  565. m.config.OSPFInterfaces[i].Disabled = disabled
  566. break
  567. }
  568. }
  569. if err := m.save(); err != nil {
  570. return err
  571. }
  572. return m.applyFRRConfig()
  573. }
  574. func (m *Manager) GetOSPFNeighbors() []OSPFNeighbor {
  575. var neighbors []OSPFNeighbor
  576. out, err := vtyshExec("show ip ospf neighbor json")
  577. if err != nil {
  578. return neighbors
  579. }
  580. var parsed struct {
  581. Neighbors map[string][]struct {
  582. NbrState string `json:"nbrState"`
  583. Address string `json:"address"`
  584. IfaceName string `json:"ifaceName"`
  585. Area string `json:"area"`
  586. DeadTime string `json:"deadTime"`
  587. } `json:"neighbors"`
  588. }
  589. if err := json.Unmarshal(out, &parsed); err == nil && parsed.Neighbors != nil {
  590. for nbrID, entries := range parsed.Neighbors {
  591. for _, entry := range entries {
  592. neighbors = append(neighbors, OSPFNeighbor{
  593. NeighborID: nbrID,
  594. Address: entry.Address,
  595. Interface: entry.IfaceName,
  596. Area: entry.Area,
  597. State: entry.NbrState,
  598. DeadTimer: entry.DeadTime,
  599. })
  600. }
  601. }
  602. }
  603. return neighbors
  604. }
  605. func (m *Manager) GetOSPFRoutes() []OSPFRoute {
  606. var routes []OSPFRoute
  607. out, err := vtyshExec("show ip ospf route json")
  608. if err != nil {
  609. return routes
  610. }
  611. var parsed map[string]struct {
  612. RouteType string `json:"routeType"`
  613. Cost int `json:"cost"`
  614. Area string `json:"area"`
  615. Nexthops []struct {
  616. IP string `json:"ip"`
  617. DirectlyAttachedTo string `json:"directlyAttachedTo"`
  618. } `json:"nexthops"`
  619. }
  620. if err := json.Unmarshal(out, &parsed); err == nil {
  621. for prefix, r := range parsed {
  622. nh := ""
  623. dev := ""
  624. if len(r.Nexthops) > 0 {
  625. nh = strings.TrimSpace(r.Nexthops[0].IP)
  626. dev = r.Nexthops[0].DirectlyAttachedTo
  627. }
  628. routes = append(routes, OSPFRoute{
  629. Prefix: prefix,
  630. RouteType: r.RouteType,
  631. Cost: r.Cost,
  632. Area: r.Area,
  633. NextHop: nh,
  634. Interface: dev,
  635. })
  636. }
  637. }
  638. return routes
  639. }
  640. // -------------------------------------------------------------
  641. // Summary Telemetry
  642. // -------------------------------------------------------------
  643. func (m *Manager) GetSummary() DynamicRoutingSummary {
  644. m.mu.RLock()
  645. defer m.mu.RUnlock()
  646. summary := DynamicRoutingSummary{
  647. BGPEnabled: m.config.BGPInstance.Enabled,
  648. OSPFEnabled: m.config.OSPFInstance.Enabled,
  649. }
  650. // Check BGP status
  651. if summary.BGPEnabled {
  652. out, err := vtyshExec("show ip bgp summary json")
  653. if err == nil && len(out) > 5 && !strings.Contains(string(out), "warning") {
  654. summary.BGPRunning = true
  655. var bgpParsed struct {
  656. IPv4Unicast struct {
  657. PeerCount int `json:"peerCount"`
  658. Peers map[string]struct {
  659. State string `json:"state"`
  660. } `json:"peers"`
  661. } `json:"ipv4Unicast"`
  662. }
  663. if err := json.Unmarshal(out, &bgpParsed); err == nil {
  664. summary.BGPPeerCount = len(m.config.BGPPeers)
  665. for _, p := range bgpParsed.IPv4Unicast.Peers {
  666. if p.State == "Established" {
  667. summary.BGPEstablishedCount++
  668. }
  669. }
  670. }
  671. }
  672. }
  673. // Check OSPF status
  674. if summary.OSPFEnabled {
  675. out, err := vtyshExec("show ip ospf json")
  676. if err == nil && len(out) > 5 && !strings.Contains(string(out), "warning") {
  677. summary.OSPFRunning = true
  678. }
  679. nbrs := m.GetOSPFNeighbors()
  680. summary.OSPFNeighborCount = len(nbrs)
  681. for _, n := range nbrs {
  682. if strings.HasPrefix(n.State, "Full") {
  683. summary.OSPFAdjacentCount++
  684. }
  685. }
  686. }
  687. return summary
  688. }