309 lines
9.1 KiB
TypeScript
309 lines
9.1 KiB
TypeScript
import moment from 'moment'
|
||
import { normalizeInterface } from '../ultils/helper.js'
|
||
import { PhysicalTestReport, PhysicalTestResult, PortState } from '../ultils/types.js'
|
||
const LINK_REGEX =
|
||
/Interface\s+((?:FastEthernet|GigabitEthernet|TenGigabitEthernet|TwentyFiveGigE|FortyGigabitEthernet|HundredGigE|Ethernet|Port-channel|Fa|Gi|Te|Hu|Eth)[\w\/.-]+),\s+changed state to\s+(up|down)/i
|
||
const POE_GRANTED_REGEX =
|
||
/.*%ILPOWER-\d+-POWER_GRANTED:\s+Interface\s+([\w\/.-]+):\s+Power granted/i
|
||
const POE_DISCONNECT_REGEX =
|
||
/%ILPOWER-\d+-IEEE_DISCONNECT:\s+Interface\s+([\w\/.-]+):\s+PD removed/i
|
||
|
||
export class PhysicalPortTest {
|
||
public ports = new Map<string, PortState>()
|
||
private expectedPorts: string[]
|
||
public done = false
|
||
private startTime: Date
|
||
public inventory: any
|
||
|
||
constructor(expectedPorts: string[]) {
|
||
this.expectedPorts = expectedPorts
|
||
this.startTime = new Date()
|
||
this.inventory = ''
|
||
|
||
expectedPorts.forEach((p) => {
|
||
this.ports.set(normalizeInterface(p), {
|
||
name: normalizeInterface(p),
|
||
tested: false,
|
||
})
|
||
})
|
||
}
|
||
|
||
start(expectedPorts: string[], inventory: any) {
|
||
this.ports.clear()
|
||
this.startTime = new Date()
|
||
this.expectedPorts = expectedPorts
|
||
this.inventory = inventory
|
||
this.done = false
|
||
expectedPorts.forEach((p) => {
|
||
this.ports.set(normalizeInterface(p), {
|
||
name: normalizeInterface(p),
|
||
tested: false,
|
||
})
|
||
})
|
||
// this.connection.writeCommand('terminal length 0')
|
||
// this.connection.writeCommand('terminal monitor')
|
||
// this.connection.onLog((line) => {
|
||
// this.handleLog(line);
|
||
// });
|
||
}
|
||
|
||
handleLog(lines: string) {
|
||
for (const line of lines.split('\n')) {
|
||
let iface: string | null = null
|
||
let markTested = false
|
||
let state: 'up' | 'down' | undefined
|
||
|
||
// 1️⃣ LINK / LINEPROTO
|
||
let match = line.match(LINK_REGEX)
|
||
if (match) {
|
||
iface = normalizeInterface(match[1])
|
||
state = match[2] as 'up' | 'down'
|
||
if (state === 'up') markTested = true
|
||
}
|
||
|
||
// 2️⃣ POE POWER GRANTED
|
||
match = line.match(POE_GRANTED_REGEX)
|
||
if (match) {
|
||
iface = normalizeInterface(match[1])
|
||
state = 'up'
|
||
markTested = true
|
||
}
|
||
|
||
// 3️⃣ POE DISCONNECT
|
||
// match = line.match(POE_DISCONNECT_REGEX)
|
||
// if (match) {
|
||
// iface = normalizeInterface(match[1])
|
||
// markTested = true
|
||
// }
|
||
|
||
if (!iface) continue
|
||
|
||
const port = this.ports.get(iface)
|
||
if (!port) continue
|
||
|
||
port.lastSeen = new Date()
|
||
|
||
if (state && port.lastState === state) continue
|
||
if (state) port.lastState = state
|
||
|
||
// ⭐ PASS nếu có ít nhất 1 event hợp lệ
|
||
if (markTested && !port.tested) {
|
||
port.tested = true
|
||
this.checkDone()
|
||
}
|
||
}
|
||
return this.getTestedPorts()
|
||
}
|
||
|
||
getTestedPorts(): string[] {
|
||
return Array.from(this.ports.values())
|
||
.filter((p) => p.tested)
|
||
.map((p) => p.name)
|
||
.sort()
|
||
}
|
||
|
||
resetTestedPorts() {
|
||
// this.ports.clear()
|
||
this.expectedPorts.forEach((p) => {
|
||
this.ports.set(normalizeInterface(p), {
|
||
name: normalizeInterface(p),
|
||
tested: false,
|
||
})
|
||
})
|
||
}
|
||
|
||
private checkDone() {
|
||
const testedCount = [...this.ports.values()].filter((p) => p.tested).length
|
||
|
||
if (testedCount === this.expectedPorts.length) {
|
||
this.done = true
|
||
this.onDone()
|
||
}
|
||
}
|
||
|
||
onDone() {
|
||
this.getFormReport()
|
||
// this.ports.clear()
|
||
console.log('✅ Physical Test DONE')
|
||
}
|
||
|
||
getFormReport() {
|
||
const report: PhysicalTestReport = {
|
||
device: {
|
||
model: this?.inventory?.pid || '',
|
||
serial: this?.inventory?.sn || '',
|
||
},
|
||
startTime: this.startTime,
|
||
endTime: new Date(),
|
||
durationMs: Date.now() - this.startTime.getTime(),
|
||
ports: Array.from(this.ports.values()),
|
||
}
|
||
return this.generateEmailReport(report)
|
||
// console.log('✅ Physical Test DONE')
|
||
}
|
||
|
||
getResult(): PhysicalTestResult {
|
||
const tested = [...this.ports.values()].filter((p) => p.tested)
|
||
const missing = [...this.ports.values()].filter((p) => !p.tested).map((p) => p.name)
|
||
|
||
return {
|
||
expected: this.expectedPorts.length,
|
||
tested: tested.length,
|
||
missingPorts: missing,
|
||
status: this.done ? 'DONE' : 'RUNNING',
|
||
}
|
||
}
|
||
|
||
generateEmailReport(report: PhysicalTestReport): string {
|
||
const tested = report.ports.filter((p) => p.tested)
|
||
const missing = report.ports.filter((p) => !p.tested)
|
||
const testedPoE = tested.filter((p) => !p.name.includes('SFP'))
|
||
const testedSFP = tested.filter((p) => p.name.includes('SFP'))
|
||
const missingPoE = missing.filter((p) => !p.name.includes('SFP'))
|
||
const missingSFP = missing.filter((p) => p.name.includes('SFP'))
|
||
|
||
const status = missing.length === 0 ? 'PASS' : 'WARNING'
|
||
|
||
return `
|
||
<b>Physical Ports Test Report</b><br/>
|
||
<table cellpadding="6" cellspacing="0" border="1" style="margin-top: 10px; border-collapse: collapse; width: 100%">
|
||
<tr>
|
||
<td style="width: 50%;">
|
||
Model : <b>${report.device.model ?? 'N/A'}</b><br/>
|
||
Serial Number : <b>${report.device.serial ?? 'N/A'}</b><br/>
|
||
Status : ${status === 'PASS' ? '✅ PASS' : '⚠️ WARNING'}<br/>
|
||
</td>
|
||
<td>
|
||
Total Ports : ${report.ports.length}<br/>
|
||
Ports Tested (UP) : <b style="color: #008000;">${tested.length}</b><br/>
|
||
Ports Missing : <b style="color: #ff0000;">${missing.length}</b><br/>
|
||
</td>
|
||
</tr>
|
||
</table>
|
||
<br/>
|
||
${
|
||
missing.length
|
||
? `
|
||
────────────────────────────────<br/>
|
||
<b style="color: #ff0000;">Missing Ports</b><br/>
|
||
<table cellpadding="6" cellspacing="0" border="1" style="margin-top: 10px; border-collapse: collapse; width: 100%">
|
||
<tr>
|
||
${
|
||
missingPoE?.length
|
||
? `<td>
|
||
<div style="column-count: 12;">${missingPoE.map((p) => this.normalizePortName(p.name)).join('<br/>')}</div>
|
||
</td>`
|
||
: ''
|
||
}
|
||
${
|
||
missingSFP?.length
|
||
? `<td>
|
||
<div style="column-count: 4;">${missingSFP.map((p) => this.normalizePortName(p.name)).join('<br/>')}</div>
|
||
</td>`
|
||
: ''
|
||
}
|
||
</tr>
|
||
</table>
|
||
`
|
||
: ''
|
||
}
|
||
<br/>
|
||
────────────────────────────────<br/>
|
||
<b style="color: #008000;">Passed Ports</b><br/>
|
||
${
|
||
tested.length
|
||
? `<table cellpadding="6" cellspacing="0" border="1" style="margin-top: 10px; border-collapse: collapse; width: 100%">
|
||
<tr>
|
||
${
|
||
testedPoE?.length
|
||
? `<td>
|
||
<div style="column-count: 12;">${testedPoE.map((p) => this.normalizePortName(p.name)).join('<br/>')}</div>
|
||
</td>`
|
||
: ''
|
||
}
|
||
${
|
||
testedSFP?.length
|
||
? `<td>
|
||
<div style="column-count: 4;">${testedSFP.map((p) => this.normalizePortName(p.name)).join('<br/>')}</div>
|
||
</td>`
|
||
: ''
|
||
}
|
||
</tr>
|
||
</table><br/>
|
||
`
|
||
: ''
|
||
}
|
||
<br/>
|
||
`.trim()
|
||
}
|
||
|
||
formatDuration(ms: number): string {
|
||
const totalSeconds = Math.floor(ms / 1000)
|
||
const minutes = Math.floor(totalSeconds / 60)
|
||
const seconds = totalSeconds % 60
|
||
|
||
return `${minutes}m ${seconds}s`
|
||
}
|
||
|
||
normalizePortName(port: string): string {
|
||
if (!port) return ''
|
||
|
||
// Example inputs: "Fa0/1", "Gi0/0/1", "Fa0/0/2"
|
||
const isSFP = port.includes('SFP')
|
||
const match = port
|
||
.replace('(SFP)', '')
|
||
.trim()
|
||
.match(/^([A-Za-z]+)([\d/]+)$/)
|
||
|
||
if (!match) return port
|
||
|
||
const type = match[1] // Fa, Gi, Te, etc.
|
||
const numbers = match[2] // "0/1" / "0/0/1" / "0/0/2"
|
||
|
||
// Get the last part after slash
|
||
const parts = numbers.split('/')
|
||
const last = parts[parts.length - 1]
|
||
const preLast = parts?.length > 1 ? parts[parts.length - 2] : ''
|
||
|
||
return `${type?.slice(0, 2)}${preLast ? preLast + '/' : ''}${last}${isSFP ? ' (SFP)' : ''}`
|
||
}
|
||
|
||
/**
|
||
* Function 1: Lấy danh sách các cổng có module SFP từ lệnh 'show interfaces status'
|
||
* Logic: Tìm dòng bắt đầu bằng Tên Port và có chứa từ khóa "SFP" ở cuối.
|
||
* Function 2: Lấy danh sách các cổng đang cấp nguồn (PoE on) từ lệnh 'show power inline'
|
||
* Logic: Tìm dòng bắt đầu bằng Tên Port, cột tiếp theo là Admin status, cột tiếp theo là 'on'.
|
||
*/
|
||
detectPorts(output: string): string[] {
|
||
for (const line of output.split('\n')) {
|
||
if (line?.includes('include')) continue
|
||
const ports: string[] = []
|
||
const regexPoE = /^([A-Za-z0-9\/]+).*\on\b/i
|
||
const regexSFP = /^([A-Za-z0-9\/]+).*\bSFP\b/i
|
||
|
||
let matchPoE = line.match(regexPoE)
|
||
if (matchPoE) {
|
||
ports.push(matchPoE[1])
|
||
}
|
||
let matchSFP = line.match(regexSFP)
|
||
if (matchSFP) {
|
||
ports.push(matchSFP[1] + ' (SFP)')
|
||
}
|
||
if (ports.length > 0) {
|
||
ports
|
||
.filter((el) => el)
|
||
.forEach((el) => {
|
||
const iface = normalizeInterface(el)
|
||
const port = this.ports.get(iface)
|
||
if (port) {
|
||
port.lastState = 'up'
|
||
port.tested = true
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
return this.getTestedPorts()
|
||
}
|
||
}
|