diff --git a/BACKEND/app/controllers/healcheck_controller.ts b/BACKEND/app/controllers/healcheck_controller.ts index 14a4ac6..77241d0 100644 --- a/BACKEND/app/controllers/healcheck_controller.ts +++ b/BACKEND/app/controllers/healcheck_controller.ts @@ -54,7 +54,9 @@ export default class HealCheckController { serialNumberA: dataSN?.serialNumberA, productModelId: dataSN?.productModelId, orgId: dataSN?.orgId, + condition: dataSN?.condition, testNotes: dataSN?.testNotes, + healthCheck: true, }, }, { @@ -71,10 +73,11 @@ export default class HealCheckController { }, { ...dataCheckNote, - status: resSN.data?.error ? false : true, - message: resSN.data?.error - ? `Checking api update note SN false: '${resSN.data?.error?.message}'` - : 'Checking api update note SN success', + status: resSN?.data?.Status === 'ERROR' ? false : true, + message: + resSN?.data?.Status === 'ERROR' + ? `Checking api update note SN false: '${resSN.data?.Msg}'` + : 'Checking api update note SN success', }, ], } diff --git a/BACKEND/app/services/line_connection.ts b/BACKEND/app/services/line_connection.ts index 219d473..c526f96 100644 --- a/BACKEND/app/services/line_connection.ts +++ b/BACKEND/app/services/line_connection.ts @@ -19,7 +19,7 @@ import { updateNoteToERP, } from '../ultils/helper.js' import Scenario from '#models/scenario' -import path from 'node:path' +import path, { join } from 'node:path' import axios from 'axios' import redis from '@adonisjs/redis/services/main' import Line from '#models/line' @@ -27,6 +27,7 @@ import { ErrorRow, TestResult } from '../ultils/types.js' import momentTZ from 'moment-timezone' import { PhysicalPortTest } from './physical_test_service.js' import Station from '#models/station' +import IosLicenseController from '#controllers/ios_license_controller' type Inventory = { pid: string @@ -126,6 +127,7 @@ export default class LineConnection { private session: TestSession public physicalTest: PhysicalPortTest private outputPhysicalTest: string + private listDeviceIos: string[] constructor(config: LineConfig, socketIO: any, handleClearLine: () => void) { this.config = config @@ -153,6 +155,7 @@ export default class LineConnection { this.handleClearLine = handleClearLine this.physicalTest = new PhysicalPortTest([]) this.outputPhysicalTest = '' + this.listDeviceIos = [] } connect(timeoutMs = 5000) { @@ -922,10 +925,10 @@ export default class LineConnection { ${r.rule} ${r.message} - -
- ${escapeHtml(r.log.trim())}
- + *${escapeHtml(r.log.trim()) + .split('*') + .filter((el) => el) + .join('
*')} ` ) @@ -939,26 +942,10 @@ export default class LineConnection { ` } - renderAIDetectTable(row: any): string { - return ` - - - - - - - - - -
SummaryIssues
${row.summary || ''}${row.issues?.length ? `- ` + row.issues.join(`
- `) : '- No issues detected.'}
- ` - } - buildEmailContent(result: TestResult): string { const rows = mapErrorsToRows(result.errors) const table = this.renderErrorTable(rows) - // const tableAI = this.renderAIDetectTable(value) - + console.log(table) return `

Cisco Device Log Result

Line: ${this.config.lineNumber} - Station: ${this.config.stationName}

@@ -1034,7 +1021,7 @@ export default class LineConnection { async getPorts(): Promise { this.writeCommand(' show power inline\r\n') this.writeCommand(' \r\n') - await this.sleep(3000) + await this.sleep(5000) const statusOutput = this.outputPhysicalTest this.outputPhysicalTest = '' @@ -1107,14 +1094,14 @@ export default class LineConnection { }, { expect: 'rommon', - send: `tftpdnld`, + send: this.listDeviceIos?.includes(nameIos) ? '' : `tftpdnld`, delay: '1', repeat: '1', note: '', }, { - expect: 'y/n', - send: `y`, + expect: this.listDeviceIos?.includes(nameIos) ? '' : 'y/n', + send: this.listDeviceIos?.includes(nameIos) ? '' : `y`, delay: '2', repeat: '1', note: '', @@ -1200,9 +1187,10 @@ export default class LineConnection { timeout: 1000, body: JSON.stringify(body), } + await sleep(5000) await this.runScript(script as any, userName) - await this.endEmailLoadIos(nameIos, startTime) + await this.sendEmailLoadIos(nameIos, startTime) } async loadIosSwitch(nameIos: string, userName: string) { @@ -1213,6 +1201,8 @@ export default class LineConnection { const [a, b] = network.split('.').map(Number) const timeZone = process.env.TIME_ZONE || 'Australia/Sydney' const startTime = momentTZ().tz(timeZone).format('YYYY/MM/DD, HH:mm:ss') + await this.backupIos(nameIos) + const body = [ { expect: '', @@ -1286,21 +1276,21 @@ export default class LineConnection { }, { expect: '#', - send: `copy tftp: flash:`, + send: this.listDeviceIos?.includes(nameIos) ? '' : `copy tftp: flash:`, delay: '1', repeat: '1', note: '', }, { expect: '', - send: `${tftpIp}`, + send: this.listDeviceIos?.includes(nameIos) ? '' : `${tftpIp}`, delay: '1', repeat: '1', note: '', }, { expect: '', - send: `ios/${nameIos}`, + send: this.listDeviceIos?.includes(nameIos) ? '' : `ios/${nameIos}`, delay: '1', repeat: '1', note: '', @@ -1409,10 +1399,10 @@ export default class LineConnection { } await this.runScript(script as any, userName) - await this.endEmailLoadIos(nameIos, startTime) + await this.sendEmailLoadIos(nameIos, startTime) } - async endEmailLoadIos(nameIos: string, startTime: string) { + async sendEmailLoadIos(nameIos: string, startTime: string) { const timeZone = process.env.TIME_ZONE || 'Australia/Sydney' const dataFormat = momentTZ().tz(timeZone).format('YYYY/MM/DD, HH:mm:ss') const body = ` @@ -1431,4 +1421,100 @@ export default class LineConnection { body ) } + + async checkDeviceFlash() { + this.writeCommand(' enable\r\n') + this.writeCommand('show flash:\r\n') + await sleep(2000) + const ios = [] + const binRegex = /^\s*\d+\s+-rwx\s+\d+\s+.*?\s+([^\s]+\.bin)\s*$/gim + + let match + while ((match = binRegex.exec(this.outputBuffer)) !== null) { + ios.push(match[1]) + } + return ios + } + + async deleteFileOnFlash(fileName: string) { + await this.writeCommand(`delete flash:${fileName}\r\n`) + await this.writeCommand(`\r\n`) + await this.writeCommand(`\r\n`) + await sleep(3000) + } + + async uploadFileToServerTFTP(fileName: string, server: string) { + this.config.runningScenario = 'Upload file' + await this.writeCommand(`copy flash: tftp:\r\n`) + await this.writeCommand(`${fileName}\r\n`) + await this.writeCommand(`${server}\r\n`) + await this.writeCommand(`ios/${fileName}\r\n`) + await sleep(5000) + while (true) { + if (this.outputBuffer.includes('#')) { + this.outputBuffer = '' + this.config.runningScenario = '' + return true + } + await sleep(5000) + } + } + + // function get list ios + async getListIos() { + try { + const controller = new IosLicenseController() + const listIos = await controller.getIos() + return listIos + } catch (error) { + console.log('Error get ios', error) + return [] + } + } + + async getCurrentBootIos() { + this.writeCommand('show version | include System image\r\n') + await sleep(2000) + + const match = this.outputBuffer.match(/"flash:(.+?)"/i) + this.outputBuffer = '' + + return match ? match[1] : null + } + + async backupIos(nameIos: string) { + const station = await Station.find(this.config.stationId) + if (!station) return + const server = station?.tftp_ip || '172.16.7.69' + // const currentBootIos = await this.getCurrentBootIos() + this.config.runningScenario = 'Backup IOS' + this.socketIO.emit('running_scenario', { + stationId: this.config.stationId, + lineId: this.config.id, + title: 'Backup IOS', + }) + await sleep(1000) + const listIos = await this.getListIos() + const dataDevice = await this.checkDeviceFlash() + this.listDeviceIos = [...dataDevice] + console.log('Data Device Flash', dataDevice) + if (dataDevice && Array.isArray(dataDevice)) { + for (const ios of dataDevice) { + // if (ios === nameIos) { + // console.log(`SKIP active IOS: ${ios}`) + // continue + // } + if (listIos?.includes(ios)) { + console.log(`Already backed up: ${ios}`) + if (ios !== nameIos) await this.deleteFileOnFlash(ios) + } else { + const ok = await this.uploadFileToServerTFTP(ios, server) + if (ok && ios !== nameIos) await this.deleteFileOnFlash(ios) + } + } + } + this.outputBuffer = '' + this.config.runningScenario = '' + await sleep(1000) + } } diff --git a/BACKEND/app/services/physical_test_service.ts b/BACKEND/app/services/physical_test_service.ts index e00a94a..7c90d86 100644 --- a/BACKEND/app/services/physical_test_service.ts +++ b/BACKEND/app/services/physical_test_service.ts @@ -1,8 +1,11 @@ import moment from 'moment' import { normalizeInterface } from '../ultils/helper.js' import { PhysicalTestReport, PhysicalTestResult, PortState } from '../ultils/types.js' -const LINK_UPDOWN_REGEX = +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() @@ -44,24 +47,44 @@ export class PhysicalPortTest { } handleLog(line: string) { - const match = line.match(LINK_UPDOWN_REGEX) - if (!match) return + let iface: string | null = null + let markTested = false + let state: 'up' | 'down' | undefined - const rawIface = match[1] - const state = match[2] as 'up' | 'down' - const iface = normalizeInterface(rawIface) + // 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) return 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) { + if (state && port.lastState === state) return + 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() } @@ -141,7 +164,7 @@ export class PhysicalPortTest { Serial Number : ${report.device.serial ?? 'N/A'}
Started At : ${moment(report.startTime).format('YYYY/MM/DD, HH:mm:ss')}
Finished At : ${moment(report.endTime).format('YYYY/MM/DD, HH:mm:ss')}
- Duration : ${Math.floor(report.durationMs / 1000)} sec
+ Duration : ${this.formatDuration(report.durationMs)}
Status : ${status === 'PASS' ? '✅ PASS' : '⚠️ WARNING'}

────────────────────────────────
@@ -169,4 +192,12 @@ export class PhysicalPortTest {
`.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` + } } diff --git a/BACKEND/app/ultils/helper.ts b/BACKEND/app/ultils/helper.ts index 7c8ce81..2e2ac56 100644 --- a/BACKEND/app/ultils/helper.ts +++ b/BACKEND/app/ultils/helper.ts @@ -8,8 +8,8 @@ import axios from 'axios' import moment from 'moment' const mailTo = 'andrew.ng@apactech.io' -const mailCC = ['ips@ipsupply.com.au', 'kay@ipsupply.com.au', 'joseph@apactech.io'] -// const mailCC = '' +// const mailCC = ['ips@ipsupply.com.au', 'kay@ipsupply.com.au', 'joseph@apactech.io'] +const mailCC = '' type DetectAI = { status: string[] diff --git a/BACKEND/app/ultils/types.ts b/BACKEND/app/ultils/types.ts index 2d2c664..d25ebe5 100644 --- a/BACKEND/app/ultils/types.ts +++ b/BACKEND/app/ultils/types.ts @@ -61,6 +61,8 @@ export interface PortState { tested: boolean lastState?: 'up' | 'down' lastSeen?: Date + poeGranted?: boolean + poeDisconnected?: boolean } export interface PhysicalTestResult { diff --git a/BACKEND/providers/socket_io_provider.ts b/BACKEND/providers/socket_io_provider.ts index 45a4be1..716933f 100644 --- a/BACKEND/providers/socket_io_provider.ts +++ b/BACKEND/providers/socket_io_provider.ts @@ -663,13 +663,31 @@ export class WebSocketIo { }) socket.on('load_ios_router', async (data) => { - const { stationId, lineId, iosName } = data + const { stationId, lineId, iosName, outletNumber, station, apcName, isReboot } = data await this.handleLineOperation( io, stationId, [lineId], async (lineCon) => { - lineCon.loadIosRouter(iosName, userName) + await lineCon.backupIos(iosName) + if (isReboot) { + if (!outletNumber || outletNumber < 0) return + if (!station) return + const apcIp = (station as any)[`${apcName}_ip`] as string + if (!this.apcsControl.get(apcIp)) await this.connectApc(io, apcName, station) + const apc = this.apcsControl.get(apcIp) + if (apc && apc.status !== 'CONNECTED') { + await apc.reconnect() + this.keepConnectAPC(apcIp, io) + } + if (apc) { + await apc?.restartOutlet(outletNumber) + setTimeout(() => { + apc?.navigateToOutlets() + }, 10000) + } + } + await lineCon.loadIosRouter(iosName, userName) }, {} ) diff --git a/FRONTEND/src/components/Modal/ModalSelectIOS.tsx b/FRONTEND/src/components/Modal/ModalSelectIOS.tsx index 3d7351e..622cefb 100644 --- a/FRONTEND/src/components/Modal/ModalSelectIOS.tsx +++ b/FRONTEND/src/components/Modal/ModalSelectIOS.tsx @@ -153,18 +153,16 @@ const ModalSelectIOS = ({ size="sm" leftSection={} onClick={() => { - if (isReboot) - socket?.emit("control_apc", { - outletNumbers: [line?.outlet], - station: { ...station, lines: [] }, - action: "restart", - apcName: line?.apc_name || line?.apcName, - }); - socket?.emit("load_ios_router", { + const payload = { stationId: Number(station?.id), lineId: Number(line?.id), iosName: ios, - }); + station: station, + outletNumber: line?.outlet || -1, + apcName: line?.apcName || line?.apc_name, + isReboot: isReboot, + }; + socket?.emit("load_ios_router", payload); close(); }} > diff --git a/FRONTEND/src/components/Modal/ModalTerminal.tsx b/FRONTEND/src/components/Modal/ModalTerminal.tsx index 77f5660..b7be0f5 100644 --- a/FRONTEND/src/components/Modal/ModalTerminal.tsx +++ b/FRONTEND/src/components/Modal/ModalTerminal.tsx @@ -1032,7 +1032,7 @@ const ModalTerminal = ({ setIsDisable(true); setTimeout(() => { setIsDisable(false); - }, 4000); + }, 10000); }} > Start Physical Test