ATC_SIMPLE/BACKEND/app/services/physical_test_service.ts

227 lines
6.6 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])
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 status = missing.length === 0 ? 'PASS' : 'WARNING'
return `
<b>Physical Port 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) : ${tested.length}<br/>
Ports Missing : ${missing.length}<br/>
</td>
</tr>
</table>
<br/>
────────────────────────────────<br/>
<b>Passed Ports</b><br/>
${
tested.length
? `<div style="margin-top: 10px; border: 1px solid #ccc; padding: 5px; column-count: 12;">${tested.map((p) => this.normalizePortName(p.name)).join('<br/>')}</div><br/>
`
: ''
}
<br/>
${
missing.length
? `
────────────────────────────────<br/>
<b>Missing Ports</b><br/>
<div style="margin-top: 10px; border: 1px solid #ccc; padding: 5px; column-count: 12;">${missing.map((p) => this.normalizePortName(p.name)).join('<br/>')}</div><br/>
`
: ''
}<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 match = port.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}`
}
}