master
go 512 lines 14.8 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package cato_networks
4
5 import (
6 "fmt"
7 "sort"
8 "time"
9
10 topologyv1 "github.com/netdata/netdata/go/plugins/pkg/topology/v1"
11 "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/cato_networks/catofunc"
12 )
13
14 const (
15 topologySource = "cato_networks"
16 topologyLayer = "network"
17 )
18
19 func buildTopology(accountID string, sites map[string]*siteState, order []string, collectedAt time.Time) (*topologyv1.Data, error) {
20 stringsDict := topologyv1.NewStringDictionary()
21 actors := topologyv1.NewTableBuilder(catoTopologyActorColumns()...)
22 links := topologyv1.NewTableBuilder(catoTopologyLinkColumns()...)
23 interfaces := topologyv1.NewTableBuilder(catoTopologyInterfaceColumns()...)
24
25 actorIndexes := make(map[string]int, len(order)*2)
26 popSeen := make(map[string]bool)
27
28 for _, siteID := range order {
29 site := sites[siteID]
30 if site == nil {
31 continue
32 }
33
34 siteActorID := catoSiteActorID(site.ID)
35 siteActorIndex := addSiteActor(actors, stringsDict, accountID, site, siteActorID)
36 actorIndexes[siteActorID] = siteActorIndex
37 deviceActors := addDeviceTopology(actors, links, stringsDict, actorIndexes, popSeen, accountID, site)
38 addInterfaceTopologyTable(interfaces, siteActorIndex, deviceActors, site)
39
40 if len(deviceActors) == 0 && site.PopName != "" {
41 popActorID := catoPopActorID(site.PopName)
42 if !popSeen[site.PopName] {
43 popSeen[site.PopName] = true
44 actorIndexes[popActorID] = addPopActor(actors, stringsDict, accountID, site.PopName, popActorID)
45 }
46 addSitePopLink(links, stringsDict, site, siteActorIndex, actorIndexes[popActorID])
47 }
48
49 addBGPPeerTopology(actors, links, stringsDict, actorIndexes, site, siteActorIndex)
50 }
51
52 actorTable, err := actors.Table()
53 if err != nil {
54 return nil, fmt.Errorf("actors table: %w", err)
55 }
56 linkTable, err := links.Table()
57 if err != nil {
58 return nil, fmt.Errorf("links table: %w", err)
59 }
60 interfaceTable, err := interfaces.Table()
61 if err != nil {
62 return nil, fmt.Errorf("interfaces table: %w", err)
63 }
64
65 data := &topologyv1.Data{
66 SchemaVersion: topologyv1.SchemaVersion,
67 Producer: topologyv1.Producer{
68 Source: topologySource,
69 Instance: accountID,
70 Plugin: "go.d/cato_networks",
71 Capabilities: []string{"sites", "devices", "interfaces", "bgp"},
72 },
73 CollectedAt: collectedAt,
74 View: &topologyv1.View{
75 ID: "summary",
76 Scope: "network",
77 Mode: "detailed",
78 },
79 Dictionaries: topologyv1.Dictionaries{
80 "strings": stringsDict.Values(),
81 },
82 Types: catoTopologyTypes(),
83 Presentation: catofunc.TopologyPresentation(),
84 Actors: actorTable,
85 Links: linkTable,
86 Tables: &topologyv1.DetailTables{
87 Actor: map[string]topologyv1.DetailTable{
88 catofunc.ActorTableInterfaces: {
89 Type: catofunc.ActorTableInterfaces,
90 Table: interfaceTable,
91 },
92 },
93 },
94 Stats: map[string]any{
95 "sites": len(order),
96 "links": links.Rows(),
97 },
98 }
99 return data, nil
100 }
101
102 func addSiteActor(table *topologyv1.TableBuilder, dict *topologyv1.StringDictionary, accountID string, site *siteState, siteActorID string) int {
103 return table.Add(
104 dict.Ref(catofunc.ActorTypeSite),
105 dict.Ref(topologyLayer),
106 siteActorID,
107 site.Name,
108 accountID,
109 site.ID,
110 "",
111 site.PopName,
112 "",
113 "",
114 site.ConnectivityStatus,
115 site.OperationalStatus,
116 site.SiteType,
117 site.ConnectionType,
118 site.CountryCode,
119 site.CountryName,
120 site.Region,
121 site.HostCount,
122 nil,
123 "",
124 "",
125 "",
126 nil,
127 )
128 }
129
130 func addPopActor(table *topologyv1.TableBuilder, dict *topologyv1.StringDictionary, accountID, popName, popActorID string) int {
131 return table.Add(
132 dict.Ref(catofunc.ActorTypePop),
133 dict.Ref(topologyLayer),
134 popActorID,
135 popName,
136 accountID,
137 "",
138 "",
139 popName,
140 "",
141 "",
142 "",
143 "",
144 "",
145 "",
146 "",
147 "",
148 "",
149 int64(0),
150 nil,
151 "",
152 "",
153 "",
154 nil,
155 )
156 }
157
158 func addSitePopLink(table *topologyv1.TableBuilder, dict *topologyv1.StringDictionary, site *siteState, siteActor, popActor int) {
159 table.Add(
160 dict.Ref(catofunc.LinkTypeTunnel),
161 siteActor,
162 popActor,
163 dict.Ref("cato"),
164 dict.Ref(site.ConnectivityStatus),
165 dict.Ref("bidirectional"),
166 1,
167 trafficMetricValue(site.Metrics, trafficMetricBytesUpstreamMax, site.Metrics.BytesUpstreamMax),
168 trafficMetricValue(site.Metrics, trafficMetricBytesDownstreamMax, site.Metrics.BytesDownstreamMax),
169 trafficMetricValue(site.Metrics, trafficMetricLostUpstreamPercent, site.Metrics.LostUpstreamPercent),
170 trafficMetricValue(site.Metrics, trafficMetricRTTMS, site.Metrics.RTTMS),
171 nil,
172 nil,
173 nil,
174 nil,
175 nil,
176 nil,
177 )
178 }
179
180 func addDeviceTopology(actors, links *topologyv1.TableBuilder, dict *topologyv1.StringDictionary, actorIndexes map[string]int, popSeen map[string]bool, accountID string, site *siteState) map[string]int {
181 deviceActors := make(map[string]int, len(site.Devices))
182
183 sortedDevices := append([]deviceState(nil), site.Devices...)
184 sort.Slice(sortedDevices, func(i, j int) bool {
185 leftName := deviceDisplayName(sortedDevices[i])
186 rightName := deviceDisplayName(sortedDevices[j])
187 if leftName != rightName {
188 return leftName < rightName
189 }
190 return stableDeviceID(sortedDevices[i]) < stableDeviceID(sortedDevices[j])
191 })
192
193 for _, dev := range sortedDevices {
194 deviceID := stableDeviceID(dev)
195 if deviceID == "" {
196 continue
197 }
198 deviceActorID := catoDeviceActorID(site.ID, deviceID)
199 deviceActor := addDeviceActor(actors, dict, accountID, site, dev, deviceID, deviceActorID)
200 actorIndexes[deviceActorID] = deviceActor
201 deviceActors[deviceID] = deviceActor
202
203 popName := dev.LastPopName
204 if popName == "" {
205 popName = site.PopName
206 }
207 if popName == "" {
208 continue
209 }
210 popActorID := catoPopActorID(popName)
211 if !popSeen[popName] {
212 popSeen[popName] = true
213 actorIndexes[popActorID] = addPopActor(actors, dict, accountID, popName, popActorID)
214 }
215 addDevicePopLink(links, dict, dev, deviceActor, actorIndexes[popActorID])
216 }
217
218 return deviceActors
219 }
220
221 func addDeviceActor(table *topologyv1.TableBuilder, dict *topologyv1.StringDictionary, accountID string, site *siteState, dev deviceState, deviceID, deviceActorID string) int {
222 displayName := deviceDisplayName(dev)
223 if displayName == "" {
224 displayName = deviceID
225 }
226 popName := dev.LastPopName
227 if popName == "" {
228 popName = site.PopName
229 }
230 return table.Add(
231 dict.Ref(catofunc.ActorTypeDevice),
232 dict.Ref(topologyLayer),
233 deviceActorID,
234 displayName,
235 accountID,
236 site.ID,
237 deviceID,
238 popName,
239 "",
240 "",
241 boolState(dev.Connected, "connected", "disconnected"),
242 "",
243 "",
244 "",
245 "",
246 "",
247 "",
248 int64(0),
249 dev.Connected,
250 dev.HaRole,
251 dev.SocketSerial,
252 dev.SocketVersion,
253 dev.InternalIP,
254 )
255 }
256
257 func addDevicePopLink(table *topologyv1.TableBuilder, dict *topologyv1.StringDictionary, dev deviceState, deviceActor, popActor int) {
258 table.Add(
259 dict.Ref(catofunc.LinkTypeTunnel),
260 deviceActor,
261 popActor,
262 dict.Ref("cato"),
263 dict.Ref(boolState(dev.Connected, "connected", "disconnected")),
264 dict.Ref("bidirectional"),
265 1,
266 nil,
267 nil,
268 nil,
269 nil,
270 nil,
271 nil,
272 nil,
273 nil,
274 nil,
275 nil,
276 )
277 }
278
279 func addBGPPeerTopology(actors, links *topologyv1.TableBuilder, dict *topologyv1.StringDictionary, actorIndexes map[string]int, site *siteState, siteActor int) {
280 seen := make(map[string]bool)
281
282 for _, peer := range site.BGPPeers {
283 if peer.RemoteIP == "" && peer.RemoteASN == "" {
284 continue
285 }
286 peerActorID := catoBGPPeerActorID(site.ID, peer.RemoteIP, peer.RemoteASN)
287 if seen[peerActorID] {
288 continue
289 }
290 seen[peerActorID] = true
291 peerActor := addBGPPeerActor(actors, dict, site, peer, peerActorID)
292 actorIndexes[peerActorID] = peerActor
293 addBGPLink(links, dict, siteActor, peerActor, peer)
294 }
295 }
296
297 func addBGPPeerActor(table *topologyv1.TableBuilder, dict *topologyv1.StringDictionary, site *siteState, peer bgpPeerState, peerActorID string) int {
298 displayName := peer.RemoteIP
299 if displayName == "" {
300 displayName = peer.RemoteASN
301 }
302 return table.Add(
303 dict.Ref(catofunc.ActorTypeBGPPeer),
304 dict.Ref(topologyLayer),
305 peerActorID,
306 displayName,
307 "",
308 site.ID,
309 "",
310 site.PopName,
311 peer.RemoteIP,
312 peer.RemoteASN,
313 peer.BGPSession,
314 "",
315 "",
316 "",
317 "",
318 "",
319 "",
320 int64(0),
321 nil,
322 "",
323 "",
324 "",
325 nil,
326 )
327 }
328
329 func addBGPLink(table *topologyv1.TableBuilder, dict *topologyv1.StringDictionary, siteActor, peerActor int, peer bgpPeerState) {
330 table.Add(
331 dict.Ref(catofunc.LinkTypeBGP),
332 siteActor,
333 peerActor,
334 dict.Ref("bgp"),
335 dict.Ref(peer.BGPSession),
336 dict.Ref("bidirectional"),
337 1,
338 nil,
339 nil,
340 nil,
341 nil,
342 peer.RoutesCount,
343 peer.RoutesCountLimit,
344 peer.RoutesCountLimitExceeded,
345 peer.RIBOutRoutes,
346 peer.IncomingState,
347 peer.OutgoingState,
348 )
349 }
350
351 func addInterfaceTopologyTable(interfaces *topologyv1.TableBuilder, siteActor int, deviceActors map[string]int, site *siteState) {
352 ifaceKeys := make([]string, 0, len(site.Interfaces))
353 for key := range site.Interfaces {
354 ifaceKeys = append(ifaceKeys, key)
355 }
356 sort.Slice(ifaceKeys, func(i, j int) bool {
357 left := site.Interfaces[ifaceKeys[i]]
358 right := site.Interfaces[ifaceKeys[j]]
359 var leftName, rightName string
360 if left != nil {
361 leftName = left.Name
362 }
363 if right != nil {
364 rightName = right.Name
365 }
366 if leftName != rightName {
367 return leftName < rightName
368 }
369 return ifaceKeys[i] < ifaceKeys[j]
370 })
371 for _, key := range ifaceKeys {
372 iface := site.Interfaces[key]
373 if iface == nil {
374 continue
375 }
376 actor := siteActor
377 if iface.DeviceID != "" {
378 if devActor, ok := deviceActors[iface.DeviceID]; ok {
379 actor = devActor
380 }
381 }
382 interfaces.Add(
383 actor,
384 iface.ID,
385 iface.Name,
386 iface.Type,
387 iface.Connected || iface.LinkUp,
388 iface.PopName,
389 iface.TunnelRemoteIP,
390 iface.TunnelUptime,
391 iface.UpstreamBandwidth,
392 iface.DownstreamBandwidth,
393 )
394 }
395 }
396
397 func trafficMetricValue(metrics trafficMetrics, metric trafficMetricPresence, value float64) any {
398 if !metrics.has(metric) {
399 return nil
400 }
401 return value
402 }
403
404 func catoSiteActorID(siteID string) string {
405 return "cato:site:" + siteID
406 }
407
408 func catoPopActorID(popName string) string {
409 return "cato:pop:" + popName
410 }
411
412 func catoDeviceActorID(siteID, deviceID string) string {
413 return fmt.Sprintf("cato:device:%s:%s", siteID, deviceID)
414 }
415
416 func catoBGPPeerActorID(siteID, remoteIP, remoteASN string) string {
417 return fmt.Sprintf("cato:bgp:%s:%s:%s", siteID, remoteIP, remoteASN)
418 }
419
420 func catoTopologyTypes() topologyv1.TypeRegistry {
421 return topologyv1.TypeRegistry{
422 ActorTypes: catofunc.TopologyActorTypes(),
423 LinkTypes: catofunc.TopologyLinkTypes(),
424 TableTypes: map[string]topologyv1.TableType{
425 catofunc.ActorTableInterfaces: {
426 Role: "actor_detail",
427 Owner: "actor",
428 Aggregation: "append",
429 Columns: catoTopologyInterfaceColumns(),
430 },
431 },
432 AggregationScopes: map[string]topologyv1.AggregationScope{
433 "site": {
434 Columns: []string{"site_id"},
435 EvidencePolicy: "preserve",
436 },
437 "pop": {
438 Columns: []string{"pop_name"},
439 EvidencePolicy: "preserve",
440 },
441 "network": {
442 Columns: []string{"type"},
443 EvidencePolicy: "preserve",
444 },
445 },
446 }
447 }
448
449 func catoTopologyActorColumns() []topologyv1.Column {
450 return []topologyv1.Column{
451 topologyv1.NewColumn("type", "string_ref", topologyv1.WithDictionary("strings")),
452 topologyv1.NewColumn("layer", "string_ref", topologyv1.WithDictionary("strings")),
453 topologyv1.NewColumn("id", "string", topologyv1.WithRole("identity")),
454 topologyv1.NewColumn("display_name", "string"),
455 topologyv1.NewColumn("account_id", "string", topologyv1.WithRole("merge_identity")),
456 topologyv1.NewColumn("site_id", "string", topologyv1.WithRole("merge_identity")),
457 topologyv1.NewColumn("device_id", "string", topologyv1.WithRole("merge_identity")),
458 topologyv1.NewColumn("pop_name", "string", topologyv1.WithRole("merge_identity")),
459 topologyv1.NewColumn("remote_ip", "ip", topologyv1.WithRole("merge_identity")),
460 topologyv1.NewColumn("remote_asn", "string", topologyv1.WithRole("merge_identity")),
461 topologyv1.NewColumn("connectivity_status", "string"),
462 topologyv1.NewColumn("operational_status", "string"),
463 topologyv1.NewColumn("site_type", "string"),
464 topologyv1.NewColumn("connection_type", "string"),
465 topologyv1.NewColumn("country_code", "string"),
466 topologyv1.NewColumn("country_name", "string"),
467 topologyv1.NewColumn("region", "string"),
468 topologyv1.NewColumn("host_count", "uint", topologyv1.WithAggregation("sum")),
469 topologyv1.NewColumn("connected", "bool", topologyv1.WithNullable()),
470 topologyv1.NewColumn("ha_role", "string"),
471 topologyv1.NewColumn("socket_serial", "string"),
472 topologyv1.NewColumn("socket_version", "string"),
473 topologyv1.NewColumn("internal_ip", "ip", topologyv1.WithNullable()),
474 }
475 }
476
477 func catoTopologyLinkColumns() []topologyv1.Column {
478 return []topologyv1.Column{
479 topologyv1.NewColumn("type", "string_ref", topologyv1.WithDictionary("strings")),
480 topologyv1.NewColumn("src_actor", "actor_ref"),
481 topologyv1.NewColumn("dst_actor", "actor_ref"),
482 topologyv1.NewColumn("protocol", "string_ref", topologyv1.WithDictionary("strings")),
483 topologyv1.NewColumn("state", "string_ref", topologyv1.WithDictionary("strings")),
484 topologyv1.NewColumn("direction", "string_ref", topologyv1.WithDictionary("strings")),
485 topologyv1.NewColumn("evidence_count", "uint", topologyv1.WithAggregation("sum")),
486 topologyv1.NewColumn("bytes_upstream_max", "float", topologyv1.WithNullable(), topologyv1.WithRole("metric"), topologyv1.WithAggregation("max")),
487 topologyv1.NewColumn("bytes_downstream_max", "float", topologyv1.WithNullable(), topologyv1.WithRole("metric"), topologyv1.WithAggregation("max")),
488 topologyv1.NewColumn("lost_upstream_percent", "float", topologyv1.WithNullable(), topologyv1.WithRole("metric"), topologyv1.WithAggregation("avg")),
489 topologyv1.NewColumn("rtt_ms", "float", topologyv1.WithNullable(), topologyv1.WithRole("metric"), topologyv1.WithUnit("ms"), topologyv1.WithAggregation("avg")),
490 topologyv1.NewColumn("routes", "int", topologyv1.WithNullable(), topologyv1.WithRole("metric"), topologyv1.WithAggregation("sum")),
491 topologyv1.NewColumn("routes_limit", "int", topologyv1.WithNullable(), topologyv1.WithRole("metric"), topologyv1.WithAggregation("max")),
492 topologyv1.NewColumn("routes_limit_exceeded", "bool", topologyv1.WithNullable()),
493 topologyv1.NewColumn("rib_out_routes", "int", topologyv1.WithNullable(), topologyv1.WithRole("metric"), topologyv1.WithAggregation("sum")),
494 topologyv1.NewColumn("incoming_connection_state", "string", topologyv1.WithNullable()),
495 topologyv1.NewColumn("outgoing_connection_state", "string", topologyv1.WithNullable()),
496 }
497 }
498
499 func catoTopologyInterfaceColumns() []topologyv1.Column {
500 return []topologyv1.Column{
501 topologyv1.NewColumn("actor", "actor_ref"),
502 topologyv1.NewColumn("id", "string"),
503 topologyv1.NewColumn("name", "string"),
504 topologyv1.NewColumn("type", "string"),
505 topologyv1.NewColumn("connected", "bool"),
506 topologyv1.NewColumn("pop_name", "string"),
507 topologyv1.NewColumn("tunnel_remote_ip", "ip"),
508 topologyv1.NewColumn("tunnel_uptime", "duration"),
509 topologyv1.NewColumn("upstream_bandwidth", "int"),
510 topologyv1.NewColumn("downstream_bandwidth", "int"),
511 }
512 }