ATC_SIMPLE/BACKEND/app/services/line_connection.ts

2067 lines
66 KiB
TypeScript

import fs from 'node:fs'
import { textfsmResults } from './../ultils/templates/index.js'
import net from 'node:net'
import {
appendLog,
buildBody,
canInputCommand,
classifyLog,
cleanData,
convertFromKilobytesString,
detectConfigRamByModel,
detectScenarioByModel,
escapeHtml,
isRamSufficient,
isValidJson,
LogStreamBuffer,
mapErrorsToRows,
mapToLineFormat,
normalizeInterface,
parseLicenseReport,
sendMessageToMail,
sleep,
TestSession,
updateNoteToERP,
} from '../ultils/helper.js'
import Scenario from '#models/scenario'
import path from 'node:path'
import axios from 'axios'
import redis from '@adonisjs/redis/services/main'
import Line from '#models/line'
import { CustomSocket, 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
vid: string
sn: string
licenseLevel: string
licenseType: string
nextLicenseLevel: string
}
interface LineConfig {
id: number
port: number
lineNumber: number
ip: string
stationId: number
stationName: string
stationIp: string
apcName?: string
outlet: number
output: string
status: string
baud: number
openCLI: boolean
userEmailOpenCLI: string
userOpenCLI: string
inventory: any
latestScenario?: {
name: string
time: number
detectAI?: {
status: string[]
issue: string[]
summary: string
}
}
data: {
command: string
output: string
textfsm: string
}[]
ports: string[]
runningScenario: string
runningPhysical: boolean
listFeatureTested: string[]
isReady: boolean
isSkipPhysical?: boolean
reasonSkipPhysical?: string
// history: string
}
/** HISTORY
* PID
* SN
* VID
* Timestamp
* Scenario
*/
interface HistoryItem {
pid: string
vid: string
sn: string
scenario: string
id: number
number: number
stationId: number
timestamp?: number
}
interface User {
userEmail: string
userName: string
}
interface DataDPELP {
line: number
pid: any
vid: any
sn: any
ios: string
mac: string
license: any
issues: string[]
summary: string
}
export default class LineConnection {
public client: net.Socket
public config: LineConfig
public readonly socketIO: any
private outputBuffer: string
private connecting: boolean
private waitingScenario: boolean
private outputInventory: string
private outputScenario: string
private bufferLog: LogStreamBuffer
public dataDPELP: DataDPELP | string
private listScenarios: number[]
public handleClearLine: () => void
private session: TestSession
public physicalTest: PhysicalPortTest
private outputPhysicalTest: string
private outputLoadIosLicense: string | boolean
private listDeviceIos: string[]
private debounceTimer: NodeJS.Timeout | null = null
private testingPortPoE: boolean
private outputTestingPortPoE: string
private debounceSendSummaryReport: NodeJS.Timeout | null = null
private isPingToServer: boolean
private outputPingToServer: string
constructor(config: LineConfig, socketIO: any, handleClearLine: () => void) {
this.config = config
this.socketIO = socketIO
this.client = new net.Socket()
this.outputBuffer = ''
this.connecting = false
this.waitingScenario = false
this.outputInventory = ''
this.outputScenario = ''
this.bufferLog = new LogStreamBuffer()
this.dataDPELP = {
line: this.config.lineNumber,
pid: '',
vid: '',
sn: '',
ios: '',
mac: '',
license: [],
issues: ['No data'],
summary: '',
}
this.listScenarios = []
this.session = new TestSession()
this.handleClearLine = handleClearLine
this.physicalTest = new PhysicalPortTest([])
this.outputPhysicalTest = ''
this.outputLoadIosLicense = ''
this.listDeviceIos = []
this.debounceTimer = null
this.debounceSendSummaryReport = null
this.testingPortPoE = false
this.outputTestingPortPoE = ''
this.isPingToServer = false
this.outputPingToServer = ''
}
/**
* Connect to line with socket
*/
connect(timeoutMs = 5000) {
return new Promise<void>((resolve, reject) => {
const { ip, port, lineNumber, id, stationId } = this.config
let resolvedOrRejected = false
// Set timeout
this.client.setTimeout(timeoutMs)
console.log(`🔌 Connecting to line ${lineNumber} (${ip}:${port})...`)
this.client.connect(port, ip, () => {
if (resolvedOrRejected) return
resolvedOrRejected = true
console.log(`[${Date.now()}] ✅ Connected to line ${lineNumber} (${ip}:${port})`)
this.connecting = true
setTimeout(() => {
this.config.status = 'connected'
// this.retryConnect = 0
this.connecting = false
this.socketIO.emit('line_connected', {
stationId,
lineId: id,
lineNumber,
status: 'connected',
})
this.config.listFeatureTested = []
this.config.isSkipPhysical = false
this.config.reasonSkipPhysical = ''
this.sendFeatureTested()
this.checkLog()
resolve()
}, 2000)
})
this.client.on('data', (data) => {
let message = this.connecting ? cleanData(data.toString()) : data.toString()
const lines = this.bufferLog.push(data)
lines.forEach(this.handleLogLine)
let rawData = ''
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.outputLoadIosLicense) {
if (this.outputLoadIosLicense === true) this.outputLoadIosLicense = ''
this.outputLoadIosLicense += cleanData(data.toString())
}
if (this.config.runningPhysical) {
this.outputPhysicalTest += message
this.outputTestingPortPoE += message
if (this.debounceTimer) clearTimeout(this.debounceTimer)
if (this.testingPortPoE)
this.debounceTimer = setTimeout(() => {
this.flushLogBuffer()
}, 1000) // 1s debounce
}
if (this.isPingToServer) this.outputPingToServer += cleanData(data.toString())
if (data.toString().includes('More') || data.toString().includes('MORE'))
this.writeCommand(' ')
// let output = cleanData(message)
// console.log(`📨 [${this.config.port}] ${message}`)
// Handle netOutput with backspace support
for (const char of message) {
if (char === '\x7F' || char === '\x08') {
this.config.output = this.config.output.slice(0, -1)
// message = message.slice(0, -1)
} else {
rawData += char
}
}
this.config.output += cleanData(rawData)
this.config.output = this.config.output.slice(-15000)
if (!this.config.isReady && canInputCommand(message)) {
this.config.isReady = true
this.socketIO.emit('update_status_ready', {
stationId,
lineId: id,
isReady: true,
})
}
this.socketIO.emit('line_output', {
stationId,
lineId: id,
data: message,
ports: this.config.ports,
})
setTimeout(() => {
if (!this.config.inventory) {
this.getInventory()
}
}, 5000)
appendLog(
cleanData(message),
this.config.stationId,
this.config.stationName,
this.config.stationIp,
this.config.lineNumber
)
})
this.client.on('error', (err) => {
if (resolvedOrRejected) return
resolvedOrRejected = true
console.error(`❌ Error line ${lineNumber}:`, err.message)
this.config.output += '\r\n' + err.message + '\r\n'
this.socketIO.emit('line_error', {
stationId,
lineId: id,
error: '\r\n' + err.message + '\r\n',
})
this.endTesting()
resolve()
})
this.client.on('close', async () => {
console.log(`[${Date.now()}] 🔌 Line ${lineNumber} disconnected`)
this.config.status = 'disconnected'
this.config.output += this.config.output + '[CLEAR_TERMINAL_SCROLL_BACK]'
this.config.listFeatureTested = []
this.config.isSkipPhysical = false
this.config.reasonSkipPhysical = ''
this.config.latestScenario = undefined
this.physicalTest = new PhysicalPortTest([])
this.config.isReady = false
// this.config.inventory = undefined
this.socketIO.emit('line_disconnected', {
stationId,
lineId: id,
lineNumber,
status: 'disconnected',
})
// if (this.retryConnect <= 5) {
// await this.sleep(5000)
// console.log(`Retry connect line [${this.config.lineNumber}] times`, this.retryConnect)
// this.retryConnect += 1
// await this.reconnect()
// } else {
// this.retryConnect = 0
// }
this.endTesting()
})
this.client.on('timeout', () => {
if (resolvedOrRejected) return
resolvedOrRejected = true
const message = '\r\nConnection timeout!!\r\n'
this.config.output += message
this.socketIO.emit('line_output', {
stationId,
lineId: id,
data: message,
})
appendLog(
cleanData(message),
this.config.stationId,
this.config.stationName,
this.config.stationIp,
this.config.lineNumber
)
console.log(`⏳ Connection timeout line ${lineNumber}`)
this.client.destroy()
resolve()
// reject(new Error('Connection timeout'))
})
})
}
/**
* Waiting with millisecond
*/
private sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms))
}
/**
* Write a command with socket.write
*/
async writeCommand(cmd: string | Buffer<ArrayBuffer>) {
if (this.client.destroyed) {
console.log(`⚠️ Cannot send, line ${this.config.lineNumber} is closed`)
this.disconnect()
// this.disconnect()
// await sleep(2000)
// await this.connect()
return
}
// console.log(
// `Write command "${cmd.toString().replace(/\r/g, '\\r').replace(/\n/g, '\\n')}" to line ${this.config.lineNumber} of ${this.config.stationName}`
// )
this.client.write(cmd)
}
/**
* Disconnect socket with line
*/
async disconnect() {
try {
console.log('[DISCONNECT] Line', this.config.lineNumber)
// this.handleClearLine()
this.client.destroy()
this.config.status = 'disconnected'
this.socketIO.emit('line_disconnected', {
...this.config,
status: 'disconnected',
})
console.log(`🔻 Closed connection to line ${this.config.lineNumber}`)
} catch (e) {
console.error('Error closing line:', e)
}
}
/**
* Run a scenario as DPELP, Breaking password, load ios,...
*/
async runScript(script: Scenario, userName: string) {
if (!this.client || this.client.destroyed) {
console.log('Not connected')
this.config.runningScenario = ''
this.socketIO.emit('running_scenario', {
stationId: this.config.stationId,
lineId: this.config.id,
title: '',
})
this.outputBuffer = ''
return
}
if (!this.config.isReady) {
console.log('Device is not ready')
return
}
if (this.config.runningScenario || this.config.runningPhysical) {
console.log('Script already running')
return
}
console.log(
`Run scenario "${script?.title}" to line ${this.config.lineNumber} of ${this.config.stationName}`
)
this.config.runningScenario = script?.title
this.config.data = []
this.outputScenario = ''
this.socketIO.emit('running_scenario', {
stationId: this.config.stationId,
lineId: this.config.id,
title: script?.title,
})
if (script?.send_result || script?.sendResult) {
this.dataDPELP = ''
// this.config.inventory = ''
}
if (script?.isReboot) {
await sleep(10000)
for (let index = 0; index < 30; index++) {
await sleep(1000)
this.breakSpam()
}
}
const now = Date.now()
this.outputScenario += `\n\n---start-scenarios---${now}---${userName}---${script?.title}---\n---scenario---${script?.title}---${now}---\n`
appendLog(
`\n\n---start-scenarios---${now}---${userName}---${script?.title}---\n---scenario---${script?.title}---${now}---\n`,
this.config.stationId,
this.config.stationName,
this.config.stationIp,
this.config.lineNumber
)
this.config.latestScenario = {
name: script?.title,
time: now,
}
const steps = typeof script?.body === 'string' ? JSON.parse(script?.body) : []
let stepIndex = 0
// Create a timeout
let timeoutTimer: NodeJS.Timeout | null = null
const timeoutNumber = script.timeout ? Number(script.timeout) * 1000 : 300000
const onTimeout = () => {
this.config.runningScenario = ''
this.socketIO.emit('running_scenario', {
stationId: this.config.stationId,
lineId: this.config.id,
title: '',
})
this.outputBuffer = ''
this.outputScenario = ''
this.config.output += '\nTimeout run scenario\n'
this.dataDPELP = {
line: this.config.lineNumber,
pid: '',
vid: '',
sn: '',
ios: '',
mac: '',
license: [],
issues: ['No data'],
summary: '',
}
this.socketIO.emit('line_output', {
stationId: this.config.stationId,
lineId: this.config.id,
data: '\nTimeout run scenario\n',
})
this.outputScenario += `\n---end-scenarios---${now}---${userName}---\n`
appendLog(
`\n---end-scenarios---${now}---${userName}---\n`,
this.config.stationId,
this.config.stationName,
this.config.stationIp,
this.config.lineNumber
)
// reject(new Error('Script timeout'))
}
const resetTimeout = () => {
// console.log('resetTimeout', timeoutNumber)
// this.outputBuffer = ''
if (timeoutTimer) clearTimeout(timeoutTimer)
timeoutTimer = setTimeout(onTimeout, timeoutNumber)
}
return new Promise((resolve, reject) => {
timeoutTimer = setTimeout(onTimeout, timeoutNumber)
const runStep = async (index: number) => {
if (index >= steps.length) {
if (this.waitingScenario) {
this.waitingScenario = false
setTimeout(() => {
runStep(index)
}, 5000)
return
} else if (timeoutTimer) clearTimeout(timeoutTimer)
this.outputScenario += `\n---end-scenarios---${now}---${userName}---\n`
this.outputBuffer = ''
this.config.runningScenario = ''
this.socketIO.emit('running_scenario', {
stationId: this.config.stationId,
lineId: this.config.id,
title: '',
})
const logScenarios = this.outputScenario
const data = textfsmResults(logScenarios, '')
let pid = this.config.inventory?.pid || ''
try {
for (const item of data) {
if (item?.textfsm && isValidJson(item?.textfsm)) {
if (
['show inventory', 'sh inventory', 'show inv', 'sh inv'].includes(item.command)
) {
const dataInventory = JSON.parse(item.textfsm)[0]
this.config.inventory = this.config.inventory
? { ...this.config.inventory, ...dataInventory }
: dataInventory
pid = dataInventory?.pid || ''
this.addHistory(this.config.stationId, this.config.id, {
id: this.config.id,
number: this.config.lineNumber,
stationId: this.config.stationId,
pid: dataInventory?.pid,
sn: dataInventory?.sn,
vid: dataInventory?.vid,
scenario: script?.title,
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
if (pid && (dataVer?.MEMORY || dataVer?.USB_FLASH)) {
await this.checkConfigRam(
dataVer?.MEMORY || '',
dataVer?.USB_FLASH || '',
pid,
cleanData(item.output)
)
}
}
if (
item.command?.trim()?.includes('show env') ||
item.command?.trim()?.includes('sh env')
) {
const dataEnv = await this.detectShowEnvWithAI(item.output)
item.dataAI = dataEnv
}
item.textfsm = JSON.parse(item.textfsm)
}
}
const scenario = await detectScenarioByModel(pid, this.listScenarios)
console.log(pid, scenario?.title, this.listScenarios)
if (
scenario &&
scenario.id !== script.id &&
scenario.title.includes('DPELP') &&
script.title.includes('DPELP')
) {
this.listScenarios.push(scenario.id)
// this.outputScenario = ''
this.runScript(scenario, userName)
// this.socketIO.emit('confirm_scenario', {
// scenario: scenario,
// id: this.config.id,
// })
resolve(true)
return
}
if (script?.send_result || script?.sendResult) {
const detectLog = await this.detectLogWithAI(logScenarios)
const result = mapToLineFormat({
lineNumber: this.config.lineNumber,
inventory: this.config.inventory,
latestScenario: {
detectAI: detectLog,
},
data,
})
// if (script?.send_result || script?.sendResult) {
this.dataDPELP = result
console.log(
`DPELP DATA line ${this.config.lineNumber} of ${this.config.stationName}:`,
this.dataDPELP
)
this.config.listFeatureTested = [
...new Set([...this.config.listFeatureTested, 'DPELP']),
]
// if (!this.config.listFeatureTested.includes('PHYSICAL')) this.runPhysicalTest()
this.sendFeatureTested()
// Set timeout send report
// this.setTimeoutSendSummaryReport(
// !this.config.listFeatureTested.includes('PHYSICAL') ? 600000 : 30000
// )
// }
if (this.config.latestScenario)
this.config.latestScenario = { ...this.config.latestScenario, detectAI: detectLog }
// if (result.sn) {
// this.updateNote(result.sn, result)
// }
}
this.config.data = data
this.socketIO.emit('data_textfsm', {
stationId: this.config.stationId,
lineId: this.config.id,
data,
inventory: this.config.inventory || null,
latestScenario: this.config.latestScenario || null,
})
} catch (error) {
console.log(error)
}
appendLog(
`\n---end-scenarios---${now}---${userName}---\n`,
this.config.stationId,
this.config.stationName,
this.config.stationIp,
this.config.lineNumber
)
this.listScenarios = []
resolve(true)
return
} else resetTimeout()
const step = steps[index]
let repeatCount = Number(step.repeat) || 1
const delay = step?.delay ? Number(step?.delay) * 1000 : 1000
const sendCommand = async () => {
// if (repeatCount <= 0) {
// // Done → next step
// stepIndex++
// return runStep(stepIndex)
// }
if (typeof step.send !== 'undefined') {
console.log(Date.now() - now, (step?.send ?? '[ENTER]').toString())
this.outputScenario += `\n---send-command---"${(step?.send ?? '[ENTER]').toString().replace(/\r/g, '\\r').replace(/\n/g, '\\n')}"---${now}---\n`
appendLog(
`\n---send-command---"${(step?.send ?? '[ENTER]').toString().replace(/\r/g, '\\r').replace(/\n/g, '\\n')}"---${now}---\n`,
this.config.stationId,
this.config.stationName,
this.config.stationIp,
this.config.lineNumber
)
this.writeCommand((step?.send || '') + '\r\n')
}
repeatCount--
if (repeatCount <= 0) {
// Done → next step
stepIndex++
return runStep(stepIndex)
} else setTimeout(() => sendCommand(), delay)
}
// Nếu expect rỗng → gửi ngay
if (!step?.expect || step?.expect.trim() === '') {
setTimeout(() => sendCommand(), delay)
return
}
// while (this.outputBuffer) {
// await sleep(200)
// if (this.outputBuffer.includes(step.expect)) {
// this.outputBuffer = ''
// setTimeout(() => sendCommand(), delay)
// }
// }
const matched = await this.waitForExpect(
step.expect.trim(),
script?.timeout ? Number(script?.timeout) * 1000 : 60000
)
if (matched) setTimeout(() => sendCommand(), delay)
}
runStep(stepIndex)
})
}
/**
* Reconnect socket with line
*/
public async reconnect(): Promise<boolean> {
try {
this.disconnect()
this.client = new net.Socket()
await this.sleep(1000)
await this.connect()
return true
} catch (err: any) {
return false
}
}
/**
* User open CLI from front-end
*/
userOpenCLI(user: User) {
this.config.openCLI = true
this.config.userEmailOpenCLI = user.userEmail
this.config.userOpenCLI = user.userName
this.socketIO.emit('user_open_cli', {
stationId: this.config.stationId,
lineId: this.config.id,
userEmailOpenCLI: user.userEmail,
userOpenCLI: user.userName,
})
appendLog(
`\n-------${user.userName}-------\n`,
this.config.stationId,
this.config.stationName,
this.config.stationIp,
this.config.lineNumber
)
}
/**
* User close CLI from front-end
*/
userCloseCLI() {
this.config.openCLI = false
this.config.userEmailOpenCLI = ''
this.config.userOpenCLI = ''
this.socketIO.emit('user_close_cli', {
stationId: this.config.stationId,
lineId: this.config.id,
userEmailOpenCLI: '',
})
}
/**
* Clear output buffer
*/
clearCLI() {
this.config.output = ''
this.socketIO.emit('user_clear_terminal', {
stationId: this.config.stationId,
lineId: this.config.id,
})
setTimeout(() => this.writeCommand('\r\n'), 100)
}
/**
* Waiting for a expect with until catch it from output
*/
waitForExpect = async (expect: string, timeout = 60000) => {
const start = Date.now()
// console.log('[EXPECT]', expect, timeout)
while (Date.now() - start < timeout) {
if (this.outputBuffer.includes(expect)) {
this.outputBuffer = ''
return true
}
await sleep(200)
}
return false
}
/**
* Detect inventory data from output
*/
getInventory = () => {
const data = textfsmResults(this.outputInventory, 'show inventory')
try {
data.forEach((item) => {
if (item?.textfsm && isValidJson(item?.textfsm)) {
if (['show inventory', 'sh inventory', 'show inv', 'sh inv'].includes(item.command)) {
const dataInventory = JSON.parse(item.textfsm)[0]
this.config.inventory = this.config.inventory
? { ...this.config.inventory, ...dataInventory }
: dataInventory
}
item.textfsm = JSON.parse(item.textfsm)
}
})
if (this.config.inventory) {
this.config.data = data
this.socketIO.emit('data_textfsm', {
stationId: this.config.stationId,
lineId: this.config.id,
data,
inventory: this.config.inventory || null,
latestScenario: this.config.latestScenario || null,
})
this.outputInventory = ''
}
} catch (error) {
console.log(error)
}
}
/**
* Gửi nhiều ký tự ESC để vào ROMMON
*/
breakSpam() {
console.log('SPAM Break to line:', this.config.lineNumber)
let count = 0
const escInterval = setInterval(() => {
if (count >= 10) {
clearInterval(escInterval)
return
}
this.client.write(Buffer.from([0xff, 0xf3])) // Ctrl + Break
count++
}, 1)
}
/**
* Set Baud of line
*/
async setBaud(baud: number) {
this.writeCommand('enable\r\n')
await sleep(500)
this.writeCommand('configure terminal\r\n')
await sleep(500)
this.writeCommand('line console 0\r\n')
await sleep(500)
this.writeCommand(`speed ${baud.toString()}\r\n`)
await sleep(500)
this.writeCommand('end\r\n')
await sleep(500)
this.writeCommand('write memory\r\n')
this.writeCommand('\r\n')
}
/**
* Get content's log of line with date
*/
async getLog(date: string) {
const logDir = path.join('storage', 'system_logs')
const logFile = path
.join(
logDir,
`${date}-AUTO-Session.${this.config.stationName}-${this.config.stationId}-${this.config.stationIp}-${this.config.lineNumber}.log`
)
.replaceAll(' ', '_')
if (!fs.existsSync(logDir) || !fs.existsSync(logFile)) {
return ''
}
return await fs.promises.readFile(logFile, 'utf8')
}
/**
* Detect log by call api gpt, return summary and issues
*/
async detectLogWithAI(log: string) {
try {
const payload = {
model: 'gpt-4o-mini',
max_tokens: 1000,
messages: [
{
role: 'user',
content: `You are a network hardware tester.
Your task is to analyze router/switch logs to determine whether the device meets hardware standards for reselling.
Focus ONLY on hardware-related problems or abnormal warnings.
Software or configuration issues (e.g., port up/down, admin down, invalid commands, CLI errors, licensing messages) should be ignored unless they indicate hardware failure.
OUTPUT FORMAT (must follow exactly):
{
"issue": [ "problem 1", "problem 2", ... ],
"summary": "short summary under 30 words"
}
RULES:
- Summaries must be in English.
- Each issue must be one short line.
- If the log contains no hardware issues, output: { "issue": ["No issues detected."], "summary": "No hardware issues found." }
- Keep responses concise, readable, and strictly in JSON format.
- Do NOT add explanations outside the JSON.
- Your job is to detect hardware faults, missing components, overheating, failing modules, PSU issues, sensor anomalies, SIM/card missing, modem errors, transceiver issues, POST/diagnostics failures, etc.
The log to analyze will be provided after this prompt.
Here is the log:
${log}
`,
},
],
}
const remoteUrl = process.env.ERP_URL || 'https://stage.nswteam.net'
const remoteResp = await axios.post(
remoteUrl + '/api/transferPostData',
{
urlAPI: '/api/open-ai-sfp/model-image-info',
data: payload,
},
{
headers: {
Authorization: 'Bearer ' + process.env.ERP_TOKEN,
},
}
)
return remoteResp.data?.Status === 'OK' ? remoteResp.data?.data : ''
} catch (error: any) {
console.log('[ERROR] Detect log from AI', error)
}
return ''
}
/**
* Add cache to list history devices on this line
*/
async addHistory(stationId: number, lineId: number, item: HistoryItem) {
if (!item.pid || !item.sn) return
const key = `station:${stationId}:line:${lineId}:history`
const now = Date.now()
const newItem = JSON.stringify({
...item,
timestamp: now,
})
// Lấy phần tử cuối
const lastItems = await redis.zrevrange(key, 0, 0)
if (lastItems.length > 0) {
const last = JSON.parse(lastItems[0])
if (last.pid === item.pid && last.sn === item.sn) {
return false // không thay đổi
}
}
const line = await Line.find(lineId)
if (line) {
const listHistory = line.history ? JSON.parse(line.history) : []
listHistory.unshift(newItem)
line.history = JSON.stringify(listHistory)
await line.save()
}
// Add vào ZSET
await redis.zadd(key, now, newItem)
// Tự động xóa item > 96h
const expireTime = now - 96 * 60 * 60 * 1000
await redis.zremrangebyscore(key, 0, expireTime)
return true
}
/**
* Get list history devices
*/
async getHistory(stationId: number, lineId: number) {
const key = `station:${stationId}:line:${lineId}:history`
const items = await redis.zrange(key, 0, -1)
return items.map((i) => JSON.parse(i))
}
/**
* Handle raw log to regex error
*/
handleLogLine = (line: string) => {
try {
const parsed = classifyLog(line)
this.session.applyParsedLog(parsed)
} catch (error) {
console.log('handleLogLine', error)
}
}
/**
* Check raw log was regex each 5 minutes, if has error will send email report
*/
checkLog() {
const interval = setInterval(async () => {
try {
if (this.config.status !== 'connected') {
clearInterval(interval)
this.session.clear()
this.bufferLog.clear()
return
}
const result = this.session.finalize()
if (result.errors.length === 0) {
this.session.clear()
this.bufferLog.clear()
return
}
// const detectLog = await this.detectLogWithAI(this.bufferLog.allBuffer)
// console.log(detectLog)
const tableHTML = this.buildEmailContent(result)
await sendMessageToMail(
`[ATC] - [${this.config.stationName} - Line: ${this.config.lineNumber}] - Raw log issue ${result?.errors?.some((e) => e.category === 'SPECIAL_KEYWORD') ? '+ Special keywords' : ''}`,
tableHTML +
`${`
<hr />
<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;">
${this.bufferLog.allBuffer}</span></div>`}`
)
this.session.clear()
this.bufferLog.clear()
} catch (err: any) {
console.error('Error checking log:', err)
}
}, 300000)
}
/**
* Render table to view error
*/
renderErrorTable(rows: ErrorRow[]): string {
if (!rows.length) {
return `<p style="color: green;">No errors detected</p>`
}
const header = `
<tr>
<th style="padding:6px; text-align:center;">Level</th>
<th style="padding:6px; text-align:center;">Rule</th>
<th style="padding:6px; text-align:center;">Message</th>
<th style="padding:6px; width:1000px;">Log Evidence</th>
</tr>
`
const body = rows
.map(
(r) => `
<tr>
<td style="padding:6px; text-align:center;">
<span style="color:${r.level === 'FAIL' ? 'red' : 'orange'};">${r.level}</span>
</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;">${escapeHtml(r.log.trim())
.split('*')
.filter((el) => el)
.join('<br/>*')}</td>
</tr>
`
)
.join('')
return `
<table border="1" cellpadding="6" style="border-collapse: collapse; width:100%;">
${header}
${body}
</table>
`
}
/**
* Return a body email
*/
buildEmailContent(result: TestResult): string {
const rows = mapErrorsToRows(result.errors)
const table = this.renderErrorTable(rows)
return `
<h3>Cisco Device Log Result</h3>
<p>Line: <b>${this.config.lineNumber}</b> - Station: <b>${this.config.stationName}</b></p>
<p><b>Summary:</b> ${result.summary}</p>
<hr />
${table}
<br />
`
}
/**
* Update note of SN to ERP after run DPELP
*/
async updateNote(sn: string, data: DataDPELP) {
const portPhysical = Array.from(this.physicalTest.ports.values())
const missing = portPhysical.filter((p) => !p.tested)
const missingPoE = missing.filter((p) => !p.name.includes('SFP'))
const missingSFP = missing.filter((p) => p.name.includes('SFP'))
const tested = portPhysical.filter((p) => p.tested)
const testedPoE = tested.filter((p) => !p.name.includes('SFP'))
const testedSFP = tested.filter((p) => p.name.includes('SFP'))
const licenses = Array.isArray(data.license)
? [...new Set(data.license)]
: data.license
? [data.license]
: []
const timeZone = process.env.TIME_ZONE || 'Australia/Sydney'
const dataFormat = momentTZ().tz(timeZone).format('YYYY/MM/DD, HH:mm')
const note = `-------[ATC]-[${dataFormat}]-------
*****[DPELP]*****
License: ${licenses.join(', ')}
Summary: ${data?.summary || ''}
Issues:
${data.issues?.length ? `- ` + data.issues.join(`\n- `) : ''}
*****[Physical]*****
Total Ports: ${portPhysical?.length}
Ports Tested (Link UP): ${tested.length} (${testedPoE?.length} PoE, ${testedSFP?.length} SFP)
Ports Missing/Down: ${missing.length}
${this.config.reasonSkipPhysical ? `***User skip test ports:\n- ${this.config.reasonSkipPhysical}` : ''}
\n`
await updateNoteToERP(sn, note)
}
/**
* Update note of SN to ERP from user input
*/
async updateNoteFromUser(sn: string, note: string, licenses: string[]) {
const portPhysical = Array.from(this.physicalTest.ports.values())
const missing = portPhysical.filter((p) => !p.tested)
const missingPoE = missing.filter((p) => !p.name.includes('SFP'))
const missingSFP = missing.filter((p) => p.name.includes('SFP'))
const tested = portPhysical.filter((p) => p.tested)
const testedPoE = tested.filter((p) => !p.name.includes('SFP'))
const testedSFP = tested.filter((p) => p.name.includes('SFP'))
const timeZone = process.env.TIME_ZONE || 'Australia/Sydney'
const dataFormat = momentTZ().tz(timeZone).format('YYYY/MM/DD, HH:mm')
const data = `-------[ATC]-[${dataFormat}]-------
*****[DPELP]*****
License: ${licenses.join(', ')}
Issues:
${note}
*****[Physical]*****
Total Ports: ${portPhysical?.length}
Ports Tested (Link UP): ${tested.length} (${testedPoE?.length} PoE, ${testedSFP?.length} SFP)
Ports Missing/Down: ${missing.length}\n\n`
const issueList = note
.split('\n')
.map((line) => (line[0] === '-' ? line.substring(1).trim() : line.trim()))
const detectAI = this.config?.latestScenario?.detectAI
? { ...this.config.latestScenario.detectAI, issue: issueList }
: { issue: issueList, summary: '', status: [] }
if (this.config.latestScenario) {
this.config.latestScenario = { ...this.config.latestScenario, detectAI }
}
await updateNoteToERP(sn, data)
}
/**
* Starting physical test (PoE ports testing)
*/
async runPhysicalTest() {
if (this.config.runningPhysical) {
console.log('Running physical test')
return
}
this.setTimeoutSendSummaryReport(600000)
this.config.runningPhysical = true
this.config.runningScenario = 'Physical Test'
this.config.isSkipPhysical = false
this.config.reasonSkipPhysical = ''
this.testingPortPoE = true
this.outputTestingPortPoE = ''
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) {
this.config.listFeatureTested = [...new Set([...this.config.listFeatureTested, 'PHYSICAL'])]
this.config.isSkipPhysical = true
this.config.reasonSkipPhysical = ''
this.sendFeatureTested()
console.log('End physical test')
this.endTesting()
return
}
this.physicalTest.start(
listPorts.map((el) => el),
this.config.inventory
)
const interval = setInterval(async () => {
if (!this.config.runningPhysical || this.config.status !== 'connected') {
clearInterval(interval)
} else if (this.physicalTest.done) {
clearInterval(interval)
this.sendReportPhysicalTest()
this.endTesting()
} else {
this.checkingPhysicalPort()
}
}, 15000)
}
async checkingPhysicalPort() {
try {
this.writeCommand('show power inline | include on\r\n')
this.writeCommand('\r\n')
await this.sleep(1000)
this.writeCommand('show interfaces status | include SFP\r\n')
this.writeCommand('\r\n')
await this.sleep(2000)
const output = this.outputPhysicalTest
this.outputPhysicalTest = ''
if (output) {
const ports = this.physicalTest.detectPorts(output)
this.socketIO.emit('test_port_physical', {
stationId: this.config.stationId,
lineId: this.config.id,
data: ports,
})
if (ports.length === this.config.ports.length) {
this.sendReportPhysicalTest()
this.endTesting()
}
}
} catch (error) {
console.log('checkingPhysicalPort', error)
}
}
async flushLogBuffer() {
try {
const lines = this.outputTestingPortPoE.split(/\r?\n/)
// giữ lại dòng cuối nếu chưa kết thúc hoàn chỉnh
this.outputTestingPortPoE = lines.pop() || ''
const completeLines = lines.join('\n')
if (completeLines.trim()) {
const ports = this.physicalTest.handleLog(completeLines)
if (ports?.length)
this.socketIO.emit('test_port_physical', {
stationId: this.config.stationId,
lineId: this.config.id,
data: ports,
})
}
} catch (error) {
console.log('flushLogBuffer', error)
}
}
/**
* End all testing
*/
endTesting() {
this.physicalTest.done = true
// this.physicalTest.resetTestedPorts()
this.config.runningPhysical = false
this.config.runningScenario = ''
this.testingPortPoE = false
this.outputTestingPortPoE = ''
this.outputBuffer = ''
this.outputScenario = ''
this.outputPhysicalTest = ''
this.config.ports = []
this.socketIO.emit('running_scenario', {
stationId: this.config.stationId,
lineId: this.config.id,
title: '',
})
}
/**
* Get list PoE ports
*/
async getPorts(): Promise<string[]> {
this.writeCommand(' terminal length 0\r\n')
this.writeCommand(' show power inline\r\n')
this.writeCommand(' \r\n')
await this.sleep(3000)
this.writeCommand(' show interfaces status\r\n')
this.writeCommand(' \r\n')
await this.sleep(4000)
const statusOutput = this.outputPhysicalTest
this.outputPhysicalTest = ''
const lines = statusOutput.split('\n')
const ports = []
for (const line of lines) {
// Match: "Gi1/0/1 auto off 0.0 n/a n/a 30.0 "
const matchPoE = line.match(/^(\S+)\s+\S+\s+(on|off)/i)
if (matchPoE) {
const name = matchPoE[1]
if (name.includes('/')) ports.push(normalizeInterface(name))
}
// Match: "Gi0/15 notconnect 1 auto auto 1000BaseSX SFP"
// Match: "Gi0/16 notconnect 1 auto auto Not Present"
// Match: "Gi1/1/4 notconnect 1 auto auto unknown"
const matchSFP = line.match(/^([A-Za-z0-9\/]+).*\b(SFP|Not Present|unknown)\b/i)
if (matchSFP) {
const name = matchSFP[1]
ports.push(normalizeInterface(name) + ' (SFP)')
}
}
this.config.ports = [...new Set(ports)]
return [...new Set(ports)]
}
/**
* Send report after done physical test
*/
async sendReportPhysicalTest(reason?: string) {
this.config.listFeatureTested = [...new Set([...this.config.listFeatureTested, 'PHYSICAL'])]
if (typeof reason === 'string' && reason.trim().length > 0) {
this.config.isSkipPhysical = true
this.config.reasonSkipPhysical = reason
}
this.sendFeatureTested()
// Set timeout send report
if (
this.config.listFeatureTested?.includes('PHYSICAL') &&
this.config.listFeatureTested?.includes('DPELP')
)
this.setTimeoutSendSummaryReport(5000)
const formReport = this.physicalTest.getFormReport(this.config.inventory)
const reasonSkipPhysical =
typeof reason === 'string' && reason.trim().length > 0
? `<b style="color: #ff0000;">User Skip Test Port</b><br/>
────────────────────────────────<br/>
${reason}`
: ''
await sendMessageToMail(
`[ATC] - [${this.config.stationName} - Line: ${this.config.lineNumber}] - Physical Ports Test`,
formReport + reasonSkipPhysical
)
}
/**
* Handle load ios for router
*/
async loadIosRouter(nameIos: string, userName: string) {
const station = await Station.find(this.config.stationId)
if (!station) return
this.outputLoadIosLicense = true
const network = station?.gateway || '172.25.1.1'
const tftpIp = station?.tftp_ip || '172.16.7.69'
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')
const body = buildBody(
'ROUTER_IOS',
tftpIp,
nameIos,
`${a}.${b}.100.${this.config.id < 254 ? this.config.id : 254 - this.config.id}`,
`${station?.gateway ? station?.gateway : '0.0.0.0'}`,
this.listDeviceIos
)
const script = {
id: 0,
isReboot: true,
sendResult: false,
send_result: false,
title: 'Load IOS Router',
timeout: 1000,
body: JSON.stringify(body),
}
await sleep(5000)
await this.runScript(script as any, userName)
await this.sendEmailLoadIos(nameIos, startTime)
}
/**
* Handle load ios for switch
*/
async loadIosSwitch(nameIos: string, userName: string) {
const station = await Station.find(this.config.stationId)
if (!station) return
this.outputLoadIosLicense = true
const network = station?.gateway || '172.25.1.1'
const tftpIp = station?.tftp_ip || '172.16.7.69'
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')
const address = `${a}.${b}.100.${this.config.id < 254 ? this.config.id : 254 - this.config.id}`
const gateway = `${station?.gateway ? station?.gateway : '0.0.0.0'}`
await this.configAddressGateway(address, gateway, 'vlan 1')
const pingSuccess = await this.pingToServer(tftpIp)
if (!pingSuccess) return
await this.backupIos(nameIos)
const body = buildBody('SWITCH_IOS', tftpIp, nameIos, address, gateway, this.listDeviceIos)
const script = {
id: 0,
isReboot: false,
sendResult: false,
send_result: false,
title: 'Load IOS Switch',
timeout: 1000,
body: JSON.stringify(body),
}
await this.runScript(script as any, userName)
await this.sendEmailLoadIos(nameIos, startTime)
}
/**
* Send mail report after load ios
*/
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 = `
Load IOS Report<br/>
────────────────────────────────<br/>
Station : <b>${this.config.stationName}</b><br/>
Line : <b>${this.config.lineNumber}</b><br/>
IOS : <b>${nameIos}</b> <br/>
Started At : ${startTime}<br/>
Finished At : ${dataFormat}<br/>
<br/>
`.trim()
await sendMessageToMail(
`[ATC] - [${this.config.stationName} - Line: ${this.config.lineNumber}] - Load IOS Report`,
body +
`${`
<hr />
<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;">
${this.outputLoadIosLicense}</span></div>`}`
)
this.outputLoadIosLicense = ''
}
/**
* Send mail report after load license
*/
async sendEmailLoadLicense(nameLicense: string, startTime: string) {
const timeZone = process.env.TIME_ZONE || 'Australia/Sydney'
const dataFormat = momentTZ().tz(timeZone).format('YYYY/MM/DD, HH:mm:ss')
const report = parseLicenseReport(
typeof this.outputLoadIosLicense === 'string' ? this.outputLoadIosLicense : ''
)
const body = `
Load License Report<br/>
────────────────────────────────<br/>
Station : <b>${this.config.stationName}</b><br/>
Line : <b>${this.config.lineNumber}</b><br/>
License : <b>${nameLicense}</b> <br/>
Started At : ${startTime}<br/>
Finished At : ${dataFormat}<br/>
────────────────────────────────<br/>
Summary licenses: <b>${report.summary.join(', ')}</b><br/>
Successful: <b>${report.imported.join(', ')}</b><br/>
Exist: <b>${report.exist.join(', ')}</b><br/>
Failed: <b>${report.failed.join(', ')}</b><br/>
<br/>
`.trim()
await sendMessageToMail(
`[ATC] - [${this.config.stationName} - Line: ${this.config.lineNumber}] - Load License Report`,
body +
`${`
<hr />
<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;">
${this.outputLoadIosLicense}</span></div>`}`
)
this.outputLoadIosLicense = ''
}
/**
* Check list ios exist on flash
*/
async checkDeviceFlash() {
this.writeCommand(' enable\r\n')
this.writeCommand('show flash:\r\n')
await sleep(2000)
const output = this.outputBuffer
const ios: string[] = []
let match
const SWITCH_BIN_REGEX = /^\s*\d+\s+-rwx\s+\d+\s+.*?\s+([^\s]+\.bin)\s*$/gim
const ROUTER_BIN_REGEX = /^\s*\d+\s+(\d+)\s+.*?\s+([^\s]+\.bin)\s*$/gim
// 🔍 Detect device type
const isSwitch = output.includes('rwx')
const regex = isSwitch ? SWITCH_BIN_REGEX : ROUTER_BIN_REGEX
// reset regex state
regex.lastIndex = 0
while ((match = regex.exec(output)) !== null) {
ios.push(isSwitch ? match[1] : match[2])
}
return ios
}
/**
* Delete File on Flash
*/
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)
}
/**
* Upload file from flash to TFTP server
*/
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(`i/${fileName}\r\n`)
this.outputBuffer = ''
await sleep(5000)
while (true) {
if (this.outputBuffer.includes('#')) {
this.outputBuffer = ''
this.config.runningScenario = ''
return true
}
await sleep(5000)
}
}
/**
* Get list ios from TFTP server
*/
async getListIos() {
try {
const controller = new IosLicenseController()
const listIos = await controller.getIos()
return listIos
} catch (error) {
console.log('Error get ios', error)
return []
}
}
/**
* Get current boot ios of device
*/
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
}
/**
* Backup ios to TFTP, after that delete it on flash for free space
*/
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?.map((value) => value.name)?.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)
}
/**
* Handle load License for switch
* Assumes traditional licensing (PAK/file-based) via TFTP
*/
async loadLicenseSwitch(licenseFileName: string, userName: string, portName: string) {
const station = await Station.find(this.config.stationId)
if (!station) return
this.outputLoadIosLicense = true
// Setup network variables (giống hệt logic load IOS để đảm bảo thông mạng)
const network = station?.gateway || '172.25.1.1'
const tftpIp = station?.tftp_ip || '172.16.7.69'
const [a, b] = network.split('.').map(Number)
// Setup time/logging
const timeZone = process.env.TIME_ZONE || 'Australia/Sydney'
const startTime = momentTZ().tz(timeZone).format('YYYY/MM/DD, HH:mm:ss')
const address = `${a}.${b}.100.${this.config.id < 254 ? this.config.id : 254 - this.config.id}`
const gateway = `${station?.gateway ? station?.gateway : '0.0.0.0'}`
await this.configAddressGateway(address, gateway, portName)
const pingSuccess = await this.pingToServer(tftpIp)
if (!pingSuccess) return
const body = buildBody(
'SWITCH_LICENSE',
tftpIp,
licenseFileName,
address,
gateway,
this.listDeviceIos,
portName
)
const script = {
id: 0,
isReboot: false,
sendResult: false,
send_result: false,
title: 'Load License Switch',
timeout: 1000,
body: JSON.stringify(body),
}
await this.runScript(script as any, userName)
await this.sendEmailLoadLicense(licenseFileName, startTime) // Nếu bạn có hàm gửi mail báo cáo
}
/**
* Handle load License for Router
*/
async loadLicenseRouter(licenseFileName: string, userName: string, portName: string) {
const station = await Station.find(this.config.stationId)
if (!station) return
this.outputLoadIosLicense = true
const network = station?.gateway || '172.25.1.1'
const tftpIp = station?.tftp_ip || '172.16.7.69'
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')
const address = `${a}.${b}.100.${this.config.id < 254 ? this.config.id : 254 - this.config.id}`
const gateway = `${station?.gateway ? station?.gateway : '0.0.0.0'}`
await this.configAddressGateway(address, gateway, portName, true)
const pingSuccess = await this.pingToServer(tftpIp)
if (!pingSuccess) return
const body = buildBody(
'ROUTER_LICENSE',
tftpIp,
licenseFileName,
address,
gateway,
this.listDeviceIos,
portName
)
const script = {
id: 0,
isReboot: false,
sendResult: false,
send_result: false,
title: 'Load License Router',
timeout: 1000,
body: JSON.stringify(body),
}
await this.runScript(script as any, userName)
await this.sendEmailLoadLicense(licenseFileName, startTime)
}
/**
* Detect log by call api gpt, return string[]
*/
async detectShowEnvWithAI(log: string) {
try {
const payload = {
model: 'gpt-4o-mini',
max_tokens: 1000,
messages: [
{
role: 'user',
content: `You are a network log parser.
Input is the raw output of Cisco "show environment" or "show environment all".
Your task:
- Focus ONLY on FAN and POWER related information.
- Ignore TEMPERATURE, VOLTAGE, and other sensors unless they relate to FAN or POWER.
- Extract each FAN or POWER component and its state.
- Normalize each item into the format:
"<NAME>: <STATE>"
Examples:
- "FAN is OK" -> "FAN: OK"
- "FAN 2 is FAILED" -> "FAN 2: FAILED"
- "POWER SUPPLY A is NOT PRESENT" -> "POWER SUPPLY A: NOT PRESENT"
- "PSU 1 Absent" -> "PSU 1: ABSENT"
Output requirements:
- Return ONLY a valid JSON array of strings.
- Do NOT include any explanation or extra text.
- Do NOT include code block.
- JSON must be directly parsable.
Here is the input log:
${log}
`,
},
],
}
const remoteUrl = process.env.ERP_URL || 'https://stage.nswteam.net'
const remoteResp = await axios.post(
remoteUrl + '/api/transferPostData',
{
urlAPI: '/api/open-ai-sfp/model-image-info',
data: payload,
},
{
headers: {
Authorization: 'Bearer ' + process.env.ERP_TOKEN,
},
}
)
return remoteResp.data?.Status === 'OK' ? remoteResp.data?.data : ''
} catch (error: any) {
console.log('[ERROR] Detect log show env from AI', error)
}
return ''
}
/**
* Check config RAM and Flash, if higher config will send report
*/
async checkConfigRam(mem: string, flash: string, pid: string, output: string) {
const configRam = await detectConfigRamByModel(pid)
if (configRam) {
const isWarningRAM = isRamSufficient(mem, configRam.ram)
const isWarningFlash = isRamSufficient(flash, configRam.flash)
if (isWarningRAM || isWarningFlash) {
const subject = `[ATC] - [${this.config.stationName} - Line: ${this.config.lineNumber}] - Warning RAM, Flash Configuration`
const body = `
<p>Station: <b>${this.config.stationName}</b></p>
<p>Line: <b>${this.config.lineNumber}</b></p>
<p>Model: <b>${pid}</b></p>
<p>RAM: ${mem ? `<b>${convertFromKilobytesString(mem)} (<span style="color: ${isWarningRAM ? 'red' : 'black'};">default: ${configRam.ram}</span>)</b>` : ''}</p>
<p>FLASH: ${flash ? `<b>${convertFromKilobytesString(flash)} (<span style="color: ${isWarningFlash ? 'red' : 'black'};">default: ${configRam.flash}</span>)</b>` : ''}</p>
<hr />
<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;">
${escapeHtml(output)
.replace('show ver', '')
.replace('sh ver', '')
.replace('show version', '')
.replace('sh version', '')
.replace(mem, `<span style="color: ${isWarningRAM ? 'red' : 'black'};">${mem}</span>`)
.replace(
flash,
`<span style="color: ${isWarningFlash ? 'red' : 'black'};">${flash}</span>`
)}</span></div>
`
await sendMessageToMail(subject, body)
}
}
}
/**
* Send list feature tested
*/
sendFeatureTested = async () => {
this.socketIO.emit('feature_tested', {
stationId: this.config.stationId,
lineId: this.config.id,
listFeatureTested: this.config.listFeatureTested,
isSkipPhysical: this.config.isSkipPhysical,
reasonSkipPhysical: this.config.reasonSkipPhysical,
})
}
/**
* Send summary of all report (DPELP, Physical Testing)
*/
sendReportSummary = async (snapshot?: {
snapConfig: LineConfig
snapPhysical: PhysicalPortTest
reason: string
}) => {
if (this.debounceSendSummaryReport) clearTimeout(this.debounceSendSummaryReport)
const physicalTest = snapshot?.snapPhysical ? snapshot?.snapPhysical : this.physicalTest
const config = snapshot?.snapConfig ? snapshot?.snapConfig : this.config
const portPhysical = Array.from(physicalTest.ports.values())
const missing = portPhysical.filter((p) => !p.tested)
const missingPoE = missing.filter((p) => !p.name.includes('SFP'))
const missingSFP = missing.filter((p) => p.name.includes('SFP'))
const tested = portPhysical.filter((p) => p.tested)
const testedPoE = tested.filter((p) => !p.name.includes('SFP'))
const testedSFP = tested.filter((p) => p.name.includes('SFP'))
const showVersion = config?.data?.find(
(d) => d.command?.trim()?.includes('show ver') || d.command?.trim()?.includes('sh ver')
)
const dataShowVersion =
showVersion?.textfsm && showVersion?.textfsm?.[0]
? showVersion?.textfsm?.[0]
: config?.inventory
const showLicense = config?.data?.find(
(d) => d.command?.trim()?.includes('show lic') || d.command?.trim()?.includes('sh lic')
)
const dataShowLic =
showLicense?.textfsm && Array.isArray(showLicense?.textfsm) ? showLicense?.textfsm : null
const issue = config?.latestScenario?.detectAI?.issue || []
const summary = config?.latestScenario?.detectAI?.summary || ''
const reason = this.config.reasonSkipPhysical || snapshot?.reason
const reasonSkipPhysical =
typeof reason === 'string' && reason.trim().length > 0
? `<br/><b style="color: #ff0000;">User Skip Test Port</b><br/>
────────────────────────────────<br/>
${reason}`
: ''
const body = `<table cellpadding="6" cellspacing="0" border="1" style="margin-top: 10px; border-collapse: collapse; width: 100%">
<tr>
<td style="width: 600px; text-align: center;">DPELP</td>
<td style="text-align: center;">Physical Testing</td>
</tr>
<tr>
<td>
Model: <b>${config?.inventory?.pid ?? ''}</b> <b>${config?.inventory?.vid ?? ''}</b><br/>
Serial Number: <b>${config?.inventory?.sn ?? ''}</b><br/>
MAC: <b>${dataShowVersion?.MAC_ADDRESS ?? ''}</b><br/>
IOS: <b>${dataShowVersion?.SOFTWARE_IMAGE ?? ''}</b> <b>${dataShowVersion?.VERSION ?? ''}</b><br/>
MEM: <b>${dataShowVersion?.MEMORY ? convertFromKilobytesString(dataShowVersion?.MEMORY) : ''}</b><br/>
FLASH: <b>${dataShowVersion?.USB_FLASH ? convertFromKilobytesString(dataShowVersion?.USB_FLASH) : ''}</b><br/>
Licenses: <b>${
dataShowLic
? dataShowLic
?.filter((el) => el.LICENSE_TYPE?.toLowerCase()?.includes('permanent'))
?.map((v) => v.FEATURE)
?.join(', ')
: ''
}</b><br/>
Summary: <b style="color: ${!summary?.includes('No hardware issues found') ? '#ff0000' : ''};">${summary}</b><br/>
Issues: <b style="color: ${issue.filter((el) => !el.includes('No issues detected')).length ? '#ff0000' : 'black'};">${issue?.length ? `<br>- ` + issue.join(`<br>- `) : 'No issues detected.'}</b><br/>
</td>
<td>
Total Ports: ${portPhysical?.length}<br/>
Ports Tested (Link UP): <b style="color: #008000;">${tested.length} (${testedPoE?.length} PoE, ${testedSFP?.length} SFP)</b><br/>
Ports Missing/Down: <b style="color: #ff0000;">${missing.length}</b><br/>
${
missingPoE?.length
? `
<br/><b style="color: #ff0000;">Ports Missing PoE</b><br/>
────────────────────────────────<br/>
<div style="column-count: 6;">${missingPoE.map((p) => physicalTest.normalizePortName(p.name)).join('<br/>')}</div>
`
: ''
}
${
missingSFP?.length
? `
<br/><b style="color: #ff0000;">Ports Missing SFP</b><br/>
────────────────────────────────<br/>
<div style="column-count: 6;">${missingSFP.map((p) => physicalTest.normalizePortName(p.name)).join('<br/>')}</div>`
: ''
}
${reasonSkipPhysical}
</td>
</tr>
</table>`
this.updateNote(config?.inventory?.sn, this.dataDPELP as DataDPELP)
await sendMessageToMail(
`[ATC] - [${config.stationName} - Line: ${config.lineNumber}] - Summary of Testing Results`,
body
)
this.socketIO.emit('summary_tested', {
stationId: this.config.stationId,
lineId: this.config.id,
body: body,
title: `[${config.stationName} - Line: ${config.lineNumber}] - Summary of Testing Results`,
})
}
/**
* Reset config information of line
*/
initConfig() {
this.config = {
id: 0,
port: 0,
lineNumber: 0,
ip: '',
stationId: 0,
stationName: '',
stationIp: '',
outlet: 0,
output: '',
status: '',
baud: 0,
openCLI: false,
userEmailOpenCLI: '',
userOpenCLI: '',
inventory: [],
data: [],
ports: [],
runningScenario: '',
runningPhysical: false,
listFeatureTested: [],
isReady: false,
}
this.physicalTest = new PhysicalPortTest([])
}
setTimeoutSendSummaryReport(timeout: number) {
// Debounce send summary report
if (this.debounceSendSummaryReport) clearTimeout(this.debounceSendSummaryReport)
// Snapshot toàn bộ data tại thời điểm này
const snapshot = {
snapConfig: this.config,
snapPhysical: this.physicalTest,
reason: '',
}
this.debounceSendSummaryReport = setTimeout(() => {
if (!this.config.listFeatureTested?.includes('PHYSICAL')) {
this.config.isSkipPhysical = true
this.config.reasonSkipPhysical = 'Timeout, The user has not completed the physical test'
snapshot.reason = 'Timeout, The user has not completed the physical test'
}
this.config.listFeatureTested = ['DPELP', 'PHYSICAL', 'SUMMARY']
this.sendFeatureTested()
this.sendReportSummary(snapshot)
}, timeout)
}
resetDPELP() {
this.config.listFeatureTested = []
this.config.isSkipPhysical = false
this.config.reasonSkipPhysical = ''
this.dataDPELP = ''
this.sendFeatureTested()
console.log('Reset DPELP data and features', this.config.id, this.config.listFeatureTested)
}
async pingToServer(serverIP: string) {
this.isPingToServer = true
this.writeCommand('\r\n')
this.writeCommand('enable\r\n')
await sleep(500)
this.writeCommand(`ping ${serverIP}\r\n`)
await sleep(500)
const start = Date.now()
// console.log('[EXPECT]', expect, timeout)
while (Date.now() - start < 60000) {
if (this.outputPingToServer.includes('Success rate')) {
const match = this.outputPingToServer.match(/Success rate is (\d+) percent/)
if (match) {
const rate = Number(match[1])
if (rate > 0) {
this.outputPingToServer = ''
this.isPingToServer = false
return true
} else {
this.isPingToServer = false
this.outputPingToServer = ''
this.config.output += '\n[CONNECT_TO_SERVER_TFTP_FAIL]\n'
this.socketIO.emit('line_output', {
stationId: this.config.stationId,
lineId: this.config.id,
data: '\n[CONNECT_TO_SERVER_TFTP_FAIL]\n',
})
return false
}
}
}
await sleep(500)
}
this.isPingToServer = false
this.outputPingToServer = ''
this.config.output += '\n[CONNECT_TO_SERVER_TFTP_FAIL]\n'
this.socketIO.emit('line_output', {
stationId: this.config.stationId,
lineId: this.config.id,
data: '\n[CONNECT_TO_SERVER_TFTP_FAIL]\n',
})
return false
}
/**
* Config ip address and default gateway for line
*/
async configAddressGateway(
address: string,
gateway: string,
portName: string,
isRouter?: boolean
) {
this.config.runningScenario = 'Config Network'
this.socketIO.emit('running_scenario', {
stationId: this.config.stationId,
lineId: this.config.id,
title: 'Config Network',
})
await this.writeCommand(`enable\r\n`)
await sleep(500)
await this.writeCommand(`configure terminal\r\n`)
await sleep(500)
await this.writeCommand(`interface ${portName}\r\n`)
await sleep(500)
await this.writeCommand(`ip address ${address} 255.255.0.0\r\n`)
await sleep(500)
await this.writeCommand(`no shutdown\r\n`)
await sleep(500)
await this.writeCommand(`exit\r\n`)
await sleep(500)
await this.writeCommand(
!isRouter ? `ip default-gateway ${gateway}\r\n` : `ip route 0.0.0.0 0.0.0.0 ${gateway}`
)
await sleep(500)
await this.writeCommand(`end\r\n`)
await sleep(500)
await this.writeCommand(`\r\n`)
await sleep(1000)
this.config.runningScenario = ''
this.socketIO.emit('running_scenario', {
stationId: this.config.stationId,
lineId: this.config.id,
title: '',
})
}
}