Update physical test

This commit is contained in:
nguyentrungthat 2025-12-26 16:09:52 +07:00
parent 2f484e19b6
commit b1b4f1b907
10 changed files with 247 additions and 68 deletions

2
BACKEND/.gitignore vendored
View File

@ -25,3 +25,5 @@ yarn-error.log
.DS_Store .DS_Store
storage/system_logs storage/system_logs
storage/ios
storage/license

View File

@ -0,0 +1,81 @@
import type { HttpContext } from '@adonisjs/core/http'
import fs from 'node:fs'
import path from 'node:path'
export default class IosLicenseController {
/* ================= LIST ================= */
async getIos() {
return fs.readdirSync('storage/ios')
}
async getLicense() {
return fs.readdirSync('storage/license')
}
/* ================= UPLOAD ================= */
async uploadIos({ request, response }: HttpContext) {
const file = request.file('file', {
size: '4gb',
extnames: ['bin', 'img', 'tar'],
})
if (!file) {
return response.badRequest('File is required')
}
await file.move('storage/ios', {
name: file.clientName,
overwrite: true,
})
return {
success: true,
filename: file.clientName,
}
}
async uploadLicense({ request, response }: HttpContext) {
const file = request.file('file', {
size: '100mb',
extnames: ['lic', 'txt'],
})
if (!file) {
return response.badRequest('File is required')
}
await file.move('storage/license', {
name: file.clientName,
overwrite: true,
})
return {
success: true,
filename: file.clientName,
}
}
/* ================= DOWNLOAD ================= */
async downloadIos({ params, response }: HttpContext) {
const filePath = path.join('"storage/ios"', params.filename)
if (!fs.existsSync(filePath)) {
return response.notFound('File not found')
}
return response.download(filePath)
}
async downloadLicense({ params, response }: HttpContext) {
const filePath = path.join('"storage/license"', params.filename)
if (!fs.existsSync(filePath)) {
return response.notFound('File not found')
}
return response.download(filePath)
}
}

View File

@ -24,7 +24,6 @@ import axios from 'axios'
import redis from '@adonisjs/redis/services/main' import redis from '@adonisjs/redis/services/main'
import Line from '#models/line' import Line from '#models/line'
import { ErrorRow, TestResult } from '../ultils/types.js' import { ErrorRow, TestResult } from '../ultils/types.js'
import moment from 'moment'
import momentTZ from 'moment-timezone' import momentTZ from 'moment-timezone'
import { PhysicalPortTest } from './physical_test_service.js' import { PhysicalPortTest } from './physical_test_service.js'
@ -359,7 +358,7 @@ export default class LineConnection {
console.log( console.log(
`Run scenario "${script?.title}" to line ${this.config.lineNumber} of ${this.config.stationName}` `Run scenario "${script?.title}" to line ${this.config.lineNumber} of ${this.config.stationName}`
) )
this.config.runningScenario = '' this.config.runningScenario = script?.title
this.socketIO.emit('running_scenario', { this.socketIO.emit('running_scenario', {
stationId: this.config.stationId, stationId: this.config.stationId,
lineId: this.config.id, lineId: this.config.id,
@ -473,6 +472,12 @@ export default class LineConnection {
timestamp: Date.now(), timestamp: Date.now(),
}) })
} }
if (['show version', 'sh version', 'show ver', 'sh ver'].includes(item.command)) {
const dataVer = JSON.parse(item.textfsm)[0]
this.config.inventory = this.config.inventory
? { ...this.config.inventory, ...dataVer }
: dataVer
}
item.textfsm = JSON.parse(item.textfsm) item.textfsm = JSON.parse(item.textfsm)
} }
}) })
@ -646,6 +651,8 @@ export default class LineConnection {
const start = Date.now() const start = Date.now()
// console.log('[EXPECT]', expect, timeout) // console.log('[EXPECT]', expect, timeout)
while (Date.now() - start < timeout) { while (Date.now() - start < timeout) {
console.log(expect)
console.log(this.outputBuffer)
if (this.outputBuffer.includes(expect)) { if (this.outputBuffer.includes(expect)) {
this.outputBuffer = '' this.outputBuffer = ''
return true return true
@ -662,7 +669,15 @@ export default class LineConnection {
if (item?.textfsm && isValidJson(item?.textfsm)) { if (item?.textfsm && isValidJson(item?.textfsm)) {
if (['show inventory', 'sh inventory', 'show inv', 'sh inv'].includes(item.command)) { if (['show inventory', 'sh inventory', 'show inv', 'sh inv'].includes(item.command)) {
const dataInventory = JSON.parse(item.textfsm)[0] const dataInventory = JSON.parse(item.textfsm)[0]
this.config.inventory = dataInventory this.config.inventory = this.config.inventory
? { ...this.config.inventory, ...dataInventory }
: dataInventory
}
if (['show version', 'sh version', 'show ver', 'sh ver'].includes(item.command)) {
const dataVer = JSON.parse(item.textfsm)[0]
this.config.inventory = this.config.inventory
? { ...this.config.inventory, ...dataVer }
: dataVer
} }
item.textfsm = JSON.parse(item.textfsm) item.textfsm = JSON.parse(item.textfsm)
} }
@ -855,15 +870,13 @@ export default class LineConnection {
// console.log(detectLog) // console.log(detectLog)
const tableHTML = this.buildEmailContent(result) const tableHTML = this.buildEmailContent(result)
await sendMessageToMail( await sendMessageToMail(
'andrew.ng@apactech.io',
`[ATC] - [${this.config.stationName} - Line: ${this.config.lineNumber}] - Raw log issue`, `[ATC] - [${this.config.stationName} - Line: ${this.config.lineNumber}] - Raw log issue`,
tableHTML + tableHTML +
`${` `${`
<hr /> <hr />
<p>Logs:</p> <p>Logs:</p>
<div style="white-space: break-spaces; background-color: #f5f5f5; color: black; padding: 8px; max-height: 500px; overflow-y: scroll; border: 1px #ccc solid;"><span style="color: black;"> <div style="white-space: break-spaces; background-color: #f5f5f5; color: black; padding: 8px; max-height: 500px; overflow-y: scroll; border: 1px #ccc solid;"><span style="color: black;">
${this.bufferLog.allBuffer}</span></div>`}`, ${this.bufferLog.allBuffer}</span></div>`}`
['ips@ipsupply.com.au', 'kay@ipsupply.com.au', 'joseph@apactech.io']
) )
this.session.clear() this.session.clear()
this.bufferLog.clear() this.bufferLog.clear()
@ -962,12 +975,6 @@ export default class LineConnection {
} }
this.config.runningPhysical = true this.config.runningPhysical = true
this.config.runningScenario = 'Physical Test' 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() const listPorts = await this.getPorts()
this.socketIO.emit('running_scenario', { this.socketIO.emit('running_scenario', {
stationId: this.config.stationId, stationId: this.config.stationId,
@ -982,16 +989,17 @@ export default class LineConnection {
return return
} }
this.physicalTest.start(listPorts) this.physicalTest.start(listPorts, this.config.inventory)
const interval = setInterval(async () => { // const interval = setInterval(async () => {
if (!this.physicalTest.done) { // if (!this.physicalTest.done) {
const result = this.physicalTest.getResult() // // const result = this.physicalTest.getResult()
// console.warn('⚠️ Missing ports:', result.missingPorts) // // console.warn('⚠️ Missing ports:', result.missingPorts)
} else { // } else {
clearInterval(interval) // clearInterval(interval)
this.endTesting() // await this.sendReportPhysicalTest()
} // this.endTesting()
}, 10000) // }
// }, 10000)
} }
endTesting() { endTesting() {
@ -1030,4 +1038,12 @@ export default class LineConnection {
this.config.ports = [...new Set(ports)] this.config.ports = [...new Set(ports)]
return [...new Set(ports)] return [...new Set(ports)]
} }
async sendReportPhysicalTest() {
const formReport = this.physicalTest.getFormReport()
await sendMessageToMail(
`[ATC] - [${this.config.stationName} - Line: ${this.config.lineNumber}] - Physical Port Test`,
formReport
)
}
} }

View File

@ -1,5 +1,6 @@
import moment from 'moment'
import { normalizeInterface } from '../ultils/helper.js' import { normalizeInterface } from '../ultils/helper.js'
import { PhysicalTestResult, PortState } from '../ultils/types.js' import { PhysicalTestReport, PhysicalTestResult, PortState } from '../ultils/types.js'
const LINK_UPDOWN_REGEX = 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 /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
@ -7,9 +8,13 @@ export class PhysicalPortTest {
public ports = new Map<string, PortState>() public ports = new Map<string, PortState>()
private expectedPorts: string[] private expectedPorts: string[]
public done = false public done = false
private startTime: Date
public inventory: any
constructor(expectedPorts: string[]) { constructor(expectedPorts: string[]) {
this.expectedPorts = expectedPorts this.expectedPorts = expectedPorts
this.startTime = new Date()
this.inventory = ''
expectedPorts.forEach((p) => { expectedPorts.forEach((p) => {
this.ports.set(normalizeInterface(p), { this.ports.set(normalizeInterface(p), {
@ -19,9 +24,11 @@ export class PhysicalPortTest {
}) })
} }
start(expectedPorts: string[]) { start(expectedPorts: string[], inventory: any) {
this.ports.clear() this.ports.clear()
this.startTime = new Date()
this.expectedPorts = expectedPorts this.expectedPorts = expectedPorts
this.inventory = inventory
this.done = false this.done = false
expectedPorts.forEach((p) => { expectedPorts.forEach((p) => {
this.ports.set(normalizeInterface(p), { this.ports.set(normalizeInterface(p), {
@ -79,10 +86,26 @@ export class PhysicalPortTest {
} }
onDone() { onDone() {
this.ports.clear() this.getFormReport()
// this.ports.clear()
console.log('✅ Physical Test DONE') 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 { getResult(): PhysicalTestResult {
const tested = [...this.ports.values()].filter((p) => p.tested) const tested = [...this.ports.values()].filter((p) => p.tested)
const missing = [...this.ports.values()].filter((p) => !p.tested).map((p) => p.name) const missing = [...this.ports.values()].filter((p) => !p.tested).map((p) => p.name)
@ -94,4 +117,46 @@ export class PhysicalPortTest {
status: this.done ? 'DONE' : 'RUNNING', 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 `
Physical Port Test Report<br/>
<br/>
Model : <b>${report.device.model ?? 'N/A'}</b><br/>
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/>
Status : ${status === 'PASS' ? '✅ PASS' : '⚠️ WARNING'}<br/>
<br/>
<br/>
<b>Test Summary</b><br/>
<br/>
Total Ports : ${report.ports.length}<br/>
Ports Tested (UP) : ${tested.length}<br/>
Ports Missing : ${missing.length}<br/>
<br/>
<br/>
<b>Passed Ports</b><br/>
<br/>
${tested.map((p) => p.name).join('<br/>')}<br/>
<br/>
${
missing.length
? `
<br/>
<b>Missing Ports</b><br/>
<br/>
${missing.map((p) => p.name).join('<br/>')}
`
: ''
}<br/>
<br/>
`.trim()
}
} }

View File

@ -7,6 +7,10 @@ import { ErrorRow, LogRule, ParsedLog, TestError, TestResult } from './types.js'
import axios from 'axios' import axios from 'axios'
import moment from 'moment' 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 = ''
type DetectAI = { type DetectAI = {
status: string[] status: string[]
issue: string[] issue: string[]
@ -221,12 +225,7 @@ export function mapToLineFormat(input: InputData) {
} }
} }
export function sendMessageToMail( export function sendMessageToMail(subject: string, text: string): Promise<SendMailResponse> {
email: string,
subject: string,
text: string,
cc?: string[]
): Promise<SendMailResponse> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const transporter = nodeMailer.createTransport({ const transporter = nodeMailer.createTransport({
pool: true, pool: true,
@ -241,10 +240,10 @@ export function sendMessageToMail(
const mailOptions = { const mailOptions = {
from: process.env.SMTP_USERNAME, from: process.env.SMTP_USERNAME,
to: email, to: mailTo,
subject, subject,
html: text, html: text,
cc: cc, cc: mailCC,
} }
transporter.sendMail(mailOptions, (error: any, info: any) => { transporter.sendMail(mailOptions, (error: any, info: any) => {

View File

@ -69,3 +69,14 @@ export interface PhysicalTestResult {
missingPorts: string[] missingPorts: string[]
status: 'RUNNING' | 'DONE' | 'WARNING' status: 'RUNNING' | 'DONE' | 'WARNING'
} }
export interface PhysicalTestReport {
device: {
model?: string
serial?: string
}
startTime: Date
endTime: Date
durationMs: number
ports: PortState[]
}

View File

@ -586,10 +586,8 @@ export class WebSocketIo {
titleAuto: `[${scenarioName || 'DPELP'}] - ${stationName} - ` + dataFormat, titleAuto: `[${scenarioName || 'DPELP'}] - ${stationName} - ` + dataFormat,
}) })
await sendMessageToMail( await sendMessageToMail(
'andrew.ng@apactech.io',
`[${scenarioName || 'DPELP'}] - ${stationName} - ${dataFormat}`, `[${scenarioName || 'DPELP'}] - ${stationName} - ${dataFormat}`,
tableHTML, tableHTML
['ips@ipsupply.com.au', 'kay@ipsupply.com.au', 'joseph@apactech.io']
) )
await sendMessageToZulip( await sendMessageToZulip(
'stream', 'stream',
@ -631,7 +629,10 @@ export class WebSocketIo {
io, io,
stationId, stationId,
[lineId], [lineId],
async (lineCon) => lineCon.endTesting(), async (lineCon) => {
lineCon.endTesting()
await lineCon.sendReportPhysicalTest()
},
{} {}
) )
}) })
@ -895,7 +896,13 @@ export class WebSocketIo {
this.lineMap.forEach((line, id) => { this.lineMap.forEach((line, id) => {
if (line && line.config) { if (line && line.config) {
newMap.set(id, { newMap.set(id, {
config: { ...line.config, status: 'disconnected' }, config: {
...line.config,
status: 'disconnected',
userEmailOpenCLI: '',
userOpenCLI: '',
openCLI: false,
},
} as LineConnection) } as LineConnection)
} }
}) })

View File

@ -105,3 +105,15 @@ router
router.get('/', '#controllers/healcheck_controller.check') router.get('/', '#controllers/healcheck_controller.check')
}) })
.prefix('atc/health-check') .prefix('atc/health-check')
router
.group(() => {
router.get('/ios', '#controllers/ios_license_controller.getIos')
router.post('/ios/upload', '#controllers/ios_license_controller.uploadIos')
router.get('/ios/download/:filename', '#controllers/ios_license_controller.downloadIos')
router.get('/license', '#controllers/ios_license_controller.getLicense')
router.post('/license/upload', '#controllers/ios_license_controller.uploadLicense')
router.get('/license/download/:filename', '#controllers/ios_license_controller.downloadLicense')
})
.prefix('/api')

View File

@ -428,17 +428,15 @@ function App() {
lines: station.lines.map((line) => { lines: station.lines.map((line) => {
const buffered = lineBuffersRef.current.get(line.id || 0); const buffered = lineBuffersRef.current.get(line.id || 0);
if (!buffered) return line; // không có update if (!buffered) return line; // không có update
updateValueSelectedLine(line?.id || 0, { const data = {
netOutput: buffered,
loadingClearTerminal: false,
});
return {
...line, ...line,
netOutput: (line.netOutput || "") + buffered, netOutput: (line.netOutput || "") + buffered,
output: buffered, output: buffered,
loadingOutput: line.loadingOutput ? false : true, loadingOutput: line.loadingOutput ? false : true,
loadingClearTerminal: false, loadingClearTerminal: false,
}; };
updateValueSelectedLine(line?.id || 0, data);
return data;
}), }),
})) }))
); );
@ -507,28 +505,16 @@ function App() {
[] []
); );
const updateValueSelectedLine = useCallback( const updateValueSelectedLine = (lineId: number, updates: Partial<TLine>) => {
(lineId: number, updates: Partial<TLine>) => { // Update selectedLine nếu nó đang được chọn
// Update selectedLine nếu nó đang được chọn setSelectedLine((prevSelected) => {
setSelectedLine((prevSelected) => { if (!prevSelected || prevSelected.id !== lineId) return prevSelected;
if (!prevSelected || prevSelected.id !== lineId) return prevSelected; return {
...prevSelected,
const isNetOutput = typeof updates?.netOutput !== "undefined"; ...updates,
};
return { });
...prevSelected, };
...updates,
...(isNetOutput && {
netOutput:
(prevSelected.netOutput || "") + (updates.netOutput || ""),
output: updates.netOutput,
loadingOutput: prevSelected.loadingOutput ? false : true,
}),
};
});
},
[]
);
// const getLine = (lineId: number, stationId: number) => { // const getLine = (lineId: number, stationId: number) => {
// const station = stations?.find((sta) => sta.id === stationId); // const station = stations?.find((sta) => sta.id === stationId);

View File

@ -432,7 +432,7 @@ const ModalTerminal = ({
); );
return showVersion?.textfsm && showVersion?.textfsm?.[0] return showVersion?.textfsm && showVersion?.textfsm?.[0]
? showVersion?.textfsm?.[0] ? showVersion?.textfsm?.[0]
: null; : line?.inventory;
}; };
const findDataShowLicense = () => { const findDataShowLicense = () => {
@ -746,9 +746,9 @@ const ModalTerminal = ({
</Text> </Text>
<Text size="md"> <Text size="md">
{findDataShowVersion() {findDataShowVersion()
? findDataShowVersion()?.MEMORY + ? (findDataShowVersion()?.MEMORY || "") +
(findDataShowVersion()?.USB_FLASH (findDataShowVersion()?.USB_FLASH
? " - " + findDataShowVersion()?.USB_FLASH ? " - " + (findDataShowVersion()?.USB_FLASH || "")
: "") : "")
: ""} : ""}
</Text> </Text>