ATC_SIMPLE/BACKEND/app/ultils/helper.ts

1526 lines
37 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import Scenario from '#models/scenario'
import fs from 'node:fs'
import path from 'node:path'
import nodeMailer from 'nodemailer'
import zulip from 'zulip-js'
import { ErrorRow, LogRule, ParsedLog, TestError, TestResult } from './types.js'
import axios from 'axios'
import moment from 'moment'
import Station from '#models/station'
import ConfigRam from '#models/config_ram'
import Keyword from '#models/keywords'
const mailTo = 'andrew.ng@apactech.io'
const mailCC = [
'ips@ipsupply.com.au',
'kay@ipsupply.com.au',
'joseph@apactech.io',
'kiet.phan@apactech.io',
]
// const mailCC = ''
type DetectAI = {
status: string[]
issue: string[]
summary: string
}
type InputData = {
lineNumber: number
inventory: any
latestScenario?: {
detectAI?: DetectAI
}
data?: any[]
}
// Types
type SendMailResponse = string
type SendMessageType = 'stream' | 'private'
type KeywordMatchType = 'contains' | 'exact'
interface KeywordRule extends LogRule {
keywordId?: number
}
/**
* Function to clean up unwanted characters from the output data.
* @param {string} data - The raw data to be cleaned.
* @returns {string} - The cleaned data.
*/
export const cleanData = (data: string) => {
return (
data
// 1⃣ Xóa chuỗi "--More--" (Cisco/Unix pager)
.replace(/--More--[\s\x08\x1b\[K]*/g, '')
// 2⃣ Xóa toàn bộ chuỗi ANSI escape sequences
// Ví dụ: ESC[2J, ESC[K, ESC[?25h, ESC[0m, ...
.replace(/\x1B\[[0-9;?]*[A-Za-z]/g, '')
// 3⃣ Xóa ký tự Backspace (BS) hoặc Delete (DEL)
.replace(/[\x08\x7F]/g, '')
// 4⃣ Xóa ký tự NUL và các control char khác (trừ \r, \n, \t)
.replace(/[^\x09\x0A\x0D\x20-\x7E]/g, '')
)
// 5⃣ Chuẩn hóa xuống dòng nếu cần
// .replace(/\r\n/g, '\n')
}
export function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms))
}
// 20250527-AUTO-Session.Station_1-13-192.168.171.9-2.log
// {DATE}-AUTO-Session.{Station name}-{Station ID}-{Station IP}-{Line number}.log
export function appendLog(
output: string,
stationId: number,
stationName: string,
stationIP: string,
lineNumber: number | string
) {
const date = new Date().toISOString().slice(0, 10).replace(/-/g, '') // YYYYMMDD
const logDir = path.join('storage', 'system_logs')
const logFile = path
.join(logDir, `${date}-AUTO-Session.${stationName}-${stationId}-${stationIP}-${lineNumber}.log`)
.replaceAll(' ', '_')
// Ensure folder exists
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir, { recursive: true })
}
fs.appendFile(logFile, output, (err) => {
if (err) {
console.error('❌ Failed to write log:', err.message)
}
})
}
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ệ
}
}
export function mapToLineFormat(input: InputData) {
const line = input.lineNumber
const pid = input.inventory?.pid || ''
const vid = input.inventory?.vid || ''
const sn = input.inventory?.sn || ''
// if (!pid || !sn) {
// return {
// line,
// pid: '',
// vid: '',
// sn: '',
// ios: '',
// mac: '',
// ram: '',
// flash: '',
// license: [],
// issues: ['No data'],
// summary: '',
// }
// }
// MAC
let mac = ''
let ios = ''
let ram = ''
let flash = ''
const showVersion = input.data?.find(
(d) =>
d.command === 'show version' ||
d.command === 'sh version' ||
d.command === 'show ver' ||
d.command === 'sh ver'
)
if (showVersion?.textfsm?.[0]?.MAC_ADDRESS) {
mac = showVersion.textfsm[0].MAC_ADDRESS
}
if (showVersion?.textfsm?.[0]?.SOFTWARE_IMAGE) {
ios = showVersion.textfsm[0].SOFTWARE_IMAGE + ' ' + (showVersion?.textfsm?.[0]?.VERSION || '')
}
if (showVersion?.textfsm?.[0]?.MEMORY) {
ram = showVersion.textfsm[0].MEMORY
}
if (showVersion?.textfsm?.[0]?.USB_FLASH) {
flash = showVersion.textfsm[0].USB_FLASH
}
// License
const dataLicense = input.data?.find((comm) => comm.command?.trim() === 'show license')
const license =
dataLicense?.textfsm && Array.isArray(dataLicense.textfsm)
? dataLicense.textfsm
?.filter((el: any) => el.LICENSE_TYPE === 'Permanent')
.map((v: any) => v.FEATURE)
: ''
// // Mode (DPEL / DPELP)
// const dataPlatform = input.data?.find((el) => el.command?.trim() === 'show platform')
// const mode = dataPlatform && !dataPlatform.output?.includes('Incomplete') ? 'DPELP' : 'DPEL'
// Issues
const issues = Array.isArray(input.latestScenario?.detectAI?.issue)
? input.latestScenario.detectAI.issue
: input.latestScenario?.detectAI?.issue
? [input.latestScenario.detectAI.issue]
: []
// Issues
const summary = input.latestScenario?.detectAI?.summary || ''
return {
line,
pid,
vid,
sn,
ios,
mac,
ram,
flash,
license,
issues,
summary,
}
}
export function sendMessageToMail(subject: string, text: string): Promise<SendMailResponse> {
return new Promise((resolve, reject) => {
const transporter = nodeMailer.createTransport({
pool: true,
host: process.env.SMTP_HOST,
port: Number(process.env.SMTP_PORT),
secure: true,
auth: {
user: process.env.SMTP_USERNAME,
pass: process.env.SMTP_PASSWORD,
},
})
const mailOptions = {
from: process.env.SMTP_USERNAME,
to: mailTo,
subject,
html: text,
cc: mailCC,
}
transporter.sendMail(mailOptions, (error: any, info: any) => {
if (error) {
console.error(error)
reject(error)
} else {
console.log('Email sent: ' + info.response)
resolve(info.response)
}
})
})
}
export function sendMessageToZulip(
type: SendMessageType,
to: string | number | string[],
topic: string | undefined,
content: string
): Promise<any> | null {
return new Promise((resolve, reject) => {
const config = {
realm: process.env.ZULIP_REALM as string,
username: process.env.ZULIP_USERNAME as string,
apiKey: process.env.ZULIP_API_KEY as string,
}
zulip(config).then((client: any) => {
if (type === 'stream') {
client.messages
.send({
type,
to,
topic: topic || '',
content,
})
.then((response: any) => {
console.log('Message sent: ' + JSON.stringify(response))
resolve(response)
})
.catch((error: any) => {
console.error(error)
reject(error)
})
} else if (type === 'private') {
client.messages
.send({
type,
to,
content,
})
.then((response: any) => {
console.log('Message sent: ' + JSON.stringify(response))
resolve(response)
})
.catch((error: any) => {
console.error(error)
reject(error)
})
}
})
})
}
// Catch scenario with key longer
export const detectScenarioByModel = async (model: string, listScenarios: number[]) => {
let scenarios = await Scenario.query().preload('brand').preload('category')
let scenarioDefault = await Scenario.findBy('title', 'DPELP DEFAULT')
const normalizedModel = model.trim().toUpperCase()
let matched: { scenario: Scenario; score: number } | null = null
for (const scenario of scenarios) {
if (listScenarios.includes(scenario.id)) continue
const seriesList: string[] = Array.isArray(scenario.series)
? scenario.series
: JSON.parse(scenario.series || '[]')
for (const s of seriesList) {
const pattern = s.trim().toUpperCase()
if (normalizedModel.startsWith(pattern)) {
const score = pattern.length
if (!matched || score > matched.score) {
matched = { scenario, score }
}
}
}
}
return matched?.scenario ? matched?.scenario : listScenarios.length === 0 ? scenarioDefault : null
}
// Catch scenario with key longer
export const detectConfigRamByModel = async (model: string) => {
let configsRam = await ConfigRam.query()
const normalizedModel = model.trim().toUpperCase()
let matched: { conf: ConfigRam; score: number } | null = null
for (const config of configsRam) {
const modelsList: string[] = Array.isArray(config.models)
? config.models
: JSON.parse(config.models || '[]')
for (const s of modelsList) {
const pattern = s.trim().toUpperCase()
if (normalizedModel.startsWith(pattern)) {
const score = pattern.length
if (!matched || score > matched.score) {
matched = { conf: config, score }
}
}
}
}
return matched?.conf ? matched?.conf : 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: 'SPECIAL_KEYWORD' }
}
export const RULES: LogRule[] = [
// BOOT
{
id: 'BOOT_OK',
category: 'BOOT',
match: /IOS XE Software|System Bootstrap|Boot successful/i,
level: 'PASS',
message: 'Boot successful',
},
{
id: 'BOOT_LOOP',
category: 'BOOT',
match: /boot loop|reloading|restart/i,
level: 'FAIL',
message: 'Boot loop detected',
},
{
id: 'BOOT_CRASH',
category: 'BOOT',
match: /crashinfo|Traceback|Kernel panic/i,
level: 'FAIL',
message: 'System crash detected during boot',
},
{
id: 'BOOT_SLOW',
category: 'BOOT',
match: /Booting.*takes longer than expected/i,
level: 'WARN',
message: 'Boot time abnormal',
},
// LICENSE
{
id: 'LICENSE_OK',
category: 'LICENSE',
match: /License State:\s*ACTIVE|Smart Licensing Status:\s*AUTHORIZED/i,
level: 'PASS',
message: 'License active',
},
{
id: 'LICENSE_EXPIRED',
category: 'LICENSE',
match: /Evaluation.*expired|license expired/i,
level: 'WARN',
message: 'License expired',
},
{
id: 'LICENSE_NOT_REGISTERED',
category: 'LICENSE',
match: /NOT REGISTERED|Registration failed/i,
level: 'WARN',
message: 'License not registered',
},
{
id: 'LICENSE_DISABLED',
category: 'LICENSE',
match: /Feature.*disabled due to license/i,
level: 'FAIL',
message: 'Critical features disabled by license',
},
// INTERFACE
// {
// id: 'INTERFACE_UP',
// category: 'INTERFACE',
// match: /LINK-3-UPDOWN: Interface .* up/i,
// level: 'PASS',
// message: 'Interface up',
// },
// {
// id: 'INTERFACE_FLAP',
// category: 'INTERFACE',
// match: /LINK-3-UPDOWN: Interface .* down/i,
// level: 'WARN',
// message: 'Interface flapping detected',
// },
{
id: 'INTERFACE_ERROR',
category: 'INTERFACE',
match: /input errors|CRC|frame error/i,
level: 'WARN',
message: 'Interface errors detected',
},
// HARDWARE
{
id: 'FAN_FAIL',
category: 'HARDWARE',
match: /FAN.*(FAIL|CRITICAL|NOT PRESENT)/i,
level: 'FAIL',
message: 'Fan failure',
},
{
id: 'PSU_FAIL',
category: 'HARDWARE',
match: /PSU.*(FAIL|CRITICAL|NOT PRESENT)/i,
level: 'FAIL',
message: 'Power supply failure',
},
{
id: 'TEMP_HIGH',
category: 'HARDWARE',
match: /TEMP.*(HIGH|CRITICAL)/i,
level: 'FAIL',
message: 'Over temperature detected',
},
{
id: 'HW_WARNING',
category: 'HARDWARE',
match: /ENVIRONMENT WARNING/i,
level: 'WARN',
message: 'Hardware environment warning',
},
// ERROR
{
id: 'MEMORY_ERROR',
category: 'ERROR',
match: /malloc|out of memory|memory corruption/i,
level: 'FAIL',
message: 'Memory error detected',
},
{
id: 'FLASH_ERROR',
category: 'ERROR',
match: /flash.*(error|corrupt|fail)/i,
level: 'FAIL',
message: 'Flash storage error',
},
{
id: 'CONFIG_MISSING',
category: 'ERROR',
match: /startup-config is missing|No configuration found/i,
level: 'WARN',
message: 'Startup configuration missing',
},
{
id: 'SECURE_BOOT_FAIL',
category: 'ERROR',
match: /Secure Boot.*(FAIL|ERROR)/i,
level: 'FAIL',
message: 'Secure boot failed',
},
]
export async function applyRules(log: ParsedLog): Promise<TestError[]> {
const KEYWORD_RULES: KeywordRule[] = await loadKeywordRules(log.raw)
return [...RULES, ...KEYWORD_RULES]
.filter(
(rule): rule is LogRule & { level: 'FAIL' | 'WARN' } =>
rule.category === log.category && rule.match.test(log.raw) && rule.level !== 'PASS'
)
.map((rule) => ({
ruleId: rule.id,
level: rule.level, // ✅ giờ TS biết chắc chỉ FAIL | WARN
message: rule.message,
category: rule.category,
evidence: {
raw: log.raw,
timestamp: log.timestamp,
},
}))
}
export class TestSession {
bootOk = false
errors: TestError[] = []
async applyParsedLog(log: ParsedLog) {
// Detect boot OK
if (/IOS XE Software|System Bootstrap/.test(log.raw)) {
this.bootOk = true
}
const matchedErrors = await applyRules(log)
matchedErrors.forEach((err) => this.addError(err))
}
private addError(err: TestError) {
const fingerprint = `${err.ruleId}|${this.normalize(err.evidence.raw)}`
const existing = this.errors.find(
(e) =>
`${e.ruleId}|${this.normalize(e.evidence.raw)}` === fingerprint ||
(err.ruleId === e.ruleId && err.category === 'SPECIAL_KEYWORD')
)
if (existing) {
existing.evidence.count = (existing.evidence.count ?? 1) + 1
return
}
err.evidence.count = 1
this.errors.push(err)
}
private normalize(raw: string): string {
return raw
.toLowerCase()
.replace(/\d+/g, '#') // thay số (PSU 1, PSU 2 → PSU #)
.replace(/\s+/g, ' ')
.trim()
}
finalize(): TestResult {
const hasFail = this.errors.some((e) => e.level === 'FAIL')
const hasWarn = this.errors.some((e) => e.level === 'WARN')
let status: TestResult['status'] = 'PASS'
if (hasFail) status = 'FAIL'
else if (hasWarn) status = 'PARTIAL'
return {
status,
summary: this.buildSummary(status),
errors: this.errors,
}
}
clear() {
this.errors = []
}
private buildSummary(status: TestResult['status']): string {
switch (status) {
case 'PASS':
return 'All tests passed'
case 'FAIL':
return 'Critical errors detected'
case 'PARTIAL':
return 'Warnings detected during test'
}
}
}
export class LogStreamBuffer {
public allBuffer = ''
private buffer = ''
public push(chunk: Buffer): string[] {
this.buffer += chunk.toString('utf8').replace('--More--', '').trim()
this.allBuffer += cleanData(chunk.toString())
const lines = this.buffer.split(/\r?\n/)
this.buffer = lines.pop() || ''
return lines.map((l) => l.replaceAll('--More--', '').trim()).filter(Boolean)
}
public flush(): string | null {
if (!this.buffer) return null
const last = this.buffer
this.buffer = ''
return last
}
public clear() {
this.allBuffer = ''
}
}
export function mapErrorsToRows(errors: TestError[]): ErrorRow[] {
return errors.map((e) => ({
level: e.level,
rule: e.ruleId,
message: e.message,
log: e.evidence.raw,
count: e.evidence.count ?? 1,
}))
}
export function escapeHtml(str: string): string {
return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
}
export async function updateNoteToERP(sn: string, note: string) {
try {
const remoteUrl = process.env.ERP_URL || 'https://stage.nswteam.net'
const header = {
Authorization: 'Bearer ' + process.env.ERP_TOKEN,
}
const responseDataSN = await axios.post(
remoteUrl + '/api/transferGetData',
{
urlAPI: '/api/stock-model-serial/get-list-regex',
filter: {
where: {
_q: sn,
},
},
orgId: ['5fadc798f070e4b64b53ac9c', '5fadc7b0f070e4b64b53ac9d'],
},
{
headers: header,
}
)
// console.log('updateNoteToERP', responseDataSN?.data?.data)
if (!responseDataSN?.data?.data || responseDataSN?.data?.data?.length === 0) {
return
}
const dataSN =
responseDataSN?.data?.data.length === 1
? responseDataSN?.data?.data[0]
: responseDataSN?.data?.data.length > 1
? responseDataSN?.data?.data.find((el: any) => el.serialNumberA === sn)
: {}
if (!dataSN?.id) return
const payload = {
id: dataSN?.id,
serialNumberA: dataSN?.serialNumberA,
productModelId: dataSN?.productModelId,
orgId: dataSN?.orgId,
testNotes: note + (dataSN?.testNotes || ''),
}
// console.log(payload)
await axios.post(
remoteUrl + '/api/transferPostData',
{
urlAPI: '/api/stock-model-serial/data-save',
data: payload,
},
{
headers: header,
}
)
} catch (error) {
console.log('updateNoteToERP', error)
}
}
export function normalizeInterface(name: string): string {
return name
.replace(/^Gi(?=\d)/, 'GigabitEthernet')
.replace(/^Fa(?=\d)/, 'FastEthernet')
.replace(/^Te(?=\d)/, 'TenGigabitEthernet')
.replace(/^Hu(?=\d)/, 'HundredGigE')
.replace(/^Eth(?=\d)/, 'Ethernet')
}
type BodyType = 'ROUTER_IOS' | 'SWITCH_IOS' | 'SWITCH_LICENSE' | 'ROUTER_LICENSE'
export function buildBody(
type: BodyType,
tftpIp: string,
fileName: string,
address: string,
gateway: string,
listDeviceIos: string[],
portName?: string
) {
switch (type) {
/* ================= ROUTER LOAD IOS ================= */
case 'ROUTER_IOS':
return [
{
expect: '',
send: `IP_ADDRESS=${address}`,
delay: '1',
repeat: '1',
note: '',
},
{
expect: 'rommon',
send: `IP_SUBNET_MASK=255.255.0.0`,
delay: '1',
repeat: '1',
note: '',
},
{
expect: 'rommon',
send: `DEFAULT_GATEWAY=${gateway}`,
delay: '1',
repeat: '1',
note: '',
},
{
expect: 'rommon',
send: `TFTP_SERVER=${tftpIp}`,
delay: '1',
repeat: '1',
note: '',
},
{
expect: 'rommon',
send: `TFTP_FILE=i/${fileName}`,
delay: '1',
repeat: '1',
note: '',
},
{
expect: 'rommon',
send: listDeviceIos?.includes(fileName) ? '' : `tftpdnld`,
delay: '1',
repeat: '1',
note: '',
},
{
expect: listDeviceIos?.includes(fileName) ? '' : 'y/n',
send: listDeviceIos?.includes(fileName) ? '' : `y`,
delay: '2',
repeat: '1',
note: '',
},
{
expect: 'rommon',
send: `boot usbflash0:${fileName}`,
delay: '1',
repeat: '1',
note: '',
},
{
expect: 'Press RETURN to get started',
send: ``,
delay: '1',
repeat: '1',
note: '',
},
{
expect: '',
send: ``,
delay: '1',
repeat: '1',
note: '',
},
{
expect: '',
send: `enable`,
delay: '1',
repeat: '1',
note: '',
},
{
expect: '#',
send: `show inventory`,
delay: '1',
repeat: '1',
note: '',
},
{
expect: '#',
send: `show license`,
delay: '1',
repeat: '1',
note: 'Verify license status',
},
{
expect: '#',
send: ` show version`,
delay: '3',
repeat: '1',
note: '',
},
{
expect: '#',
send: `configure terminal`,
delay: '1',
repeat: '1',
note: '',
},
{
expect: '',
send: `boot system usbflash0:${fileName}`,
delay: '1',
repeat: '1',
note: '',
},
{
expect: '',
send: `end`,
delay: '1',
repeat: '1',
note: '',
},
{
expect: '',
send: `write memory`,
delay: '1',
repeat: '1',
note: '',
},
{
expect: '',
send: ``,
delay: '1',
repeat: '1',
note: '',
},
]
/* ================= SWITCH LOAD IOS ================= */
case 'SWITCH_IOS':
return [
{
expect: '',
send: ``,
delay: '1',
repeat: '1',
note: '',
},
{
expect: '',
send: `enable`,
delay: '1',
repeat: '1',
note: '',
},
{
expect: '#',
send: `configure terminal`,
delay: '1',
repeat: '1',
note: '',
},
{
expect: '#',
send: `interface vlan 1`,
delay: '1',
repeat: '1',
note: '',
},
{
expect: '#',
send: `ip address ${address} 255.255.0.0`,
delay: '1',
repeat: '1',
note: '',
},
{
expect: '#',
send: `no shutdown`,
delay: '1',
repeat: '1',
note: '',
},
{
expect: '#',
send: `exit`,
delay: '1',
repeat: '1',
note: '',
},
{
expect: '#',
send: `ip default-gateway ${gateway}`,
delay: '1',
repeat: '1',
note: '',
},
{
expect: '#',
send: `end`,
delay: '1',
repeat: '1',
note: '',
},
{ expect: '', send: ``, delay: '4', repeat: '1', note: '' },
{
expect: '#',
send: listDeviceIos?.includes(fileName) ? '' : `copy tftp: flash:`,
delay: '1',
repeat: '1',
note: '',
},
{
expect: '',
send: listDeviceIos?.includes(fileName) ? '' : `${tftpIp}`,
delay: '1',
repeat: '1',
note: '',
},
{
expect: '',
send: listDeviceIos?.includes(fileName) ? '' : `i/${fileName}`,
delay: '1',
repeat: '1',
note: '',
},
{ expect: '', send: ``, delay: '1', repeat: '1', note: '' },
{ expect: '', send: ``, delay: '1', repeat: '1', note: '' },
{ expect: '#', send: ``, delay: '1', repeat: '1', note: '' },
{
expect: '#',
send: `configure terminal`,
delay: '1',
repeat: '1',
note: '',
},
{
expect: '#',
send: `boot system flash:${fileName}`,
delay: '1',
repeat: '1',
note: '',
},
{
expect: '',
send: `end`,
delay: '1',
repeat: '1',
note: '',
},
{
expect: '',
send: `write memory`,
delay: '1',
repeat: '1',
note: '',
},
{
expect: '',
send: `reload`,
delay: '1',
repeat: '1',
note: '',
},
{
expect: '', // Router thường hỏi câu này
send: ``, // Enter confirm
delay: '1',
repeat: '1',
note: 'Confirm reload',
},
{
expect: '',
send: ``,
delay: '1',
repeat: '1',
note: 'Waiting for reboot...',
},
// --- PHẦN 4: VERIFY ---
{
expect: 'Press RETURN to get started!',
send: ``,
delay: '1',
repeat: '1',
note: 'Router is back online',
},
{
expect: '',
send: `enable`,
delay: '3',
repeat: '1',
note: 'Enable again',
},
{
expect: '#',
send: `show inventory`,
delay: '1',
repeat: '1',
note: '',
},
{
expect: '#',
send: `show license`,
delay: '1',
repeat: '1',
note: 'Verify license status',
},
{
expect: '#',
send: ` show version`,
delay: '3',
repeat: '1',
note: 'Verify version info',
},
]
/* ================= SWITCH LICENSE ================= */
case 'SWITCH_LICENSE':
return [
{
expect: '',
send: ``,
delay: '1',
repeat: '1',
note: 'Start session',
},
{
expect: '',
send: `enable`,
delay: '1',
repeat: '1',
note: 'Enter Enable mode',
},
{
expect: '#',
send: `configure terminal`,
delay: '1',
repeat: '1',
note: 'Enter Config mode',
},
{
expect: '#',
send: `interface ${portName ? portName : 'vlan 1'}`,
delay: '1',
repeat: '1',
note: 'Select Interface Vlan 1',
},
{
expect: '#',
send: `ip address ${address} 255.255.0.0`,
delay: '1',
repeat: '1',
note: 'Set IP Address',
},
{
expect: '#',
send: `no shutdown`,
delay: '1',
repeat: '1',
note: 'Up interface',
},
{
expect: '#',
send: `exit`,
delay: '1',
repeat: '1',
note: 'Exit interface',
},
{
expect: '#',
send: `ip default-gateway ${gateway}`,
delay: '1',
repeat: '1',
note: 'Set Gateway',
},
{
expect: '#',
send: `end`,
delay: '1',
repeat: '1',
note: 'End config',
},
{
expect: '',
send: ``,
delay: '4',
repeat: '1',
note: '',
},
{
expect: '#',
send: `license install tftp://${tftpIp}/License/${fileName}`,
delay: '1',
repeat: '1',
note: 'Install license',
},
{
expect: '',
send: ``,
delay: '1',
repeat: '1',
note: '',
},
{
expect: '#',
send: `write memory`,
delay: '1',
repeat: '1',
note: 'Save config',
},
{
expect: '',
send: ``,
delay: '1',
repeat: '1',
note: '',
},
{
expect: '#',
send: `reload`,
delay: '1',
repeat: '1',
note: 'Reload switch',
},
{
expect: '', // Router thường hỏi câu này
send: ``, // Enter confirm
delay: '1',
repeat: '1',
note: 'Confirm reload',
},
{
expect: '',
send: ``,
delay: '1',
repeat: '1',
note: 'Waiting for reboot...',
},
// --- PHẦN 4: VERIFY ---
{
expect: 'Press RETURN to get started!',
send: ``,
delay: '1',
repeat: '1',
note: 'Router is back online',
},
{
expect: '',
send: `enable`,
delay: '3',
repeat: '1',
note: 'Enable again',
},
{
expect: '#',
send: `show inventory`,
delay: '1',
repeat: '1',
note: '',
},
{
expect: '#',
send: `show license`,
delay: '1',
repeat: '1',
note: 'Verify license status',
},
{
expect: '#',
send: ` show version`,
delay: '3',
repeat: '1',
note: 'Verify version info',
},
]
/* ================= ROUTER LICENSE ================= */
case 'ROUTER_LICENSE':
return [
{
expect: '',
send: ``,
delay: '1',
repeat: '1',
note: 'Start session',
},
{
expect: '',
send: `enable`,
delay: '1',
repeat: '1',
note: 'Enter Enable mode',
},
{
expect: '#',
send: `configure terminal`,
delay: '1',
repeat: '1',
note: 'Enter Config mode',
},
{
expect: '#',
send: `interface ${portName ? portName : 'GigabitEthernet0/0'}`,
delay: '1',
repeat: '1',
note: 'Select management interface',
},
{
expect: '#',
send: `ip address ${address} 255.255.0.0`,
delay: '1',
repeat: '1',
note: 'Set IP Address',
},
{
expect: '#',
send: `no shutdown`,
delay: '1',
repeat: '1',
note: 'Up interface',
},
{
expect: '#',
send: `exit`,
delay: '1',
repeat: '1',
note: 'Exit interface',
},
{
expect: '#',
send: `ip route 0.0.0.0 0.0.0.0 ${gateway}`,
delay: '1',
repeat: '1',
note: 'Set default route',
},
{
expect: '#',
send: `end`,
delay: '1',
repeat: '1',
note: 'End config',
},
{
expect: '',
send: ``,
delay: '4',
repeat: '1',
note: '',
},
{
expect: '#',
send: `license install tftp://${tftpIp}/License/${fileName}`,
delay: '1',
repeat: '1',
note: 'Install license',
},
{
expect: '',
send: ``,
delay: '1',
repeat: '1',
note: '',
},
{
expect: '#',
send: `write memory`,
delay: '1',
repeat: '1',
note: 'Save config',
},
{
expect: '',
send: ``,
delay: '1',
repeat: '1',
note: '',
},
{
expect: '#',
send: `reload`,
delay: '1',
repeat: '1',
note: 'Reload router',
},
{
expect: '',
send: `yes`,
delay: '1',
repeat: '1',
note: 'Confirm reload',
},
{
expect: '',
send: ``,
delay: '1',
repeat: '1',
note: 'Confirm reload',
},
{
expect: '',
send: ``,
delay: '1',
repeat: '1',
note: 'Waiting for reboot...',
},
// --- PHẦN 4: VERIFY ---
{
expect: 'Press RETURN to get started!',
send: ``,
delay: '1',
repeat: '1',
note: 'Router is back online',
},
{
expect: '',
send: `enable`,
delay: '3',
repeat: '1',
note: 'Enable again',
},
{
expect: '#',
send: `show inventory`,
delay: '1',
repeat: '1',
note: '',
},
{
expect: '#',
send: `show license`,
delay: '1',
repeat: '1',
note: 'Verify license status',
},
{
expect: '#',
send: ` show version`,
delay: '3',
repeat: '1',
note: 'Verify version info',
},
]
default:
return []
}
}
export function parseLicenseReport(output: string) {
const summary = []
const imported = []
const exist = []
const failed = []
// lấy toàn bộ feature
const allFeatureRegex = /Feature:([a-z0-9_]+)/gi
let match
while ((match = allFeatureRegex.exec(output))) {
summary.push(match[1])
}
// split theo từng dòng install
const lines = output.split('\n')
for (let i = 0; i < lines.length; i++) {
const line = lines[i]
// success
const successMatch = line.match(/Feature:([a-z0-9_]+)\.\.\.Successful/i)
if (successMatch) {
imported.push(successMatch[1])
continue
}
// failed
const failedMatch = line.match(/Feature:([a-z0-9_]+)\.\.\.Failed:/i)
if (failedMatch) {
const feature = failedMatch[1]
// check duplicate ở dòng sau
const nextLine = lines[i + 1] || ''
if (/Duplicate license/i.test(nextLine)) {
exist.push(feature)
} else {
failed.push(feature)
}
}
}
return {
summary: [...summary],
imported: [...imported],
exist: [...exist],
failed: [...failed],
}
}
export async function checkStationActive(stationId: string): Promise<boolean> {
const station = await Station.find(stationId)
return station?.is_active || false
}
// Kiểm tra RAM total lớn hơn RAM mặc định
export function isRamSufficient(deviceRam: string, defaultRamLimit: string): boolean {
if (!defaultRamLimit || !deviceRam) return false
const parts = deviceRam.split('/')
if (parts.length === 0) return false
const totalRamStr = parts[0].trim() // lấy phần total (thường là trước dấu '/')
const totalRamKB = convertToKilobytes(totalRamStr)
const defaultRamKB = convertToKilobytes(defaultRamLimit)
return totalRamKB >= defaultRamKB
}
export function convertToKilobytes(input: string): number {
const trimmed = input.trim().toUpperCase()
const match = trimmed.match(/^([\d.]+)\s*(K|M|G|T)?B?$/)
if (!match) {
return trimmed ? Number.parseFloat(trimmed) : 0
}
const value = Number.parseFloat(match[1])
const unit = match[2] || 'K' // default to KB if no unit
const unitMultipliers: { [key: string]: number } = {
K: 1,
M: 1024,
G: 1024 * 1024,
T: 1024 * 1024 * 1024,
}
return Math.round(value * unitMultipliers[unit])
}
function escapeRegex(str: string) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
}
function keywordToRule(keyword: Keyword): KeywordRule {
let match: RegExp
switch (keyword.match_type) {
case 'exact':
match = new RegExp(`^${escapeRegex(keyword.name)}$`, 'i')
break
case 'contains':
default:
match = new RegExp(escapeRegex(keyword.name), 'i')
}
return {
id: `${keyword.name}`,
keywordId: keyword.id,
category: 'SPECIAL_KEYWORD',
match,
level: 'WARN',
message: `Type: ${keyword.type}`,
}
}
async function loadKeywordRules(log: string): Promise<KeywordRule[]> {
const keywords = await Keyword.query()
for (const keyword of keywords) {
if (log.toUpperCase().includes(keyword.name.toUpperCase())) {
return keywords.map(keywordToRule)
}
}
return []
}