1526 lines
37 KiB
TypeScript
1526 lines
37 KiB
TypeScript
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, '&').replace(/</g, '<').replace(/>/g, '>')
|
||
}
|
||
|
||
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 []
|
||
}
|