From 2f484e19b6ec6b1b1c60f82b0dd35ad575fac146 Mon Sep 17 00:00:00 2001 From: nguyentrungthat <80239428+nguentrungthat@users.noreply.github.com> Date: Thu, 25 Dec 2025 16:17:52 +0700 Subject: [PATCH] update physical test --- BACKEND/app/services/line_connection.ts | 114 ++++++++++++++++-- BACKEND/app/services/physical_test_service.ts | 97 +++++++++++++++ BACKEND/app/ultils/helper.ts | 9 ++ BACKEND/app/ultils/types.ts | 14 +++ BACKEND/providers/socket_io_provider.ts | 35 +++++- FRONTEND/src/App.tsx | 6 +- .../src/components/Modal/ModalTerminal.tsx | 104 ++++++++++++++-- FRONTEND/src/untils/types.ts | 3 +- 8 files changed, 359 insertions(+), 23 deletions(-) create mode 100644 BACKEND/app/services/physical_test_service.ts diff --git a/BACKEND/app/services/line_connection.ts b/BACKEND/app/services/line_connection.ts index ad0c225..0872fac 100644 --- a/BACKEND/app/services/line_connection.ts +++ b/BACKEND/app/services/line_connection.ts @@ -12,6 +12,7 @@ import { LogStreamBuffer, mapErrorsToRows, mapToLineFormat, + normalizeInterface, sendMessageToMail, sleep, TestSession, @@ -25,6 +26,7 @@ import Line from '#models/line' import { ErrorRow, TestResult } from '../ultils/types.js' import moment from 'moment' import momentTZ from 'moment-timezone' +import { PhysicalPortTest } from './physical_test_service.js' type Inventory = { pid: string @@ -66,7 +68,9 @@ interface LineConfig { output: string textfsm: string }[] - commands: string[] + ports: string[] + runningScenario: string + runningPhysical: boolean // history: string } @@ -111,7 +115,6 @@ export default class LineConnection { public config: LineConfig public readonly socketIO: any private outputBuffer: string - private isRunningScript: boolean private connecting: boolean private waitingScenario: boolean private outputInventory: string @@ -121,13 +124,14 @@ export default class LineConnection { private listScenarios: number[] public handleClearLine: () => void private session: TestSession + private physicalTest: PhysicalPortTest + private outputPhysicalTest: string constructor(config: LineConfig, socketIO: any, handleClearLine: () => void) { this.config = config this.socketIO = socketIO this.client = new net.Socket() this.outputBuffer = '' - this.isRunningScript = false this.connecting = false this.waitingScenario = false this.outputInventory = '' @@ -147,6 +151,8 @@ export default class LineConnection { this.listScenarios = [] this.session = new TestSession() this.handleClearLine = handleClearLine + this.physicalTest = new PhysicalPortTest([]) + this.outputPhysicalTest = '' } connect(timeoutMs = 5000) { @@ -182,13 +188,23 @@ export default class LineConnection { const lines = this.bufferLog.push(data) lines.forEach(this.handleLogLine) let rawData = '' - if (this.isRunningScript) { + if (this.config.runningScenario) { this.waitingScenario = true this.outputBuffer += message this.outputScenario += message if (!this.config.inventory) this.outputInventory = this.outputInventory.slice(-3000) + message } + if (this.config.runningPhysical) { + this.outputPhysicalTest += message + const ports = this.physicalTest.handleLog(message) + if (ports?.length) + this.socketIO.emit('test_port_physical', { + stationId, + lineId: id, + data: ports, + }) + } if (data.toString().includes('More') || data.toString().includes('MORE')) this.writeCommand(' ') @@ -209,7 +225,7 @@ export default class LineConnection { stationId, lineId: id, data: message, - commands: this.config.commands, + ports: this.config.ports, }) if (!this.config.inventory) { setTimeout(() => { @@ -235,6 +251,7 @@ export default class LineConnection { lineId: id, error: '\r\n' + err.message + '\r\n', }) + this.endTesting() resolve() }) @@ -256,6 +273,7 @@ export default class LineConnection { // } else { // this.retryConnect = 0 // } + this.endTesting() }) this.client.on('timeout', () => { @@ -324,7 +342,7 @@ export default class LineConnection { async runScript(script: Scenario, userName: string) { if (!this.client || this.client.destroyed) { console.log('Not connected') - this.isRunningScript = false + this.config.runningScenario = '' this.socketIO.emit('running_scenario', { stationId: this.config.stationId, lineId: this.config.id, @@ -333,7 +351,7 @@ export default class LineConnection { this.outputBuffer = '' return } - if (this.isRunningScript) { + if (this.config.runningScenario || this.config.runningPhysical) { console.log('Script already running') return } @@ -341,7 +359,7 @@ export default class LineConnection { console.log( `Run scenario "${script?.title}" to line ${this.config.lineNumber} of ${this.config.stationName}` ) - this.isRunningScript = true + this.config.runningScenario = '' this.socketIO.emit('running_scenario', { stationId: this.config.stationId, lineId: this.config.id, @@ -378,7 +396,7 @@ export default class LineConnection { return new Promise((resolve, reject) => { const timeoutTimer = setTimeout( () => { - this.isRunningScript = false + this.config.runningScenario = '' this.socketIO.emit('running_scenario', { stationId: this.config.stationId, lineId: this.config.id, @@ -425,7 +443,7 @@ export default class LineConnection { }, 5000) return } else clearTimeout(timeoutTimer) - this.isRunningScript = false + this.config.runningScenario = '' this.socketIO.emit('running_scenario', { stationId: this.config.stationId, lineId: this.config.id, @@ -936,4 +954,80 @@ export default class LineConnection { const note = `-------[ATC]-[${dataFormat}]-------\nLicense: ${licenses.join(', ')}\nSummary: ${data?.summary || ''}\nIssues:\n${data.issues?.length ? `- ` + data.issues.join(`\n- `) : ''}\n\n` await updateNoteToERP(sn, note) } + + async runPhysicalTest() { + if (this.config.runningPhysical) { + console.log('Running physical test') + return + } + this.config.runningPhysical = true + this.config.runningScenario = 'Physical Test' + this.socketIO.emit('running_scenario', { + stationId: this.config.stationId, + lineId: this.config.id, + title: 'Physical Test', + physical: true, + }) + const listPorts = await this.getPorts() + this.socketIO.emit('running_scenario', { + stationId: this.config.stationId, + lineId: this.config.id, + title: 'Physical Test', + physical: true, + ports: listPorts, + }) + if (listPorts.length === 0) { + console.log('End physical test') + this.endTesting() + return + } + + this.physicalTest.start(listPorts) + const interval = setInterval(async () => { + if (!this.physicalTest.done) { + const result = this.physicalTest.getResult() + // console.warn('⚠️ Missing ports:', result.missingPorts) + } else { + clearInterval(interval) + this.endTesting() + } + }, 10000) + } + + endTesting() { + this.physicalTest.done = true + this.config.runningPhysical = false + this.config.runningScenario = '' + this.outputBuffer = '' + this.outputScenario = '' + this.outputPhysicalTest = '' + this.config.ports = [] + this.socketIO.emit('running_scenario', { + stationId: this.config.stationId, + lineId: this.config.id, + title: '', + }) + } + + async getPorts(): Promise { + this.writeCommand(' show power inline\r\n') + this.writeCommand(' \r\n') + await this.sleep(3000) + const statusOutput = this.outputPhysicalTest + this.outputPhysicalTest = '' + + const lines = statusOutput.split('\n') + const ports = [] + for (const line of lines) { + // Match: "Gi0/1 is up, line protocol is up" + const match = line.match(/^(\S+)\s+\S+\s+(on|off)/i) + + if (match) { + const name = match[1] + ports.push(normalizeInterface(name)) + } + } + this.config.ports = [...new Set(ports)] + return [...new Set(ports)] + } } diff --git a/BACKEND/app/services/physical_test_service.ts b/BACKEND/app/services/physical_test_service.ts new file mode 100644 index 0000000..75a0c29 --- /dev/null +++ b/BACKEND/app/services/physical_test_service.ts @@ -0,0 +1,97 @@ +import { normalizeInterface } from '../ultils/helper.js' +import { PhysicalTestResult, PortState } from '../ultils/types.js' +const LINK_UPDOWN_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 + +export class PhysicalPortTest { + public ports = new Map() + private expectedPorts: string[] + public done = false + + constructor(expectedPorts: string[]) { + this.expectedPorts = expectedPorts + + expectedPorts.forEach((p) => { + this.ports.set(normalizeInterface(p), { + name: normalizeInterface(p), + tested: false, + }) + }) + } + + start(expectedPorts: string[]) { + this.ports.clear() + this.expectedPorts = expectedPorts + 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(line: string) { + const match = line.match(LINK_UPDOWN_REGEX) + if (!match) return + + const rawIface = match[1] + const state = match[2] as 'up' | 'down' + const iface = normalizeInterface(rawIface) + + const port = this.ports.get(iface) + if (!port) return + + // tránh update trùng state liên tiếp + if (port.lastState === state) return + + port.lastState = state + port.lastSeen = new Date() + + // chỉ cần UP 1 lần là pass + if (state === 'up' && !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() + } + + private checkDone() { + const testedCount = [...this.ports.values()].filter((p) => p.tested).length + + if (testedCount === this.expectedPorts.length) { + this.done = true + this.onDone() + } + } + + onDone() { + this.ports.clear() + 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', + } + } +} diff --git a/BACKEND/app/ultils/helper.ts b/BACKEND/app/ultils/helper.ts index 5473326..9aab582 100644 --- a/BACKEND/app/ultils/helper.ts +++ b/BACKEND/app/ultils/helper.ts @@ -667,3 +667,12 @@ export async function updateNoteToERP(sn: string, note: string) { console.log(error) } } + +export function normalizeInterface(name: string): string { + return name + .replace(/^Gi(?=\d)/, 'GigabitEthernet') + .replace(/^Fa(?=\d)/, 'FastEthernet') + .replace(/^Te(?=\d)/, 'TenGigabitEthernet') + .replace(/^Hu(?=\d)/, 'HundredGigE') + .replace(/^Eth(?=\d)/, 'Ethernet') +} diff --git a/BACKEND/app/ultils/types.ts b/BACKEND/app/ultils/types.ts index 734cd25..2483817 100644 --- a/BACKEND/app/ultils/types.ts +++ b/BACKEND/app/ultils/types.ts @@ -55,3 +55,17 @@ export interface ErrorRow { log: string count: number } + +export interface PortState { + name: string + tested: boolean + lastState?: 'up' | 'down' + lastSeen?: Date +} + +export interface PhysicalTestResult { + expected: number + tested: number + missingPorts: string[] + status: 'RUNNING' | 'DONE' | 'WARNING' +} diff --git a/BACKEND/providers/socket_io_provider.ts b/BACKEND/providers/socket_io_provider.ts index 0ea1d85..52f5398 100644 --- a/BACKEND/providers/socket_io_provider.ts +++ b/BACKEND/providers/socket_io_provider.ts @@ -133,7 +133,14 @@ export class WebSocketIo { setTimeout(() => { io.to(socket.id).emit( 'init', - Array.from(this.lineMap.values()).map((el) => el?.config || {}) + Array.from(this.lineMap.values()).map((el) => { + const config = el?.config || {} + if (config.status !== 'connected') { + config.runningScenario = '' + config.runningPhysical = false + } + return config + }) ) }, 500) @@ -606,6 +613,28 @@ export class WebSocketIo { {} ) }) + + socket.on('run_physical_test', async (data) => { + const { stationId, lineId } = data + await this.handleLineOperation( + io, + stationId, + [lineId], + async (lineCon) => lineCon.runPhysicalTest(), + {} + ) + }) + + socket.on('end_run_physical_test', async (data) => { + const { stationId, lineId } = data + await this.handleLineOperation( + io, + stationId, + [lineId], + async (lineCon) => lineCon.endTesting(), + {} + ) + }) }) socketServer.listen(SOCKET_IO_PORT, () => { @@ -645,8 +674,10 @@ export class WebSocketIo { userEmailOpenCLI: '', userOpenCLI: '', data: [], - commands: [], + ports: [], inventory: inventory, + runningPhysical: false, + runningScenario: '', }, socket, async () => { diff --git a/FRONTEND/src/App.tsx b/FRONTEND/src/App.tsx index d93885a..20f482f 100644 --- a/FRONTEND/src/App.tsx +++ b/FRONTEND/src/App.tsx @@ -383,7 +383,11 @@ function App() { setTimeout(() => { updateValueLineStation( data?.lineId, - { runningScenario: data?.title || "" }, + { + runningScenario: data?.title || "", + runningPhysical: data?.physical || false, + ports: data?.ports || [], + }, data?.stationId ); }, 100); diff --git a/FRONTEND/src/components/Modal/ModalTerminal.tsx b/FRONTEND/src/components/Modal/ModalTerminal.tsx index 6353068..3780569 100644 --- a/FRONTEND/src/components/Modal/ModalTerminal.tsx +++ b/FRONTEND/src/components/Modal/ModalTerminal.tsx @@ -78,6 +78,7 @@ const ModalTerminal = ({ const [isDisable, setIsDisable] = useState(false); const [isDisableTicket, setIsDisableTicket] = useState(false); const [listPorts, setListPorts] = useState([]); + const [listPortsPhysical, setListPortsPhysical] = useState([]); const [latestTicket, setLatestTicket] = useState(INIT_TICKET); const [dataTicket, setDataTicket] = useState(INIT_TICKET); const [valueBaud, setValueBaud] = useState(""); @@ -130,8 +131,14 @@ const ModalTerminal = ({ if (data?.ports && data?.ports.length > 0) setListPorts(data?.ports || []); }); + socket?.on("test_port_physical", (data) => { + if (data.stationId !== stationItem?.id) return; + if (data?.data && data?.data.length > 0) + setListPortsPhysical(data?.data || []); + }); return () => { socket?.off("switch_ports_status"); + socket?.off("test_port_physical"); }; }, [socket, stationItem]); @@ -696,7 +703,7 @@ const ModalTerminal = ({ copiedColor="violet" /> - + IOS: @@ -711,7 +718,7 @@ const ModalTerminal = ({ - + License: @@ -727,13 +734,13 @@ const ModalTerminal = ({ : ""} - + Sh env/module: {""} - + Mem/Flash: @@ -746,7 +753,7 @@ const ModalTerminal = ({ : ""} - + Warning from test report: AI @@ -762,6 +769,65 @@ const ModalTerminal = ({ /> + +
+ + + + List ports{" "} + {line?.ports?.length + ? `(${listPortsPhysical.length}/${line?.ports?.length})` + : ""} + + + + + + + {line?.ports?.map((port, i) => ( + + {port} + + ))} + + +
+
+