Update config ram and check ram

This commit is contained in:
nguyentrungthat 2026-01-26 14:45:04 +07:00
parent 20ef2732da
commit 0ce936b685
9 changed files with 802 additions and 7 deletions

View File

@ -0,0 +1,125 @@
import ConfigRam from '#models/config_ram'
import type { HttpContext } from '@adonisjs/core/http'
export default class ConfigRamController {
/**
* Display a list of resource
*/
async get({}: HttpContext) {
const configRams = await ConfigRam.all()
return { status: true, data: configRams }
}
/**
* Display form to create a new record
*/
async create({ auth, request, response }: HttpContext) {
let payload = request.only([...Array.from(ConfigRam.$columnsDefinitions.keys())])
try {
// Check exist models
const inputModels: string[] = payload.models.map((s: string) => s.trim().toUpperCase())
const existedConfigs = await ConfigRam.query().select('id', 'models')
const duplicatedModels: string[] = []
for (const sc of existedConfigs) {
const scModels: string[] = JSON.parse(sc.models || '[]').map((s: string) =>
s.trim().toUpperCase()
)
for (const s of inputModels) {
if (scModels.includes(s)) {
duplicatedModels.push(s)
}
}
}
if (duplicatedModels.length) {
return response.badRequest({
status: false,
message: 'Models already exists in another config',
duplicatedModels: [...new Set(duplicatedModels)],
})
}
const configRam = await ConfigRam.create({
...payload,
models: JSON.stringify(payload.models),
})
return response.created({
status: true,
message: 'Config created successfully',
data: configRam,
})
} catch (error) {
return response.badRequest({ error: error, message: 'Config create failed', status: false })
}
}
async update({ request, response, auth }: HttpContext) {
let payload = request.only(
Array.from(ConfigRam.$columnsDefinitions.keys()).filter(
(f) => f !== 'created_at' && f !== 'updated_at'
)
)
try {
const configRam = await ConfigRam.find(request.body().id)
if (!configRam) {
return response.status(404).json({ message: 'Config not found' })
}
// Check exist models
const inputModels: string[] = payload.models.map((s: string) => s.trim().toUpperCase())
const existedConfigRams = await ConfigRam.query().select('id', 'models')
const duplicatedModels: string[] = []
for (const sc of existedConfigRams) {
if (sc.id === configRam.id) continue
const scModels: string[] = JSON.parse(sc.models || '[]').map((s: string) =>
s.trim().toUpperCase()
)
for (const s of inputModels) {
if (scModels.includes(s)) {
duplicatedModels.push(s)
}
}
}
if (duplicatedModels.length) {
return response.badRequest({
status: false,
message: 'Models already exists in another config',
duplicatedModels: [...new Set(duplicatedModels)],
})
}
Object.assign(configRam, { ...payload, models: JSON.stringify(payload.models) })
await configRam.save()
return response.ok({ status: true, message: 'Config update successfully', data: configRam })
} catch (error) {
return response.badRequest({ error: error, message: 'Config update failed', status: false })
}
}
/**
* Delete record
*/
async delete({ auth, request, response }: HttpContext) {
try {
const configRam = await ConfigRam.find(request.body().id)
if (!configRam) {
return response.status(404).json({ message: 'Config not found' })
}
// Delete the configRam
await configRam.delete()
return response.ok({
status: true,
message: 'Config Ram delete successfully',
data: configRam,
})
} catch (error) {
return response.badRequest({
error: error,
message: 'Config Ram delete failed',
status: false,
})
}
}
}

View File

@ -0,0 +1,22 @@
import { DateTime } from 'luxon'
import { BaseModel, column } from '@adonisjs/lucid/orm'
export default class ConfigRam extends BaseModel {
@column({ isPrimary: true })
declare id: number
@column()
declare models: string
@column()
declare ram: string
@column()
declare flash: string
@column.dateTime({ autoCreate: true })
declare createdAt: DateTime
@column.dateTime({ autoCreate: true, autoUpdate: true })
declare updatedAt: DateTime
}

View File

@ -6,8 +6,10 @@ import {
buildBody, buildBody,
classifyLog, classifyLog,
cleanData, cleanData,
detectConfigRamByModel,
detectScenarioByModel, detectScenarioByModel,
escapeHtml, escapeHtml,
isRamSufficient,
isValidJson, isValidJson,
LogStreamBuffer, LogStreamBuffer,
mapErrorsToRows, mapErrorsToRows,
@ -29,6 +31,7 @@ import momentTZ from 'moment-timezone'
import { PhysicalPortTest } from './physical_test_service.js' import { PhysicalPortTest } from './physical_test_service.js'
import Station from '#models/station' import Station from '#models/station'
import IosLicenseController from '#controllers/ios_license_controller' import IosLicenseController from '#controllers/ios_license_controller'
import ConfigRam from '#models/config_ram'
type Inventory = { type Inventory = {
pid: string pid: string
@ -520,6 +523,9 @@ export default class LineConnection {
this.config.inventory = this.config.inventory this.config.inventory = this.config.inventory
? { ...this.config.inventory, ...dataVer } ? { ...this.config.inventory, ...dataVer }
: dataVer : dataVer
if (pid && dataVer?.MEMORY) {
await this.checkConfigRam(dataVer?.MEMORY, pid, cleanData(item.output))
}
} }
if ( if (
item.command?.trim()?.includes('show env') || item.command?.trim()?.includes('show env') ||
@ -1591,4 +1597,24 @@ ${log}
} }
return '' return ''
} }
async checkConfigRam(mem: string, pid: string, output: string) {
const configRam = await detectConfigRamByModel(pid)
if (configRam) {
const isWarningRAM = isRamSufficient(mem, configRam.ram)
if (isWarningRAM) {
const subject = `[ATC] - [${this.config.stationName} - Line: ${this.config.lineNumber}] - Warning RAM 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: <b>${mem + ' bytes'} (<span style="color: red;">default: ${configRam.ram}</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: red;">${mem}</span>`)}</span></div>
`
await sendMessageToMail(subject, body)
}
}
}
} }

View File

@ -7,15 +7,16 @@ import { ErrorRow, LogRule, ParsedLog, TestError, TestResult } from './types.js'
import axios from 'axios' import axios from 'axios'
import moment from 'moment' import moment from 'moment'
import Station from '#models/station' import Station from '#models/station'
import ConfigRam from '#models/config_ram'
const mailTo = 'andrew.ng@apactech.io' const mailTo = 'andrew.ng@apactech.io'
const mailCC = [ // const mailCC = [
'ips@ipsupply.com.au', // 'ips@ipsupply.com.au',
'kay@ipsupply.com.au', // 'kay@ipsupply.com.au',
'joseph@apactech.io', // 'joseph@apactech.io',
'kiet.phan@apactech.io', // 'kiet.phan@apactech.io',
] // ]
// const mailCC = '' const mailCC = ''
type DetectAI = { type DetectAI = {
status: string[] status: string[]
@ -173,6 +174,8 @@ export function mapToLineFormat(input: InputData) {
sn: '', sn: '',
ios: '', ios: '',
mac: '', mac: '',
ram: '',
flash: '',
license: [], license: [],
issues: ['No data'], issues: ['No data'],
summary: '', summary: '',
@ -182,6 +185,8 @@ export function mapToLineFormat(input: InputData) {
// MAC // MAC
let mac = '' let mac = ''
let ios = '' let ios = ''
let ram = ''
let flash = ''
const showVersion = input.data?.find( const showVersion = input.data?.find(
(d) => (d) =>
d.command === 'show version' || d.command === 'show version' ||
@ -195,6 +200,12 @@ export function mapToLineFormat(input: InputData) {
if (showVersion?.textfsm?.[0]?.SOFTWARE_IMAGE) { if (showVersion?.textfsm?.[0]?.SOFTWARE_IMAGE) {
ios = showVersion.textfsm[0].SOFTWARE_IMAGE + ' ' + (showVersion?.textfsm?.[0]?.VERSION || '') 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 // License
const dataLicense = input.data?.find((comm) => comm.command?.trim() === 'show license') const dataLicense = input.data?.find((comm) => comm.command?.trim() === 'show license')
@ -225,6 +236,8 @@ export function mapToLineFormat(input: InputData) {
sn, sn,
ios, ios,
mac, mac,
ram,
flash,
license, license,
issues, issues,
summary, summary,
@ -343,6 +356,32 @@ export const detectScenarioByModel = async (model: string, listScenarios: number
return matched?.scenario ? matched?.scenario : listScenarios.length === 0 ? scenarioDefault : null 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 { export function classifyLog(line: string): ParsedLog {
if (/System Bootstrap|IOS XE Software|Booting/.test(line)) return { raw: line, category: 'BOOT' } if (/System Bootstrap|IOS XE Software|Booting/.test(line)) return { raw: line, category: 'BOOT' }
@ -1403,3 +1442,37 @@ export async function checkStationActive(stationId: string): Promise<boolean> {
const station = await Station.find(stationId) const station = await Station.find(stationId)
return station?.is_active || false 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])
}

View File

@ -0,0 +1,19 @@
import { BaseSchema } from '@adonisjs/lucid/schema'
export default class extends BaseSchema {
protected tableName = 'config_rams'
async up() {
this.schema.createTable(this.tableName, (table) => {
table.increments('id')
table.text('models').notNullable()
table.string('ram')
table.string('flash')
table.timestamps()
})
}
async down() {
this.schema.dropTable(this.tableName)
}
}

View File

@ -112,3 +112,12 @@ router
router.get('/license', '#controllers/ios_license_controller.getLicense') router.get('/license', '#controllers/ios_license_controller.getLicense')
}) })
.prefix('/api') .prefix('/api')
router
.group(() => {
router.get('/', '#controllers/config_ram_controller.get')
router.post('/create', '#controllers/config_ram_controller.create')
router.post('/update', '#controllers/config_ram_controller.update')
router.post('/delete', '#controllers/config_ram_controller.delete')
})
.prefix('/api/config-ram')

View File

@ -48,6 +48,7 @@ import type { Socket } from "socket.io-client";
import ModalHistory from "./Modal/ModalHistory"; import ModalHistory from "./Modal/ModalHistory";
import ModalConfig from "./Modal/ModalConfig"; import ModalConfig from "./Modal/ModalConfig";
import DrawerScenario from "./Modal/ModalScenario"; import DrawerScenario from "./Modal/ModalScenario";
import ModalConfigRamFlash from "./Modal/ModalConfigRamFlash";
interface DraggableTabsProps { interface DraggableTabsProps {
tabsData: TStation[]; tabsData: TStation[];
@ -151,6 +152,7 @@ export default function DraggableTabs({
const [isHistoryModalOpen, setIsHistoryModalOpen] = useState<boolean>(false); const [isHistoryModalOpen, setIsHistoryModalOpen] = useState<boolean>(false);
const [openConfig, setOpenConfig] = useState<boolean>(false); const [openConfig, setOpenConfig] = useState<boolean>(false);
const [openDrawerScenario, setOpenDrawerScenario] = useState<boolean>(false); const [openDrawerScenario, setOpenDrawerScenario] = useState<boolean>(false);
const [openConfigRam, setOpenConfigRam] = useState<boolean>(false);
const sensors = useSensors(useSensor(PointerSensor)); const sensors = useSensors(useSensor(PointerSensor));
@ -271,6 +273,15 @@ export default function DraggableTabs({
> >
Scenario Scenario
</Button> </Button>
<Button
color="cyan"
variant="filled"
size="xs"
leftSection={<IconSettings size={16} />}
onClick={() => setOpenConfigRam(true)}
>
Config Ram
</Button>
</Flex> </Flex>
<Tabs.List className={classes.list}> <Tabs.List className={classes.list}>
<SortableContext <SortableContext
@ -414,6 +425,11 @@ export default function DraggableTabs({
listBrands={listBrands} listBrands={listBrands}
listCategories={listCategories} listCategories={listCategories}
/> />
<ModalConfigRamFlash
opened={openConfigRam}
onClose={() => setOpenConfigRam(false)}
/>
</DndContext> </DndContext>
); );
} }

View File

@ -0,0 +1,498 @@
import { useEffect, useState } from "react";
import {
Modal,
Button,
Grid,
ScrollArea,
Table,
Box,
Flex,
TextInput,
TagsInput,
Badge,
} from "@mantine/core";
import axios from "axios";
import type { ConfigRam } from "../../untils/types";
import { IconX } from "@tabler/icons-react";
import { notifications } from "@mantine/notifications";
import DialogConfirm from "../DialogConfirm";
const apiUrl = import.meta.env.VITE_BACKEND_URL;
interface Props {
opened: boolean;
onClose: () => void;
}
export default function ModalConfigRamFlash({ opened, onClose }: Props) {
const [configs, setConfigs] = useState<ConfigRam[]>([]);
const [dataConfig, setDataConfig] = useState<ConfigRam | null>(null);
const [newConfig, setNewConfig] = useState<ConfigRam>({
flash: "",
models: [],
ram: "",
});
const [inputModels, setInputModels] = useState<string>("");
const [disabled, setDisabled] = useState<boolean>(false);
const [openConfirm, setOpenConfirm] = useState<boolean>(false);
// get list config
const getListConfig = async () => {
try {
const response = await axios.get(apiUrl + "api/config-ram");
if (response.data.data && Array.isArray(response.data.data)) {
setConfigs(
response.data.data.map((item: ConfigRam) => ({
...item,
models:
typeof item.models === "string"
? JSON.parse(item.models)
: item.models,
}))
);
}
} catch (error) {
console.log("Error get config", error);
}
};
useEffect(() => {
if (opened) getListConfig();
}, [opened]);
const handleChange = (field: keyof ConfigRam, value: string | string[]) => {
if (!dataConfig) return;
setDataConfig({
...dataConfig,
[field]: value,
});
};
const handleAdd = async () => {
setDisabled(true);
try {
const response = await axios.post(apiUrl + `api/config-ram/create`, {
...newConfig,
models: newConfig.models,
});
// update local list
setConfigs((prev) => [
...prev,
{
...response.data.data,
models:
typeof response.data.data.models === "string"
? JSON.parse(response.data.data.models)
: response.data.data.models,
},
]);
notifications.show({
title: "Success",
message: response.data.message,
color: "green",
});
setNewConfig({
flash: "",
models: [],
ram: "",
});
setDisabled(false);
} catch (error) {
console.log("Error save config", error);
setDisabled(false);
if (axios.isAxiosError(error)) {
notifications.show({
title: "Error",
message: error?.response?.data?.message,
color: "red",
});
} else {
notifications.show({
title: "Error",
message: "Create fail, please try again!",
color: "red",
});
}
}
};
const handleSave = async () => {
setDisabled(true);
if (!dataConfig) return;
try {
const response = await axios.post(apiUrl + `api/config-ram/update`, {
...dataConfig,
models: dataConfig.models,
});
// update local list
setConfigs((prev) =>
prev.map((item) => (item.id === dataConfig.id ? dataConfig : item))
);
setDataConfig(null);
setDisabled(false);
notifications.show({
title: "Success",
message: response.data.message,
color: "green",
});
} catch (error) {
console.log("Error save config", error);
setDisabled(false);
if (axios.isAxiosError(error)) {
notifications.show({
title: "Error",
message: error?.response?.data?.message,
color: "red",
});
} else {
notifications.show({
title: "Error",
message: "Update fail, please try again!",
color: "red",
});
}
}
};
const handleDelete = async () => {
setDisabled(true);
if (!dataConfig) return;
try {
const response = await axios.post(apiUrl + `api/config-ram/delete`, {
...dataConfig,
});
// update local list
setConfigs((prev) => prev.filter((item) => item.id !== dataConfig.id));
setDataConfig(null);
setDisabled(false);
notifications.show({
title: "Success",
message: response.data.message,
color: "green",
});
} catch (error) {
console.log("Error save config", error);
setDisabled(false);
if (axios.isAxiosError(error)) {
notifications.show({
title: "Error",
message: error?.response?.data?.message,
color: "red",
});
} else {
notifications.show({
title: "Error",
message: "Delete fail, please try again!",
color: "red",
});
}
}
};
const handleCancel = () => {
setDataConfig(null);
};
const filteredConfigs = () => {
const listConfigs = [...configs].filter((el) => {
const models: string[] = el.models || [];
if (!models.length) return false;
return models.some((model) =>
model.toLowerCase().includes(inputModels.trim().toLowerCase())
);
});
return listConfigs;
};
return (
<Modal
size={"70%"}
style={{ position: "absolute", left: 0 }}
opened={opened}
onClose={onClose}
title="Config RAM Flash"
>
<Grid>
<Grid.Col span={12}>
<Box style={{ width: "300px", marginBottom: "10px" }}>
<TextInput
autoCapitalize="on"
label="Search model"
placeholder="Enter Model"
value={inputModels}
onChange={(e) => setInputModels(e.currentTarget.value)}
size="xs"
rightSection={
inputModels ? (
<IconX
size={14}
style={{ cursor: "pointer" }}
onClick={() => setInputModels("")}
/>
) : null
}
rightSectionPointerEvents="auto"
/>
</Box>
<hr />
<Flex
align={"center"}
justify={"space-between"}
style={{ marginBottom: "10px", marginTop: "10px" }}
>
<Flex align={"center"} gap={10}>
<TagsInput
autoCapitalize="words"
style={{ width: "500px" }}
label="Models"
placeholder="Enter models"
data={[]}
value={newConfig?.models || []}
onChange={(value: string[]) =>
setNewConfig({
...newConfig,
models: value.map((v) => v.toUpperCase()),
})
}
size="xs"
/>
<TextInput
label="Ram"
placeholder="Enter ram"
style={{ width: "200px" }}
value={newConfig?.ram}
onChange={(e) =>
setNewConfig({
...newConfig,
ram: e.currentTarget.value.toUpperCase(),
})
}
size="xs"
/>
<TextInput
autoCapitalize="characters"
label="Flash"
placeholder="Enter flash"
style={{ width: "200px" }}
value={newConfig?.flash}
onChange={(e) =>
setNewConfig({
...newConfig,
flash: e.currentTarget.value.toUpperCase(),
})
}
size="xs"
/>
</Flex>
<Button
disabled={
disabled || newConfig.models.length === 0 || !newConfig.ram
}
size="xs"
color="green"
onClick={() => handleAdd()}
variant="filled"
>
Add
</Button>
</Flex>
<Box>
<ScrollArea
h={"calc(75vh - 150px)"}
style={{ marginBottom: "20px" }}
>
<Table
stickyHeader
stickyHeaderOffset={-1}
striped
highlightOnHover
withRowBorders
withTableBorder
withColumnBorders
>
<Table.Thead style={{ zIndex: 100 }}>
<Table.Tr>
<Table.Th style={{ textAlign: "center" }}>Models</Table.Th>
<Table.Th w={150} style={{ textAlign: "center" }}>
Ram
</Table.Th>
<Table.Th w={150} style={{ textAlign: "center" }}>
Flash
</Table.Th>
<Table.Th w={200} style={{ textAlign: "center" }}>
Action
</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{filteredConfigs().length > 0 ? (
filteredConfigs().map((element) => {
const isEditing =
dataConfig?.id === element.id && !openConfirm;
return (
<Table.Tr key={element.id}>
{/* MODELS */}
<Table.Td>
{" "}
<Flex wrap={"wrap"} gap={"4px"}>
{isEditing ? (
<TagsInput
w={"100%"}
label=""
placeholder="Enter models"
data={[]}
value={dataConfig?.models || []}
onChange={(value: string[]) =>
handleChange(
"models",
value.map((v) => v.toUpperCase())
)
}
/>
) : element.models ? (
element.models?.map((el: string, i: number) => (
<Badge
key={i}
size="md"
color="orange"
variant="dot"
>
{el}
</Badge>
))
) : (
""
)}
</Flex>
</Table.Td>
{/* RAM */}
<Table.Td style={{ textAlign: "center" }}>
{isEditing ? (
<TextInput
value={dataConfig?.ram}
onChange={(e) =>
handleChange(
"ram",
e.currentTarget.value.toUpperCase()
)
}
size="xs"
/>
) : (
element.ram
)}
</Table.Td>
{/* FLASH */}
<Table.Td style={{ textAlign: "center" }}>
{isEditing ? (
<TextInput
value={dataConfig?.flash}
onChange={(e) =>
handleChange(
"flash",
e.currentTarget.value.toUpperCase()
)
}
size="xs"
/>
) : (
element.flash
)}
</Table.Td>
{/* ACTION */}
<Table.Td>
<Flex gap={10} justify="center">
{isEditing ? (
<>
<Button
disabled={disabled}
size="xs"
color="green"
onClick={handleSave}
variant="outline"
>
Save
</Button>
<Button
disabled={disabled}
size="xs"
color="gray"
onClick={handleCancel}
variant="outline"
>
Cancel
</Button>
</>
) : (
<>
<Button
disabled={disabled}
size="xs"
onClick={() =>
setDataConfig({ ...element })
}
variant="outline"
>
Edit
</Button>
<Button
disabled={disabled}
size="xs"
color={"red"}
onClick={() => {
setDataConfig({ ...element });
setOpenConfirm(true);
}}
variant="outline"
>
Delete
</Button>
</>
)}
</Flex>
</Table.Td>
</Table.Tr>
);
})
) : (
<Table.Tr>
<Table.Td colSpan={4} style={{ textAlign: "center" }}>
No configurations found.
</Table.Td>
</Table.Tr>
)}
</Table.Tbody>
</Table>
</ScrollArea>
</Box>
</Grid.Col>
</Grid>
<DialogConfirm
opened={openConfirm}
close={() => {
setDataConfig(null);
setOpenConfirm(false);
}}
message={"Are you sure delete this config?"}
handle={() => {
setOpenConfirm(false);
handleDelete();
}}
centered={true}
/>
</Modal>
);
}

View File

@ -281,3 +281,10 @@ export type FileInfo = {
fileSize: number; fileSize: number;
dateModify: string; dateModify: string;
}; };
export type ConfigRam = {
id?: number;
models: string[];
ram: string;
flash: string;
};