Files
network-topology-discovery/internal/device/h3c.go
T
Your Name 6d2323b5b6 feat: enhance H3C LLDP parser to support verbose format with System name and Management address
- 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.
2026-04-26 03:56:04 +08:00

435 lines
13 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}