From fb1554d857968428017cf25ce5fd362d5d2fcbd8 Mon Sep 17 00:00:00 2001 From: nguyentrungthat <80239428+nguentrungthat@users.noreply.github.com> Date: Thu, 30 Oct 2025 16:23:08 +0700 Subject: [PATCH] Update --- BACKEND/app/services/line_connection.ts | 54 +++++++- BACKEND/app/ultils/helper.ts | 65 +++++++++ BACKEND/app/ultils/templates/index.ts | 78 +++++++++++ .../app/ultils/templates/show_inventory.ts | 130 ++++++++++++++++++ BACKEND/app/ultils/templates/show_license.ts | 62 +++++++++ BACKEND/app/ultils/templates/show_logging.ts | 65 +++++++++ BACKEND/app/ultils/templates/show_power.ts | 39 ++++++ BACKEND/app/ultils/templates/show_version.ts | 80 +++++++++++ BACKEND/package-lock.json | 35 ++++- BACKEND/package.json | 3 +- BACKEND/providers/socket_io_provider.ts | 1 + FRONTEND/src/App.tsx | 67 ++++++++- FRONTEND/src/components/CardLine.tsx | 1 + FRONTEND/src/untils/helper.ts | 7 + FRONTEND/src/untils/types.ts | 6 + 15 files changed, 684 insertions(+), 9 deletions(-) create mode 100644 BACKEND/app/ultils/templates/index.ts create mode 100644 BACKEND/app/ultils/templates/show_inventory.ts create mode 100644 BACKEND/app/ultils/templates/show_license.ts create mode 100644 BACKEND/app/ultils/templates/show_logging.ts create mode 100644 BACKEND/app/ultils/templates/show_power.ts create mode 100644 BACKEND/app/ultils/templates/show_version.ts diff --git a/BACKEND/app/services/line_connection.ts b/BACKEND/app/services/line_connection.ts index 8b86f7e..7e2c508 100644 --- a/BACKEND/app/services/line_connection.ts +++ b/BACKEND/app/services/line_connection.ts @@ -1,5 +1,14 @@ +import { textfsmResults } from './../ultils/templates/index.js' +import fs from 'node:fs' import net from 'node:net' -import { appendLog, cleanData, sleep } from '../ultils/helper.js' +import { + appendLog, + cleanData, + getLogWithTimeScenario, + getPathLog, + isValidJson, + sleep, +} from '../ultils/helper.js' import Scenario from '#models/scenario' interface LineConfig { @@ -14,6 +23,11 @@ interface LineConfig { openCLI: boolean userEmailOpenCLI: string userOpenCLI: string + data: { + command: string + output: string + textfsm: string + }[] } interface User { @@ -163,8 +177,9 @@ export default class LineConnection { } this.isRunningScript = true + const now = Date.now() appendLog( - `\n\n---start-scenarios---${Date.now()}---\n---scenario---${script?.title}---${Date.now()}---\n`, + `\n\n---start-scenarios---${now}---\n---scenario---${script?.title}---${now}---\n`, this.config.stationId, this.config.lineNumber, this.config.port @@ -183,7 +198,7 @@ export default class LineConnection { data: 'Timeout run scenario', }) appendLog( - `\n---end-scenarios---${Date.now()}---\n`, + `\n---end-scenarios---${now}---\n`, this.config.stationId, this.config.lineNumber, this.config.port @@ -197,18 +212,47 @@ export default class LineConnection { this.isRunningScript = false this.outputBuffer = '' appendLog( - `\n---end-scenarios---${Date.now()}---\n`, + `\n---end-scenarios---${now}---\n`, this.config.stationId, this.config.lineNumber, this.config.port ) + const pathLog = getPathLog( + this.config.stationId, + this.config.lineNumber, + this.config.port + ) + + if (pathLog) + fs.readFile(pathLog, 'utf8', async (err, content) => { + if (err) return + + const logScenarios = getLogWithTimeScenario(content, now) || '' + const data = await textfsmResults(logScenarios, '') + try { + data.forEach((item) => { + if (item?.textfsm && isValidJson(item?.textfsm)) { + item.textfsm = JSON.parse(item.textfsm) + } + }) + this.config.data = data + this.socketIO.emit('data_textfsm', { + stationId: this.config.stationId, + lineId: this.config.id, + data, + }) + } catch (error) { + console.log(error) + } + }) + resolve(true) return } const step = steps[index] appendLog( - `\n---send-command---"${step?.send ?? ''}"---${Date.now()}---\n`, + `\n---send-command---"${step?.send ?? ''}"---${now}---\n`, this.config.stationId, this.config.lineNumber, this.config.port diff --git a/BACKEND/app/ultils/helper.ts b/BACKEND/app/ultils/helper.ts index 32eb541..8523a5f 100644 --- a/BACKEND/app/ultils/helper.ts +++ b/BACKEND/app/ultils/helper.ts @@ -36,3 +36,68 @@ export function appendLog(output: string, stationId: number, lineNumber: number, } }) } + +export const getPathLog = (stationId: number, lineNumber: number, port: number) => { + const date = new Date().toISOString().slice(0, 10).replace(/-/g, '') // YYYYMMDD + const logDir = path.join('storage', 'system_logs') + const logFile = path.join(logDir, `${date}-Station_${stationId}-Line_${lineNumber}_${port}.log`) + // Ensure folder exists + if (!fs.existsSync(logDir)) { + fs.mkdirSync(logDir, { recursive: true }) + return null + } else return logFile +} + +/** + * Utility function get scope log with timestamp. + * @param {string} text - content log. + * @param {number} time - Timestamp. + */ +export const getLogWithTimeScenario = (text: string, time: number) => { + try { + // Match all start and end blocks + const regex = /---(start|end)-scenarios---(\d+)---/g + + let match + const blocks = [] + + while ((match = regex.exec(text)) !== null) { + blocks.push({ + type: match[1], + timestamp: match[2], + index: match.index, + }) + } + + // Find the matching block for the end timestamp + let result = null + for (let i = 0; i < blocks.length; i++) { + const block = blocks[i] + if (block.type === 'end' && block.timestamp === time.toString()) { + // Find nearest preceding "start" + for (let j = i - 1; j >= 0; j--) { + if (blocks[j].type === 'start') { + const startIndex = blocks[j].index + const endIndex = block.index + text.slice(block.index).indexOf('\n') // or manually offset length of the line + result = text.slice(startIndex, endIndex).trim() + break + } + } + break + } + } + return result + } catch (err) { + console.error('Error get log:', err) + return '' + } +} + +export function isValidJson(string: string) { + try { + JSON.parse(string) + return true // Chuỗi là định dạng JSON hợp lệ + } catch (e) { + return false // Chuỗi không phải là định dạng JSON hợp lệ + } +} diff --git a/BACKEND/app/ultils/templates/index.ts b/BACKEND/app/ultils/templates/index.ts new file mode 100644 index 0000000..31fa249 --- /dev/null +++ b/BACKEND/app/ultils/templates/index.ts @@ -0,0 +1,78 @@ +import showInventory from './show_inventory.js' +import showVersion from './show_version.js' +import showLicense from './show_license.js' +import showLogging from './show_logging.js' +// const showPower = require("./show_power.js"); + +// Function to parse logs +function getStructuredDataTextfsm(output: string, command: string) { + switch (command) { + case 'show inventory': + case 'show inv': + case 'sh inventory': + case 'sh inv': + return showInventory(output) + case 'show version': + case 'show ver': + case 'sh version': + case 'sh ver': + return showVersion(output) + case 'show license': + case 'sh license': + return showLicense(output) + case 'show logging': + case 'sh logging': + return showLogging(output) + default: + return '' + } + + // Call the parseLog method with log data and patterns +} + +export const textfsmResults = (logContent: string, cmd: string) => { + let results = [] + if (cmd) { + let structuredOutput = getStructuredDataTextfsm(logContent, cmd) + + if (typeof structuredOutput === 'string') { + structuredOutput = {} // Convert to an empty object if it's a string + } + results = [ + { + command: cmd, + output: logContent, + textfsm: JSON.stringify(structuredOutput).replace(/[\x00-\x1f\x7f-\x9f]/g, ''), + }, + ] + } else { + // Regular expression to parse commands and outputs inside the scoped content + const regexPattern = + /---send-command---"(?.+?)"---\d+---(?[\s\S]*?)(?=(---send-command---|---split-point---|---end-testing---|---end-scenarios---))/gms + + // Parse commands and outputs + const matches = [...logContent.matchAll(regexPattern)] + + // Process matches + results = matches + .map((match) => { + const command = match.groups?.command.trim() || '' + const output = match.groups?.output.trim() || '' + + // Get structured output using the parser + let structuredOutput = getStructuredDataTextfsm(output, command) + + if (typeof structuredOutput === 'string') { + structuredOutput = {} // Convert to an empty object if it's a string + } + + return { + command, + output, + textfsm: JSON.stringify(structuredOutput).replace(/[\x00-\x1f\x7f-\x9f]/g, ''), // Clean special characters + } + }) + .filter((el) => el.command) + } + return results +} diff --git a/BACKEND/app/ultils/templates/show_inventory.ts b/BACKEND/app/ultils/templates/show_inventory.ts new file mode 100644 index 0000000..b412a5e --- /dev/null +++ b/BACKEND/app/ultils/templates/show_inventory.ts @@ -0,0 +1,130 @@ +import XRegExp from 'xregexp' + +// Parse the log data +const parseLog = (data: string) => { + const patterns = [ + XRegExp('^NAME:\\s+"(?.*)",\\s+DESCR:\\s+"(?.*)"'), + XRegExp('^PID:\\s+(?[\\S+]+|.*),.*VID:\\s+(?.*),.*SN:\\s+(?[\\w+\\d+]*)'), + XRegExp('^PID:\\s+,.*VID:\\s+(?.*),.*SN:\\s+(?[\\w+\\d+]*)'), + XRegExp('^PID:\\s+(?[\\S+]+|.*),.*VID:\\s+(?.*),.*SN:'), + XRegExp('^PID:\\s+(?\\S+)(?:,|\\s+)VID:\\s+(?\\S+)(?:,|\\s+)SN:\\s+(?\\w+)'), + // License info + XRegExp('^License Level:\\s+(?.*)'), + XRegExp('^License Type:\\s+(?.*)'), + XRegExp('^Next reload license Level:\\s+(?.*)'), + ] + + const lines = data.split('\n') + const licenseLog = data.split('show version | include License') + let records: any = [] + let currentRecord: any = { + name: '', + descr: '', + pid: '', + vid: '', + sn: '', + licenseLevel: '', + licenseType: '', + nextLicenseLevel: '', + } + + for (const line of lines) { + for (const pattern of patterns) { + const match = XRegExp.exec(line, pattern) + if (match) { + const item = match?.groups + // Update current record with matched fields + Object.keys(currentRecord).forEach((key) => { + if (item && item[key] !== undefined) { + currentRecord[key] = item[key].trim() + } + }) + // If "pid", "vid", or "sn" are matched, push a completed record + if (currentRecord.pid || currentRecord.vid || currentRecord.sn) { + records.push({ ...currentRecord }) + currentRecord = { + name: '', + descr: '', + pid: '', + vid: '', + sn: '', + licenseLevel: '', + licenseType: '', + nextLicenseLevel: '', + } // Reset for the next record + } + if ((currentRecord.licenseLevel || currentRecord.licenseType) && records.length > 0) { + const value = records[0] + value.licenseLevel = currentRecord.licenseLevel + ? currentRecord.licenseLevel + : value.licenseLevel + value.licenseType = currentRecord.licenseType + ? currentRecord.licenseType + : value.licenseType + value.nextLicenseLevel = currentRecord.nextLicenseLevel + ? currentRecord.nextLicenseLevel + : value.nextLicenseLevel + records = [value] + currentRecord = { + name: '', + descr: '', + pid: '', + vid: '', + sn: '', + licenseLevel: '', + licenseType: '', + nextLicenseLevel: '', + } // Reset for the next record + } + break // Stop checking other patterns for this line + } + } + } + + if (records.length > 0) { + let extend = null + const firstRecord = records[0] + // check license and last 2 chars of pid + if ( + firstRecord.licenseLevel && + typeof firstRecord.licenseLevel === 'string' && + firstRecord.pid.length >= 2 + ) { + switch (firstRecord.licenseLevel.toLowerCase()) { + case 'network essentials': + case 'network-essentials': + case 'dna essentials': + case 'dna-essentials': + extend = '-E' + break + case 'network advantage': + case 'network-advantage': + case 'dna advantage': + case 'dna-advantage': + extend = '-A' + break + default: + break + } + } + if (licenseLog[1] && !firstRecord.licenseLevel) { + if (licenseLog[1]?.includes('essentials')) { + extend = '-E' + firstRecord.licenseLevel = 'network-essentials' + firstRecord.licenseType = 'Smart License' + } else if (licenseLog[1]?.includes('advantage')) { + extend = '-A' + firstRecord.licenseLevel = 'network-advantage' + firstRecord.licenseType = 'Smart License' + } + } + if (extend) { + const key = firstRecord.pid.slice(-2) + if (key !== extend) firstRecord.pid = firstRecord.pid + extend + records = [firstRecord] + } + } + return records +} + +export default parseLog diff --git a/BACKEND/app/ultils/templates/show_license.ts b/BACKEND/app/ultils/templates/show_license.ts new file mode 100644 index 0000000..dcd4d1c --- /dev/null +++ b/BACKEND/app/ultils/templates/show_license.ts @@ -0,0 +1,62 @@ +import XRegExp from 'xregexp' + +// Patterns for each field + +// Parser function +const parseLog = (data: string) => { + const patterns = [ + XRegExp('^Index\\s+\\d+\\s+Feature:\\s+(?\\S+)'), + XRegExp('^Period\\s+left:\\s+(?.+)'), + XRegExp('^Period\\s+Used:\\s+(?.+)'), + XRegExp('^License\\s+Type:\\s+(?.+)'), + XRegExp('^License\\s+State:\\s+(?.+)'), + XRegExp('^License\\s+Count:\\s+(?.+)'), + XRegExp('^License\\s+Priority:\\s+(?.+)'), + ] + + const lines = data.split('\n') + const records = [] + let currentRecord: any = null + + for (const line of lines) { + if (XRegExp.test(line, XRegExp('^Index\\s+\\d+\\s+Feature:'))) { + // Start a new record + if (currentRecord) { + records.push(currentRecord) + } + currentRecord = { + FEATURE: '', + PERIOD_LEFT: '', + PERIOD_USED: '', + LICENSE_TYPE: '', + LICENSE_STATE: '', + LICENSE_COUNT: '', + LICENSE_PRIORITY: '', + } + } + + if (currentRecord) { + for (const pattern of patterns) { + const match = XRegExp.exec(line, pattern) + if (match) { + const item = match?.groups || {} + Object.keys(item).forEach((key) => { + if (item && item[key] !== undefined) { + currentRecord[key] = item[key] + } + }) + break // Stop processing this line once a pattern matches + } + } + } + } + + // Push the last record if it exists + if (currentRecord) { + records.push(currentRecord) + } + + return records +} + +export default parseLog diff --git a/BACKEND/app/ultils/templates/show_logging.ts b/BACKEND/app/ultils/templates/show_logging.ts new file mode 100644 index 0000000..cc38a30 --- /dev/null +++ b/BACKEND/app/ultils/templates/show_logging.ts @@ -0,0 +1,65 @@ +// Import XRegExp +import XRegExp from 'xregexp' + +// Example matching function +const parseLog = (data: string) => { + // Define the regex components + const logPattern = XRegExp( + ` + ^\\*(?\\w{3})\\s+ + (?\\d{1,2})\\s+ + (?