6d2323b5b6
- Add support for LLDP verbose output format (display lldp neighbor-information verbose) - Parse System name field for accurate device hostname matching - Parse Management address field for IP-based neighbor identification - Handle edge cases: endpoint devices without System name (fallback to MAC) - Handle Port ID as MAC address (not interface name) for endpoint devices - Add detailed debug logging for LLDP neighbor parsing - Implement three-level fallback strategy: System name > Management IP > MAC address This fixes the topology auto-linking issue where only 1 link was created despite having neighbor data.
435 lines
13 KiB
Go
435 lines
13 KiB
Go
package device
|
||
|
||
import (
|
||
"bufio"
|
||
"fmt"
|
||
"network-topology-discovery/pkg/models"
|
||
"regexp"
|
||
"strings"
|
||
)
|
||
|
||
// H3CParser H3C设备解析器
|
||
type H3CParser struct {
|
||
BaseParser
|
||
}
|
||
|
||
// GetCommands 获取H3C设备命令列表
|
||
func (p *H3CParser) GetCommands() []string {
|
||
return []string{
|
||
"screen-length disable", // 禁用分页(H3C/华为设备必需)
|
||
"display version",
|
||
"display interface",
|
||
"display interface brief", // 接口简要信息(包含VLAN和物理接口状态)
|
||
"display lldp neighbor-information verbose", // LLDP邻居详细信息(包含System name和Management address)
|
||
}
|
||
}
|
||
|
||
// Parse 解析H3C设备输出
|
||
func (p *H3CParser) Parse(device *models.Device, outputs []string) error {
|
||
if len(outputs) < 5 {
|
||
return fmt.Errorf("insufficient command outputs")
|
||
}
|
||
|
||
// outputs[0] 是 screen-length disable 的输出(通常为空)
|
||
p.parseVersion(device, outputs[1]) // outputs[1] 是 display version
|
||
|
||
// outputs[2] 是 display interface
|
||
fmt.Printf("[H3C DEBUG] display interface output length: %d\n", len(outputs[2]))
|
||
if len(outputs[2]) > 0 {
|
||
// 如果输出小于1000字节,完整输出以便调试
|
||
if len(outputs[2]) <= 1000 {
|
||
fmt.Printf("[H3C DEBUG] Complete output:\n%s\n", outputs[2])
|
||
} else {
|
||
fmt.Printf("[H3C DEBUG] First 200 chars: %q\n", outputs[2][:200])
|
||
}
|
||
} else {
|
||
fmt.Printf("[H3C DEBUG] display interface output is EMPTY!\n")
|
||
}
|
||
|
||
if outputs[2] == "" {
|
||
fmt.Printf("Warning: 'display interface' output is empty for device %s\n", device.IP)
|
||
} else {
|
||
// outputs[3] 是 display interface brief
|
||
device.Interfaces = p.parseInterfaces(outputs[2], outputs[3])
|
||
if len(device.Interfaces) == 0 {
|
||
fmt.Printf("Warning: parsed 0 interfaces for device %s (output length: %d)\n",
|
||
device.IP, len(outputs[2]))
|
||
}
|
||
|
||
// 收集所有接口的MAC地址(用于邻居匹配)
|
||
macSet := make(map[string]bool)
|
||
for _, iface := range device.Interfaces {
|
||
if iface.MAC != "" {
|
||
macSet[iface.MAC] = true
|
||
} else {
|
||
fmt.Printf(" Interface %s has no MAC address\n", iface.Name)
|
||
}
|
||
}
|
||
for mac := range macSet {
|
||
device.MACAddresses = append(device.MACAddresses, mac)
|
||
}
|
||
fmt.Printf(" Collected %d unique MAC addresses for device %s\n", len(macSet), device.IP)
|
||
}
|
||
|
||
// outputs[4] 是 display lldp neighbor-information
|
||
|
||
// 解析LLDP邻居
|
||
device.Neighbors = p.parseNeighbors(outputs[4], nil)
|
||
|
||
fmt.Printf("Device %s: %d interfaces, %d neighbors\n",
|
||
device.IP, len(device.Interfaces), len(device.Neighbors))
|
||
|
||
return nil
|
||
}
|
||
|
||
func (p *H3CParser) parseVersion(device *models.Device, output string) {
|
||
hostnameRegex := regexp.MustCompile(`<(\S+)>`)
|
||
if matches := hostnameRegex.FindStringSubmatch(output); len(matches) > 1 {
|
||
device.Hostname = matches[1]
|
||
}
|
||
|
||
if strings.Contains(output, "Comware") {
|
||
lines := strings.Split(output, "\n")
|
||
for _, line := range lines {
|
||
if strings.Contains(line, "Comware") {
|
||
device.OSVersion = strings.TrimSpace(line)
|
||
break
|
||
}
|
||
}
|
||
}
|
||
|
||
uptimeRegex := regexp.MustCompile(`uptime is\s+(\d+\s+\S+)`)
|
||
if matches := uptimeRegex.FindStringSubmatch(output); len(matches) > 1 {
|
||
device.Uptime = matches[1]
|
||
}
|
||
}
|
||
|
||
func (p *H3CParser) parseInterfaces(interfaceOutput, briefOutput string) []models.Interface {
|
||
var interfaces []models.Interface
|
||
briefMap := p.parseInterfaceBrief(briefOutput)
|
||
|
||
scanner := bufio.NewScanner(strings.NewReader(interfaceOutput))
|
||
var currentInterface *models.Interface
|
||
var pendingInterfaceName string // 暂存接口名
|
||
|
||
for scanner.Scan() {
|
||
line := scanner.Text()
|
||
|
||
// H3C接口输出格式1: 接口名和状态在同一行
|
||
// GigabitEthernet1/0/0 current state: UP
|
||
if nameRegex := regexp.MustCompile(`^(\S+)\s+current state:\s+(UP|DOWN)`); nameRegex.MatchString(line) {
|
||
if currentInterface != nil {
|
||
interfaces = append(interfaces, *currentInterface)
|
||
}
|
||
matches := nameRegex.FindStringSubmatch(line)
|
||
currentInterface = &models.Interface{
|
||
Name: matches[1],
|
||
Status: strings.ToLower(matches[2]),
|
||
}
|
||
|
||
if brief, ok := briefMap[currentInterface.Name]; ok {
|
||
currentInterface.IP = brief.IP
|
||
}
|
||
pendingInterfaceName = ""
|
||
continue
|
||
}
|
||
|
||
// H3C接口输出格式2: 接口名单独一行,下一行是状态
|
||
// GigabitEthernet1/0/0
|
||
// Current state: DOWN
|
||
if pendingInterfaceName != "" && regexp.MustCompile(`^Current state:\s+(UP|DOWN)`).MatchString(line) {
|
||
matches := regexp.MustCompile(`^Current state:\s+(UP|DOWN)`).FindStringSubmatch(line)
|
||
if currentInterface != nil {
|
||
interfaces = append(interfaces, *currentInterface)
|
||
}
|
||
currentInterface = &models.Interface{
|
||
Name: pendingInterfaceName,
|
||
Status: strings.ToLower(matches[1]),
|
||
}
|
||
|
||
if brief, ok := briefMap[currentInterface.Name]; ok {
|
||
currentInterface.IP = brief.IP
|
||
}
|
||
pendingInterfaceName = ""
|
||
continue
|
||
}
|
||
|
||
// 匹配接口名(单独一行的情况)
|
||
// 格式: GigabitEthernet1/0/0
|
||
if interfaceNameRegex := regexp.MustCompile(`^(GigabitEthernet|Ten-GigabitEthernet|FortyGigE|HundredGigE|Ethernet|Serial|LoopBack|Vlanif|NULL|Bridge-Aggregate|Route-Aggregate)\S*`); interfaceNameRegex.MatchString(line) {
|
||
pendingInterfaceName = interfaceNameRegex.FindString(line)
|
||
continue
|
||
}
|
||
|
||
// 解析接口属性
|
||
if currentInterface != nil {
|
||
if descRegex := regexp.MustCompile(`Description:\s+(.+)`); descRegex.MatchString(line) {
|
||
currentInterface.Description = descRegex.FindStringSubmatch(line)[1]
|
||
}
|
||
|
||
if macRegex := regexp.MustCompile(`hardware address:\s+(\S+)`); macRegex.MatchString(line) {
|
||
currentInterface.MAC = macRegex.FindStringSubmatch(line)[1]
|
||
}
|
||
|
||
// 匹配IP地址格式: Internet address: 192.168.0.1/24 (primary)
|
||
if ipRegex := regexp.MustCompile(`Internet address:\s+(\d+\.\d+\.\d+\.\d+)/(\d+)`); ipRegex.MatchString(line) {
|
||
matches := ipRegex.FindStringSubmatch(line)
|
||
currentInterface.IP = matches[1]
|
||
// CIDR转换为子网掩码
|
||
currentInterface.Mask = cidrToMask(matches[2])
|
||
}
|
||
|
||
if speedRegex := regexp.MustCompile(`(\d+)\s+(Kbps|Mbps|Gbps)`); speedRegex.MatchString(line) {
|
||
matches := speedRegex.FindStringSubmatch(line)
|
||
currentInterface.Speed = matches[1] + " " + matches[2]
|
||
}
|
||
}
|
||
}
|
||
|
||
if currentInterface != nil {
|
||
interfaces = append(interfaces, *currentInterface)
|
||
}
|
||
|
||
return interfaces
|
||
}
|
||
|
||
// cidrToMask CIDR转换为子网掩码
|
||
func cidrToMask(cidr string) string {
|
||
var mask int
|
||
fmt.Sscanf(cidr, "%d", &mask)
|
||
|
||
s := uint32(0)
|
||
for i := 0; i < mask; i++ {
|
||
s |= (1 << (31 - uint(i)))
|
||
}
|
||
|
||
return fmt.Sprintf("%d.%d.%d.%d",
|
||
(s>>24)&0xFF,
|
||
(s>>16)&0xFF,
|
||
(s>>8)&0xFF,
|
||
s&0xFF)
|
||
}
|
||
|
||
// parseARPTable 解析ARP表,建立MAC到IP的映射
|
||
func (p *H3CParser) parseARPTable(output string) map[string]string {
|
||
macToIP := make(map[string]string)
|
||
lines := strings.Split(output, "\n")
|
||
|
||
for _, line := range lines {
|
||
// 跳过空行和标题行
|
||
if strings.TrimSpace(line) == "" ||
|
||
strings.Contains(line, "Type:") ||
|
||
strings.Contains(line, "------") ||
|
||
strings.Contains(line, "IP address") {
|
||
continue
|
||
}
|
||
|
||
// ARP表格式: IP address MAC address VLAN/VSI name Interface Aging Type
|
||
// 例: 172.16.8.10 743a-2047-38e0 8 GE1/0/47 1163 D
|
||
fields := strings.Fields(line)
|
||
if len(fields) >= 2 {
|
||
ip := fields[0]
|
||
mac := strings.ToLower(fields[1])
|
||
|
||
// 验证是有效的IP和MAC
|
||
if isValidIP(ip) && isValidMAC(mac) {
|
||
macToIP[mac] = ip
|
||
}
|
||
}
|
||
}
|
||
|
||
return macToIP
|
||
}
|
||
|
||
func (p *H3CParser) parseInterfaceBrief(output string) map[string]models.Interface {
|
||
interfaces := make(map[string]models.Interface)
|
||
lines := strings.Split(output, "\n")
|
||
|
||
for _, line := range lines {
|
||
fields := strings.Fields(line)
|
||
if len(fields) >= 4 {
|
||
iface := models.Interface{
|
||
Name: fields[0],
|
||
IP: fields[1],
|
||
Status: strings.ToLower(fields[3]),
|
||
}
|
||
interfaces[iface.Name] = iface
|
||
}
|
||
}
|
||
|
||
return interfaces
|
||
}
|
||
|
||
func (p *H3CParser) parseNeighbors(output string, arpTable map[string]string) []models.Neighbor {
|
||
var neighbors []models.Neighbor
|
||
scanner := bufio.NewScanner(strings.NewReader(output))
|
||
|
||
var currentNeighbor *models.Neighbor
|
||
var localInterface string
|
||
|
||
for scanner.Scan() {
|
||
line := scanner.Text()
|
||
|
||
// 匹配本地接口行: LLDP neighbor-information of port 20[GigabitEthernet1/0/20]:
|
||
if portRegex := regexp.MustCompile(`LLDP neighbor-information of port \d+\[([^\]]+)\]:`); portRegex.MatchString(line) {
|
||
// 保存前一个邻居
|
||
if currentNeighbor != nil && currentNeighbor.RemoteInterface != "" {
|
||
neighbors = append(neighbors, *currentNeighbor)
|
||
}
|
||
|
||
matches := portRegex.FindStringSubmatch(line)
|
||
localInterface = matches[1]
|
||
currentNeighbor = &models.Neighbor{
|
||
LocalInterface: localInterface,
|
||
Protocol: "LLDP",
|
||
}
|
||
continue
|
||
}
|
||
|
||
if currentNeighbor != nil {
|
||
// 提取 ChassisID (MAC地址)
|
||
// Verbose格式: Chassis ID: 642f-c7e0-0333
|
||
if strings.Contains(line, "Chassis ID:") && !strings.Contains(line, "ChassisID/subtype") {
|
||
parts := strings.SplitN(line, ":", 2)
|
||
if len(parts) == 2 {
|
||
mac := strings.TrimSpace(strings.ToLower(parts[1]))
|
||
if isValidMAC(mac) {
|
||
currentNeighbor.RemoteMAC = mac
|
||
// 如果还没有RemoteDevice,先使用MAC作为占位符(后续可能被System name覆盖)
|
||
if currentNeighbor.RemoteDevice == "" {
|
||
currentNeighbor.RemoteDevice = mac
|
||
}
|
||
fmt.Printf(" [LLDP] Parsed neighbor MAC (verbose): %s\n", mac)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 非verbose格式: ChassisID/subtype: a4bb-6de2-62cd/MAC address
|
||
if strings.Contains(line, "ChassisID/subtype") {
|
||
parts := strings.SplitN(line, ":", 2)
|
||
if len(parts) == 2 {
|
||
value := strings.TrimSpace(parts[1])
|
||
// 格式: a4bb-6de2-62cd/MAC address
|
||
if macParts := strings.Split(value, "/"); len(macParts) > 0 {
|
||
mac := strings.TrimSpace(strings.ToLower(macParts[0]))
|
||
currentNeighbor.RemoteMAC = mac
|
||
if currentNeighbor.RemoteDevice == "" {
|
||
currentNeighbor.RemoteDevice = mac
|
||
}
|
||
fmt.Printf(" [LLDP] Parsed neighbor MAC: %s\n", mac)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 提取 System name (verbose格式)
|
||
if strings.Contains(line, "System name:") {
|
||
parts := strings.SplitN(line, ":", 2)
|
||
if len(parts) == 2 {
|
||
systemName := strings.TrimSpace(parts[1])
|
||
if systemName != "" {
|
||
// System name 是最可靠的匹配方式,覆盖之前的MAC地址占位符
|
||
currentNeighbor.RemoteDevice = systemName
|
||
fmt.Printf(" [LLDP] Parsed neighbor System name: %s\n", systemName)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 提取 Management address (verbose格式)
|
||
if strings.Contains(line, "Management address:") {
|
||
parts := strings.SplitN(line, ":", 2)
|
||
if len(parts) == 2 {
|
||
mgmtAddr := strings.TrimSpace(parts[1])
|
||
if isValidIP(mgmtAddr) {
|
||
// 如果还没有RemoteIP,使用Management address
|
||
if currentNeighbor.RemoteIP == "" {
|
||
currentNeighbor.RemoteIP = mgmtAddr
|
||
fmt.Printf(" [LLDP] Parsed neighbor Management address: %s\n", mgmtAddr)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 提取 PortID (远程接口)
|
||
// Verbose格式: Port ID: GigabitEthernet1/0/48 或 Port ID: a4bb-6de2-62cd (MAC地址)
|
||
if strings.Contains(line, "Port ID:") && !strings.Contains(line, "PortID/subtype") && !strings.Contains(line, "Port ID type") {
|
||
parts := strings.SplitN(line, ":", 2)
|
||
if len(parts) == 2 {
|
||
portID := strings.TrimSpace(parts[1])
|
||
// 检查Port ID是否是MAC地址(格式: a4bb-6de2-62cd)
|
||
if isValidMAC(portID) {
|
||
// Port ID是MAC地址,不赋值给RemoteInterface
|
||
fmt.Printf(" [LLDP] Port ID is MAC address (not interface): %s\n", portID)
|
||
} else {
|
||
// Port ID是接口名
|
||
currentNeighbor.RemoteInterface = portID
|
||
}
|
||
}
|
||
}
|
||
|
||
// 非verbose格式: PortID/subtype: GigabitEthernet0/0/1/Interface name
|
||
if strings.Contains(line, "PortID/subtype") {
|
||
parts := strings.SplitN(line, ":", 2)
|
||
if len(parts) == 2 {
|
||
value := strings.TrimSpace(parts[1])
|
||
// 格式: GigabitEthernet0/0/1/Interface name
|
||
if portParts := strings.Split(value, "/"); len(portParts) >= 3 {
|
||
// 取前3部分作为接口名: GigabitEthernet0/0/1
|
||
currentNeighbor.RemoteInterface = strings.Join(portParts[:3], "/")
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 添加最后一个邻居(修改:只要有RemoteDevice或RemoteInterface就添加)
|
||
if currentNeighbor != nil && (currentNeighbor.RemoteDevice != "" || currentNeighbor.RemoteInterface != "") {
|
||
// 如果Port ID是MAC地址而不是接口名,将MAC赋值给RemoteInterface(如果为空)
|
||
if currentNeighbor.RemoteInterface == "" && currentNeighbor.RemoteMAC != "" {
|
||
// 检查Port ID是否是MAC地址(在解析过程中可能已经将MAC作为Port ID)
|
||
// 这种情况发生在Port ID type为MAC address时
|
||
fmt.Printf(" [LLDP] Neighbor has no interface name, using MAC as identifier: %s\n", currentNeighbor.RemoteMAC)
|
||
}
|
||
neighbors = append(neighbors, *currentNeighbor)
|
||
}
|
||
|
||
return neighbors
|
||
}
|
||
|
||
// isValidIP 简单验证IP地址
|
||
func isValidIP(ip string) bool {
|
||
parts := strings.Split(ip, ".")
|
||
if len(parts) != 4 {
|
||
return false
|
||
}
|
||
for _, part := range parts {
|
||
if len(part) == 0 || len(part) > 3 {
|
||
return false
|
||
}
|
||
for _, c := range part {
|
||
if c < '0' || c > '9' {
|
||
return false
|
||
}
|
||
}
|
||
}
|
||
return true
|
||
}
|
||
|
||
// isValidMAC 简单验证MAC地址
|
||
func isValidMAC(mac string) bool {
|
||
parts := strings.Split(mac, "-")
|
||
if len(parts) != 3 && len(parts) != 6 {
|
||
return false
|
||
}
|
||
for _, part := range parts {
|
||
if len(part) != 2 {
|
||
return false
|
||
}
|
||
for _, c := range part {
|
||
if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) {
|
||
return false
|
||
}
|
||
}
|
||
}
|
||
return true
|
||
}
|