| 1 | // SPDX-License-Identifier: GPL-3.0-or-later |
| 2 | |
| 3 | package l2topology |
| 4 | |
| 5 | import ( |
| 6 | "strings" |
| 7 | ) |
| 8 | |
| 9 | func topologyDeviceInferred(dev Device) bool { |
| 10 | if len(dev.Labels) == 0 { |
| 11 | return false |
| 12 | } |
| 13 | switch strings.ToLower(strings.TrimSpace(dev.Labels["inferred"])) { |
| 14 | case "1", "true", "yes", "on": |
| 15 | return true |
| 16 | default: |
| 17 | return false |
| 18 | } |
| 19 | } |
| 20 | |
| 21 | func buildDeviceActorMatch(dev Device, reporterAliases []string) Match { |
| 22 | match := Match{ |
| 23 | SysObjectID: strings.TrimSpace(dev.SysObject), |
| 24 | SysName: strings.TrimSpace(dev.Hostname), |
| 25 | } |
| 26 | |
| 27 | macSet := make(map[string]struct{}, 1+len(reporterAliases)) |
| 28 | chassis := strings.TrimSpace(dev.ChassisID) |
| 29 | if chassis != "" { |
| 30 | match.ChassisIDs = []string{chassis} |
| 31 | if mac := normalizeMAC(chassis); mac != "" { |
| 32 | macSet[mac] = struct{}{} |
| 33 | } |
| 34 | } |
| 35 | for _, alias := range reporterAliases { |
| 36 | alias = strings.TrimSpace(alias) |
| 37 | if alias == "" { |
| 38 | continue |
| 39 | } |
| 40 | if after, ok := strings.CutPrefix(alias, "mac:"); ok { |
| 41 | if mac := normalizeMAC(after); mac != "" { |
| 42 | macSet[mac] = struct{}{} |
| 43 | } |
| 44 | continue |
| 45 | } |
| 46 | if mac := normalizeMAC(alias); mac != "" { |
| 47 | macSet[mac] = struct{}{} |
| 48 | } |
| 49 | } |
| 50 | if len(macSet) > 0 { |
| 51 | match.MacAddresses = sortedTopologySet(macSet) |
| 52 | } |
| 53 | |
| 54 | if len(dev.Addresses) > 0 { |
| 55 | ips := make([]string, 0, len(dev.Addresses)) |
| 56 | for _, addr := range dev.Addresses { |
| 57 | if !addr.IsValid() { |
| 58 | continue |
| 59 | } |
| 60 | ips = append(ips, addr.String()) |
| 61 | } |
| 62 | match.IPAddresses = uniqueTopologyStrings(ips) |
| 63 | } |
| 64 | |
| 65 | return match |
| 66 | } |
| 67 | |
| 68 | func buildDeviceActorAttributes( |
| 69 | dev Device, |
| 70 | localDeviceID string, |
| 71 | ifaceSummary topologyDeviceInterfaceSummary, |
| 72 | match Match, |
| 73 | ) map[string]any { |
| 74 | discovered := strings.TrimSpace(localDeviceID) == "" || dev.ID != localDeviceID |
| 75 | |
| 76 | attrs := map[string]any{ |
| 77 | "device_id": dev.ID, |
| 78 | "discovered": discovered, |
| 79 | "inferred": topologyDeviceInferred(dev), |
| 80 | "management_ip": firstAddress(dev.Addresses), |
| 81 | "management_addresses": addressStrings(dev.Addresses), |
| 82 | "protocols": labelsCSVToSlice(dev.Labels, "protocols_observed"), |
| 83 | "protocols_collected": labelsCSVToSlice(dev.Labels, "protocols_observed"), |
| 84 | "capabilities": labelsCSVToSlice(dev.Labels, "capabilities"), |
| 85 | "capabilities_supported": labelsCSVToSlice(dev.Labels, "capabilities_supported"), |
| 86 | "capabilities_enabled": labelsCSVToSlice(dev.Labels, "capabilities_enabled"), |
| 87 | } |
| 88 | derivedVendor, derivedPrefix := inferTopologyVendorFromMatch(match) |
| 89 | if derivedVendor != "" { |
| 90 | attrs["vendor_derived"] = derivedVendor |
| 91 | attrs["vendor_derived_source"] = "mac_oui" |
| 92 | attrs["vendor_derived_confidence"] = "low" |
| 93 | attrs["vendor_derived_match_prefix"] = derivedPrefix |
| 94 | } |
| 95 | if vendor := strings.TrimSpace(dev.Labels["vendor"]); vendor != "" { |
| 96 | attrs["vendor"] = vendor |
| 97 | attrs["vendor_source"] = "labels" |
| 98 | attrs["vendor_confidence"] = "high" |
| 99 | } else if derivedVendor != "" { |
| 100 | attrs["vendor"] = derivedVendor |
| 101 | attrs["vendor_source"] = "mac_oui" |
| 102 | attrs["vendor_confidence"] = "low" |
| 103 | attrs["vendor_match_prefix"] = derivedPrefix |
| 104 | } |
| 105 | if ifaceSummary.portsTotal > 0 { |
| 106 | attrs["ports_total"] = ifaceSummary.portsTotal |
| 107 | } |
| 108 | if len(ifaceSummary.ifIndexes) > 0 { |
| 109 | attrs["if_indexes"] = ifaceSummary.ifIndexes |
| 110 | } |
| 111 | if len(ifaceSummary.ifNames) > 0 { |
| 112 | attrs["if_names"] = ifaceSummary.ifNames |
| 113 | } |
| 114 | if ifaceSummary.portsUp > 0 { |
| 115 | attrs["ports_up"] = ifaceSummary.portsUp |
| 116 | } |
| 117 | if ifaceSummary.portsDown > 0 { |
| 118 | attrs["ports_down"] = ifaceSummary.portsDown |
| 119 | } |
| 120 | if ifaceSummary.portsAdminDown > 0 { |
| 121 | attrs["ports_admin_down"] = ifaceSummary.portsAdminDown |
| 122 | } |
| 123 | if ifaceSummary.totalBandwidthBps > 0 { |
| 124 | attrs["total_bandwidth_bps"] = ifaceSummary.totalBandwidthBps |
| 125 | } |
| 126 | if ifaceSummary.fdbTotalMACs > 0 { |
| 127 | attrs["fdb_total_macs"] = ifaceSummary.fdbTotalMACs |
| 128 | } |
| 129 | if ifaceSummary.vlanCount > 0 { |
| 130 | attrs["vlan_count"] = ifaceSummary.vlanCount |
| 131 | } |
| 132 | if ifaceSummary.lldpNeighborCount > 0 { |
| 133 | attrs["lldp_neighbor_count"] = ifaceSummary.lldpNeighborCount |
| 134 | } |
| 135 | if ifaceSummary.cdpNeighborCount > 0 { |
| 136 | attrs["cdp_neighbor_count"] = ifaceSummary.cdpNeighborCount |
| 137 | } |
| 138 | if len(ifaceSummary.adminStatusCount) > 0 { |
| 139 | attrs["if_admin_status_counts"] = ifaceSummary.adminStatusCount |
| 140 | } |
| 141 | if len(ifaceSummary.operStatusCount) > 0 { |
| 142 | attrs["if_oper_status_counts"] = ifaceSummary.operStatusCount |
| 143 | } |
| 144 | if len(ifaceSummary.linkModeCount) > 0 { |
| 145 | attrs["if_link_mode_counts"] = ifaceSummary.linkModeCount |
| 146 | } |
| 147 | if len(ifaceSummary.roleCount) > 0 { |
| 148 | attrs["if_topology_role_counts"] = ifaceSummary.roleCount |
| 149 | } |
| 150 | if len(ifaceSummary.portStatuses) > 0 { |
| 151 | attrs["if_statuses"] = ifaceSummary.portStatuses |
| 152 | } |
| 153 | return attrs |
| 154 | } |
| 155 | |
| 156 | func buildDeviceActorTables(ifaceSummary topologyDeviceInterfaceSummary) map[string][]map[string]any { |
| 157 | if len(ifaceSummary.portStatuses) == 0 { |
| 158 | return nil |
| 159 | } |
| 160 | |
| 161 | rows := make([]map[string]any, 0, len(ifaceSummary.portStatuses)) |
| 162 | for _, ps := range ifaceSummary.portStatuses { |
| 163 | row := make(map[string]any, len(ps)+2) |
| 164 | for k, v := range ps { |
| 165 | switch k { |
| 166 | case "if_name": |
| 167 | row["name"] = v |
| 168 | case "if_type": |
| 169 | row["port_type"] = v |
| 170 | default: |
| 171 | row[k] = v |
| 172 | } |
| 173 | } |
| 174 | if neighbors, ok := ps["neighbors"]; ok { |
| 175 | switch nb := neighbors.(type) { |
| 176 | case []map[string]any: |
| 177 | row["neighbor_count"] = len(nb) |
| 178 | case []any: |
| 179 | row["neighbor_count"] = len(nb) |
| 180 | } |
| 181 | } |
| 182 | rows = append(rows, row) |
| 183 | } |
| 184 | |
| 185 | return map[string][]map[string]any{"ports": rows} |
| 186 | } |