Update physical test, load ios

This commit is contained in:
nguyentrungthat 2026-01-07 16:04:24 +07:00
parent 07cfde8c15
commit 28059f85ed
8 changed files with 201 additions and 63 deletions

View File

@ -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,9 +73,10 @@ 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}'`
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',
},
],

View File

@ -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 {
</td>
<td style="padding:6px; text-align:center;">${r.rule}</td>
<td style="padding:6px; text-align:center;">${r.message}</td>
<td style="padding:6px; font-family:monospace;">
<div style="white-space: break-spaces;"><span style="color: black;">
${escapeHtml(r.log.trim())}</span></div>
</td>
<td style="padding:6px; font-family:monospace;">*${escapeHtml(r.log.trim())
.split('*')
.filter((el) => el)
.join('<br/>*')}</td>
</tr>
`
)
@ -939,26 +942,10 @@ export default class LineConnection {
`
}
renderAIDetectTable(row: any): string {
return `
<table border="1" cellpadding="6" style="border-collapse: collapse; width:100%; margin-bottom: 15px;">
<tr>
<th style="padding:6px;">Summary</th>
<th style="padding:6px;">Issues</th>
</tr>
<tr>
<td style="text-wrap: wrap;">${row.summary || ''}</td>
<td style="width:1000px; text-wrap: wrap;">${row.issues?.length ? `- ` + row.issues.join(`<br>- `) : '- No issues detected.'}</td>
</tr>
</table>
`
}
buildEmailContent(result: TestResult): string {
const rows = mapErrorsToRows(result.errors)
const table = this.renderErrorTable(rows)
// const tableAI = this.renderAIDetectTable(value)
console.log(table)
return `
<h3>Cisco Device Log Result</h3>
<p>Line: <b>${this.config.lineNumber}</b> - Station: <b>${this.config.stationName}</b></p>
@ -1034,7 +1021,7 @@ export default class LineConnection {
async getPorts(): Promise<string[]> {
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)
}
}

View File

@ -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<string, PortState>()
@ -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 : <b>${report.device.serial ?? 'N/A'}</b><br/>
Started At : ${moment(report.startTime).format('YYYY/MM/DD, HH:mm:ss')}<br/>
Finished At : ${moment(report.endTime).format('YYYY/MM/DD, HH:mm:ss')}<br/>
Duration : ${Math.floor(report.durationMs / 1000)} sec<br/>
Duration : ${this.formatDuration(report.durationMs)}<br/>
Status : ${status === 'PASS' ? '✅ PASS' : '⚠️ WARNING'}<br/>
<br/>
<br/>
@ -169,4 +192,12 @@ export class PhysicalPortTest {
<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`
}
}

View File

@ -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[]

View File

@ -61,6 +61,8 @@ export interface PortState {
tested: boolean
lastState?: 'up' | 'down'
lastSeen?: Date
poeGranted?: boolean
poeDisconnected?: boolean
}
export interface PhysicalTestResult {

View File

@ -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)
},
{}
)

View File

@ -153,18 +153,16 @@ const ModalSelectIOS = ({
size="sm"
leftSection={<IconPlayerPlay size={16} />}
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();
}}
>

View File

@ -1032,7 +1032,7 @@ const ModalTerminal = ({
setIsDisable(true);
setTimeout(() => {
setIsDisable(false);
}, 4000);
}, 10000);
}}
>
Start Physical Test