This commit is contained in:
nguyentrungthat 2025-12-19 12:02:16 +07:00
parent 6c00e35072
commit b8ab1f0583
4 changed files with 160 additions and 7 deletions

View File

@ -3,11 +3,15 @@ import { textfsmResults } from './../ultils/templates/index.js'
import net from 'node:net' import net from 'node:net'
import { import {
appendLog, appendLog,
applyRules,
classifyLog,
cleanData, cleanData,
detectScenarioByModel, detectScenarioByModel,
isValidJson, isValidJson,
LogStreamBuffer,
mapToLineFormat, mapToLineFormat,
sleep, sleep,
TestSession,
} from '../ultils/helper.js' } from '../ultils/helper.js'
import Scenario from '#models/scenario' import Scenario from '#models/scenario'
import path from 'node:path' import path from 'node:path'
@ -105,10 +109,11 @@ export default class LineConnection {
private waitingScenario: boolean private waitingScenario: boolean
private outputInventory: string private outputInventory: string
private outputScenario: string private outputScenario: string
// private bufferCommand: string private bufferLog: LogStreamBuffer
public dataDPELP: DataDPELP | string public dataDPELP: DataDPELP | string
private listScenarios: number[] private listScenarios: number[]
public handleClearLine: () => void public handleClearLine: () => void
private session: TestSession
constructor(config: LineConfig, socketIO: any, handleClearLine: () => void) { constructor(config: LineConfig, socketIO: any, handleClearLine: () => void) {
this.config = config this.config = config
@ -120,7 +125,7 @@ export default class LineConnection {
this.waitingScenario = false this.waitingScenario = false
this.outputInventory = '' this.outputInventory = ''
this.outputScenario = '' this.outputScenario = ''
// this.bufferCommand = '' this.bufferLog = new LogStreamBuffer()
this.dataDPELP = { this.dataDPELP = {
line: this.config.lineNumber, line: this.config.lineNumber,
pid: '', pid: '',
@ -133,6 +138,7 @@ export default class LineConnection {
summary: '', summary: '',
} }
this.listScenarios = [] this.listScenarios = []
this.session = new TestSession()
this.handleClearLine = handleClearLine this.handleClearLine = handleClearLine
} }
@ -159,12 +165,15 @@ export default class LineConnection {
lineNumber, lineNumber,
status: 'connected', status: 'connected',
}) })
// this.checkLog()
resolve() resolve()
}, 1000) }, 1000)
}) })
this.client.on('data', (data) => { this.client.on('data', (data) => {
let message = this.connecting ? cleanData(data.toString()) : data.toString() let message = this.connecting ? cleanData(data.toString()) : data.toString()
// const lines = this.bufferLog.push(data)
// lines.forEach(this.handleLogLine)
let rawData = '' let rawData = ''
if (this.isRunningScript) { if (this.isRunningScript) {
this.waitingScenario = true this.waitingScenario = true
@ -449,9 +458,14 @@ export default class LineConnection {
}) })
const scenario = await detectScenarioByModel(pid, this.listScenarios) const scenario = await detectScenarioByModel(pid, this.listScenarios)
console.log(pid, scenario?.title, this.listScenarios) console.log(pid, scenario?.title, this.listScenarios)
if (scenario && scenario.id !== script.id) { if (
scenario &&
scenario.id !== script.id &&
scenario.title.includes('DPELP') &&
script.title.includes('DPELP')
) {
this.listScenarios.push(scenario.id) this.listScenarios.push(scenario.id)
// this.outputScenario = '' this.outputScenario = ''
this.runScript(scenario, userName) this.runScript(scenario, userName)
// this.socketIO.emit('confirm_scenario', { // this.socketIO.emit('confirm_scenario', {
// scenario: scenario, // scenario: scenario,
@ -770,4 +784,33 @@ export default class LineConnection {
const items = await redis.zrange(key, 0, -1) const items = await redis.zrange(key, 0, -1)
return items.map((i) => JSON.parse(i)) return items.map((i) => JSON.parse(i))
} }
handleLogLine = (line: string) => {
try {
const parsed = classifyLog(line)
const matchedRules = applyRules(parsed)
matchedRules.forEach((rule) => {
// console.log(rule)
this.session.applyRule(rule)
})
} catch (error) {
console.log('handleLogLine', error)
}
}
checkLog() {
const interval = setInterval(async () => {
try {
if (this.config.status !== 'connected') {
clearInterval(interval)
return
}
const result = this.session.finalize()
console.log('FINAL RESULT:', this.config.apcName, this.config.lineNumber, result)
} catch (err: any) {
console.error('Error checking log:', err)
}
}, 30000)
}
} }

View File

@ -3,6 +3,7 @@ import fs from 'node:fs'
import path from 'node:path' import path from 'node:path'
import nodeMailer from 'nodemailer' import nodeMailer from 'nodemailer'
import zulip from 'zulip-js' import zulip from 'zulip-js'
import { LogRule, ParsedLog } from './types.js'
type DetectAI = { type DetectAI = {
status: string[] status: string[]
@ -309,6 +310,7 @@ export function sendMessageToZulip(
// Catch scenario with key longer // Catch scenario with key longer
export const detectScenarioByModel = async (model: string, listScenarios: number[]) => { export const detectScenarioByModel = async (model: string, listScenarios: number[]) => {
let scenarios = await Scenario.query().preload('brand').preload('category') let scenarios = await Scenario.query().preload('brand').preload('category')
let scenarioDefault = await Scenario.findBy('title', 'DPELP DEFAULT')
const normalizedModel = model.trim().toUpperCase() const normalizedModel = model.trim().toUpperCase()
let matched: { scenario: Scenario; score: number } | null = null let matched: { scenario: Scenario; score: number } | null = null
@ -331,5 +333,95 @@ export const detectScenarioByModel = async (model: string, listScenarios: number
} }
} }
return matched?.scenario || null return matched?.scenario ? matched?.scenario : listScenarios.length === 0 ? scenarioDefault : null
}
export function classifyLog(line: string): ParsedLog {
if (/System Bootstrap|IOS XE Software|Booting/.test(line)) return { raw: line, category: 'BOOT' }
if (/LICENSE|Smart Licensing|Evaluation/.test(line)) return { raw: line, category: 'LICENSE' }
if (/LINK-3-UPDOWN|line protocol/.test(line)) return { raw: line, category: 'INTERFACE' }
if (/FAN|TEMP|POWER|PSU/.test(line)) return { raw: line, category: 'HARDWARE' }
if (/ERROR|FAIL|CRITICAL|Traceback/.test(line)) return { raw: line, category: 'ERROR' }
return { raw: line, category: 'UNKNOWN' }
}
export const RULES: LogRule[] = [
{
id: 'BOOT_OK',
category: 'BOOT',
match: /IOS XE Software|System Bootstrap/,
result: 'PASS',
message: 'Boot successful',
},
{
id: 'BOOT_LOOP',
category: 'BOOT',
match: /boot loop|reloading|restart/i,
result: 'FAIL',
message: 'Boot loop detected',
},
{
id: 'LICENSE_EXPIRED',
category: 'LICENSE',
match: /Evaluation.*expired|license expired/i,
result: 'WARN',
message: 'License expired',
},
{
id: 'HW_FAIL',
category: 'HARDWARE',
match: /(FAN|PSU).*(FAIL|CRITICAL)/i,
result: 'FAIL',
message: 'Hardware failure',
},
]
export function applyRules(log: ParsedLog): LogRule[] {
return RULES.filter((rule) => rule.category === log.category && rule.match.test(log.raw))
}
export class TestSession {
boot = false
hasHwFail = false
licenseWarn = false
issues: string[] = []
public applyRule(rule: LogRule) {
if (rule.id === 'BOOT_OK') this.boot = true
if (rule.result === 'FAIL') this.hasHwFail = true
if (rule.result === 'WARN') this.licenseWarn = true
this.issues.push(rule.message)
}
public finalize() {
if (!this.boot) return 'FAIL'
if (this.hasHwFail) return 'FAIL'
if (this.licenseWarn) return 'PARTIAL'
return 'PASS'
}
}
export class LogStreamBuffer {
private buffer = ''
public push(chunk: Buffer): string[] {
this.buffer += chunk.toString('utf8')
const lines = this.buffer.split(/\r?\n/)
this.buffer = lines.pop() || ''
return lines.map((l) => l.trim()).filter(Boolean)
}
public flush(): string | null {
if (!this.buffer) return null
const last = this.buffer
this.buffer = ''
return last
}
} }

View File

@ -7,3 +7,21 @@ export interface CustomSocket extends Socket {
export interface CustomServer extends Server { export interface CustomServer extends Server {
userKeys?: string[] userKeys?: string[]
} }
type LogCategory = 'BOOT' | 'LICENSE' | 'INTERFACE' | 'HARDWARE' | 'ERROR' | 'UNKNOWN'
export interface ParsedLog {
raw: string
category: LogCategory
timestamp?: Date
}
type RuleResult = 'PASS' | 'FAIL' | 'WARN'
export interface LogRule {
id: string
category: LogCategory
match: RegExp
result: RuleResult
message: string
}

View File

@ -1092,7 +1092,7 @@ export class WebSocketIo {
<td style="width:270px">${item.ios || ''}</td> <td style="width:270px">${item.ios || ''}</td>
<td style="width:200px;">${licenseHTML}</td> <td style="width:200px;">${licenseHTML}</td>
<td style="width:200px; text-wrap: wrap;">${item.summary || ''}</td> <td style="width:200px; text-wrap: wrap;">${item.summary || ''}</td>
<td>${item.issues?.length ? `- ` + item.issues.join(`<br>- `) : ''}</td> <td>${item.issues?.length ? `- ` + item.issues.join(`<br>- `) : '- No issues detected.'}</td>
</tr> </tr>
` `
} }
@ -1127,7 +1127,7 @@ export class WebSocketIo {
// Format issues // Format issues
const issuesMd = item.issues?.length const issuesMd = item.issues?.length
? item.issues.map((i: string) => `${i}`).join(' --') ? item.issues.map((i: string) => `${i}`).join(' --')
: '' : '- No issues detected.'
msg += msg +=
`| ${item.line || ''}` + `| ${item.line || ''}` +