ATC_SIMPLE/BACKEND/app/services/physical_test_service.ts

309 lines
9.1 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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()
}
}