Files
network-topology-discovery/internal/device/h3c.go
T
Your Name 21326b6731 Fix: 添加screen-length disable命令并恢复SSH客户端
- 在H3C命令列表最前面添加screen-length disable
- 恢复SSH客户端为原始简单模式(session.Run)
- 修正解析器输出索引适配新命令列表
2026-04-26 01:58:23 +08:00

387 baris
11 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 ip interface brief",
"display lldp neighbor-information",
"display arp", // ARP表用于MAC到IP的映射
}
}
// Parse 解析H3C设备输出
func (p *H3CParser) Parse(device *models.Device, outputs []string) error {
if len(outputs) < 6 {
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 && len(outputs[2]) <= 200 {
fmt.Printf("[H3C DEBUG] Full output: %q\n", outputs[2])
} else if len(outputs[2]) > 200 {
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 {
device.Interfaces = p.parseInterfaces(outputs[2], outputs[3]) // outputs[3] 是 display ip interface brief
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
// outputs[5] 是 display arp
// 解析ARP表用于MAC到IP映射(允许失败)
arpTable := make(map[string]string)
if outputs[4] != "" {
arpTable = p.parseARPTable(outputs[4])
if len(arpTable) == 0 {
fmt.Printf("Warning: ARP table is empty for device %s\n", device.IP)
}
} else {
fmt.Printf("Warning: ARP command output is empty for device %s\n", device.IP)
}
// 解析LLDP邻居,使用ARP表进行MAC到IP的映射
device.Neighbors = p.parseNeighbors(outputs[3], arpTable)
fmt.Printf("Device %s: %d interfaces, %d neighbors, %d ARP entries\n",
device.IP, len(device.Interfaces), len(device.Neighbors), len(arpTable))
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地址)
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]))
// 保存MAC地址
currentNeighbor.RemoteMAC = mac
fmt.Printf(" Parsed neighbor MAC: %s (from line: %s)\n", mac, line)
// 通过ARP表查找IP(如果有)
if ip, ok := arpTable[mac]; ok {
currentNeighbor.RemoteIP = ip
currentNeighbor.RemoteDevice = ip
} else {
// 如果ARP表中没有,使用MAC地址作为标识(但RemoteIP仍为空)
currentNeighbor.RemoteDevice = mac
}
} else {
fmt.Printf(" WARNING: Could not parse MAC from ChassisID line: %s\n", line)
}
} else {
fmt.Printf(" WARNING: ChassisID line has no colon: %s\n", line)
}
}
// 提取 PortID (远程接口)
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], "/")
}
}
}
}
}
// 添加最后一个邻居
if currentNeighbor != nil && currentNeighbor.RemoteInterface != "" {
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
}