update physical test

This commit is contained in:
nguyentrungthat 2025-12-25 16:17:52 +07:00
parent ab7db61608
commit 2f484e19b6
8 changed files with 359 additions and 23 deletions

View File

@ -12,6 +12,7 @@ import {
LogStreamBuffer,
mapErrorsToRows,
mapToLineFormat,
normalizeInterface,
sendMessageToMail,
sleep,
TestSession,
@ -25,6 +26,7 @@ import Line from '#models/line'
import { ErrorRow, TestResult } from '../ultils/types.js'
import moment from 'moment'
import momentTZ from 'moment-timezone'
import { PhysicalPortTest } from './physical_test_service.js'
type Inventory = {
pid: string
@ -66,7 +68,9 @@ interface LineConfig {
output: string
textfsm: string
}[]
commands: string[]
ports: string[]
runningScenario: string
runningPhysical: boolean
// history: string
}
@ -111,7 +115,6 @@ export default class LineConnection {
public config: LineConfig
public readonly socketIO: any
private outputBuffer: string
private isRunningScript: boolean
private connecting: boolean
private waitingScenario: boolean
private outputInventory: string
@ -121,13 +124,14 @@ export default class LineConnection {
private listScenarios: number[]
public handleClearLine: () => void
private session: TestSession
private physicalTest: PhysicalPortTest
private outputPhysicalTest: string
constructor(config: LineConfig, socketIO: any, handleClearLine: () => void) {
this.config = config
this.socketIO = socketIO
this.client = new net.Socket()
this.outputBuffer = ''
this.isRunningScript = false
this.connecting = false
this.waitingScenario = false
this.outputInventory = ''
@ -147,6 +151,8 @@ export default class LineConnection {
this.listScenarios = []
this.session = new TestSession()
this.handleClearLine = handleClearLine
this.physicalTest = new PhysicalPortTest([])
this.outputPhysicalTest = ''
}
connect(timeoutMs = 5000) {
@ -182,13 +188,23 @@ export default class LineConnection {
const lines = this.bufferLog.push(data)
lines.forEach(this.handleLogLine)
let rawData = ''
if (this.isRunningScript) {
if (this.config.runningScenario) {
this.waitingScenario = true
this.outputBuffer += message
this.outputScenario += message
if (!this.config.inventory)
this.outputInventory = this.outputInventory.slice(-3000) + message
}
if (this.config.runningPhysical) {
this.outputPhysicalTest += message
const ports = this.physicalTest.handleLog(message)
if (ports?.length)
this.socketIO.emit('test_port_physical', {
stationId,
lineId: id,
data: ports,
})
}
if (data.toString().includes('More') || data.toString().includes('MORE'))
this.writeCommand(' ')
@ -209,7 +225,7 @@ export default class LineConnection {
stationId,
lineId: id,
data: message,
commands: this.config.commands,
ports: this.config.ports,
})
if (!this.config.inventory) {
setTimeout(() => {
@ -235,6 +251,7 @@ export default class LineConnection {
lineId: id,
error: '\r\n' + err.message + '\r\n',
})
this.endTesting()
resolve()
})
@ -256,6 +273,7 @@ export default class LineConnection {
// } else {
// this.retryConnect = 0
// }
this.endTesting()
})
this.client.on('timeout', () => {
@ -324,7 +342,7 @@ export default class LineConnection {
async runScript(script: Scenario, userName: string) {
if (!this.client || this.client.destroyed) {
console.log('Not connected')
this.isRunningScript = false
this.config.runningScenario = ''
this.socketIO.emit('running_scenario', {
stationId: this.config.stationId,
lineId: this.config.id,
@ -333,7 +351,7 @@ export default class LineConnection {
this.outputBuffer = ''
return
}
if (this.isRunningScript) {
if (this.config.runningScenario || this.config.runningPhysical) {
console.log('Script already running')
return
}
@ -341,7 +359,7 @@ export default class LineConnection {
console.log(
`Run scenario "${script?.title}" to line ${this.config.lineNumber} of ${this.config.stationName}`
)
this.isRunningScript = true
this.config.runningScenario = ''
this.socketIO.emit('running_scenario', {
stationId: this.config.stationId,
lineId: this.config.id,
@ -378,7 +396,7 @@ export default class LineConnection {
return new Promise((resolve, reject) => {
const timeoutTimer = setTimeout(
() => {
this.isRunningScript = false
this.config.runningScenario = ''
this.socketIO.emit('running_scenario', {
stationId: this.config.stationId,
lineId: this.config.id,
@ -425,7 +443,7 @@ export default class LineConnection {
}, 5000)
return
} else clearTimeout(timeoutTimer)
this.isRunningScript = false
this.config.runningScenario = ''
this.socketIO.emit('running_scenario', {
stationId: this.config.stationId,
lineId: this.config.id,
@ -936,4 +954,80 @@ export default class LineConnection {
const note = `-------[ATC]-[${dataFormat}]-------\nLicense: ${licenses.join(', ')}\nSummary: ${data?.summary || ''}\nIssues:\n${data.issues?.length ? `- ` + data.issues.join(`\n- `) : ''}\n\n`
await updateNoteToERP(sn, note)
}
async runPhysicalTest() {
if (this.config.runningPhysical) {
console.log('Running physical test')
return
}
this.config.runningPhysical = true
this.config.runningScenario = 'Physical Test'
this.socketIO.emit('running_scenario', {
stationId: this.config.stationId,
lineId: this.config.id,
title: 'Physical Test',
physical: true,
})
const listPorts = await this.getPorts()
this.socketIO.emit('running_scenario', {
stationId: this.config.stationId,
lineId: this.config.id,
title: 'Physical Test',
physical: true,
ports: listPorts,
})
if (listPorts.length === 0) {
console.log('End physical test')
this.endTesting()
return
}
this.physicalTest.start(listPorts)
const interval = setInterval(async () => {
if (!this.physicalTest.done) {
const result = this.physicalTest.getResult()
// console.warn('⚠️ Missing ports:', result.missingPorts)
} else {
clearInterval(interval)
this.endTesting()
}
}, 10000)
}
endTesting() {
this.physicalTest.done = true
this.config.runningPhysical = false
this.config.runningScenario = ''
this.outputBuffer = ''
this.outputScenario = ''
this.outputPhysicalTest = ''
this.config.ports = []
this.socketIO.emit('running_scenario', {
stationId: this.config.stationId,
lineId: this.config.id,
title: '',
})
}
async getPorts(): Promise<string[]> {
this.writeCommand(' show power inline\r\n')
this.writeCommand(' \r\n')
await this.sleep(3000)
const statusOutput = this.outputPhysicalTest
this.outputPhysicalTest = ''
const lines = statusOutput.split('\n')
const ports = []
for (const line of lines) {
// Match: "Gi0/1 is up, line protocol is up"
const match = line.match(/^(\S+)\s+\S+\s+(on|off)/i)
if (match) {
const name = match[1]
ports.push(normalizeInterface(name))
}
}
this.config.ports = [...new Set(ports)]
return [...new Set(ports)]
}
}

View File

@ -0,0 +1,97 @@
import { normalizeInterface } from '../ultils/helper.js'
import { PhysicalTestResult, PortState } from '../ultils/types.js'
const LINK_UPDOWN_REGEX =
/Interface\s+((?:FastEthernet|GigabitEthernet|TenGigabitEthernet|TwentyFiveGigE|FortyGigabitEthernet|HundredGigE|Ethernet|Port-channel|Fa|Gi|Te|Hu|Eth)[\w\/.-]+),\s+changed state to\s+(up|down)/i
export class PhysicalPortTest {
public ports = new Map<string, PortState>()
private expectedPorts: string[]
public done = false
constructor(expectedPorts: string[]) {
this.expectedPorts = expectedPorts
expectedPorts.forEach((p) => {
this.ports.set(normalizeInterface(p), {
name: normalizeInterface(p),
tested: false,
})
})
}
start(expectedPorts: string[]) {
this.ports.clear()
this.expectedPorts = expectedPorts
this.done = false
expectedPorts.forEach((p) => {
this.ports.set(normalizeInterface(p), {
name: normalizeInterface(p),
tested: false,
})
})
// this.connection.writeCommand('terminal length 0')
// this.connection.writeCommand('terminal monitor')
// this.connection.onLog((line) => {
// this.handleLog(line);
// });
}
handleLog(line: string) {
const match = line.match(LINK_UPDOWN_REGEX)
if (!match) return
const rawIface = match[1]
const state = match[2] as 'up' | 'down'
const iface = normalizeInterface(rawIface)
const port = this.ports.get(iface)
if (!port) return
// tránh update trùng state liên tiếp
if (port.lastState === state) return
port.lastState = state
port.lastSeen = new Date()
// chỉ cần UP 1 lần là pass
if (state === 'up' && !port.tested) {
port.tested = true
this.checkDone()
}
return this.getTestedPorts()
}
getTestedPorts(): string[] {
return Array.from(this.ports.values())
.filter((p) => p.tested)
.map((p) => p.name)
.sort()
}
private checkDone() {
const testedCount = [...this.ports.values()].filter((p) => p.tested).length
if (testedCount === this.expectedPorts.length) {
this.done = true
this.onDone()
}
}
onDone() {
this.ports.clear()
console.log('✅ Physical Test DONE')
}
getResult(): PhysicalTestResult {
const tested = [...this.ports.values()].filter((p) => p.tested)
const missing = [...this.ports.values()].filter((p) => !p.tested).map((p) => p.name)
return {
expected: this.expectedPorts.length,
tested: tested.length,
missingPorts: missing,
status: this.done ? 'DONE' : 'RUNNING',
}
}
}

View File

@ -667,3 +667,12 @@ export async function updateNoteToERP(sn: string, note: string) {
console.log(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')
}

View File

@ -55,3 +55,17 @@ export interface ErrorRow {
log: string
count: number
}
export interface PortState {
name: string
tested: boolean
lastState?: 'up' | 'down'
lastSeen?: Date
}
export interface PhysicalTestResult {
expected: number
tested: number
missingPorts: string[]
status: 'RUNNING' | 'DONE' | 'WARNING'
}

View File

@ -133,7 +133,14 @@ export class WebSocketIo {
setTimeout(() => {
io.to(socket.id).emit(
'init',
Array.from(this.lineMap.values()).map((el) => el?.config || {})
Array.from(this.lineMap.values()).map((el) => {
const config = el?.config || {}
if (config.status !== 'connected') {
config.runningScenario = ''
config.runningPhysical = false
}
return config
})
)
}, 500)
@ -606,6 +613,28 @@ export class WebSocketIo {
{}
)
})
socket.on('run_physical_test', async (data) => {
const { stationId, lineId } = data
await this.handleLineOperation(
io,
stationId,
[lineId],
async (lineCon) => lineCon.runPhysicalTest(),
{}
)
})
socket.on('end_run_physical_test', async (data) => {
const { stationId, lineId } = data
await this.handleLineOperation(
io,
stationId,
[lineId],
async (lineCon) => lineCon.endTesting(),
{}
)
})
})
socketServer.listen(SOCKET_IO_PORT, () => {
@ -645,8 +674,10 @@ export class WebSocketIo {
userEmailOpenCLI: '',
userOpenCLI: '',
data: [],
commands: [],
ports: [],
inventory: inventory,
runningPhysical: false,
runningScenario: '',
},
socket,
async () => {

View File

@ -383,7 +383,11 @@ function App() {
setTimeout(() => {
updateValueLineStation(
data?.lineId,
{ runningScenario: data?.title || "" },
{
runningScenario: data?.title || "",
runningPhysical: data?.physical || false,
ports: data?.ports || [],
},
data?.stationId
);
}, 100);

View File

@ -78,6 +78,7 @@ const ModalTerminal = ({
const [isDisable, setIsDisable] = useState<boolean>(false);
const [isDisableTicket, setIsDisableTicket] = useState<boolean>(false);
const [listPorts, setListPorts] = useState<SwitchPortsProps[]>([]);
const [listPortsPhysical, setListPortsPhysical] = useState<string[]>([]);
const [latestTicket, setLatestTicket] = useState<TDataTicket>(INIT_TICKET);
const [dataTicket, setDataTicket] = useState<TDataTicket>(INIT_TICKET);
const [valueBaud, setValueBaud] = useState<string>("");
@ -130,8 +131,14 @@ const ModalTerminal = ({
if (data?.ports && data?.ports.length > 0)
setListPorts(data?.ports || []);
});
socket?.on("test_port_physical", (data) => {
if (data.stationId !== stationItem?.id) return;
if (data?.data && data?.data.length > 0)
setListPortsPhysical(data?.data || []);
});
return () => {
socket?.off("switch_ports_status");
socket?.off("test_port_physical");
};
}, [socket, stationItem]);
@ -696,7 +703,7 @@ const ModalTerminal = ({
copiedColor="violet"
/>
</Flex>
<Flex mt="4px">
<Flex mt="4px" display={line?.runningPhysical ? "none" : ""}>
<Text size="md" mr={"6px"} fw={"bold"}>
IOS:
</Text>
@ -711,7 +718,7 @@ const ModalTerminal = ({
</Text>
</Flex>
</Box>
<Flex>
<Flex display={line?.runningPhysical ? "none" : ""}>
<Text size="md" mr={"sm"} fw={"bold"}>
License:
</Text>
@ -727,13 +734,13 @@ const ModalTerminal = ({
: ""}
</Text>
</Flex>
<Flex>
<Flex display={line?.runningPhysical ? "none" : ""}>
<Text size="md" mr={"sm"} fw={"bold"}>
Sh env/module:
</Text>
<Text size="md">{""}</Text>
</Flex>
<Flex>
<Flex display={line?.runningPhysical ? "none" : ""}>
<Text size="md" mr={"sm"} fw={"bold"}>
Mem/Flash:
</Text>
@ -746,7 +753,7 @@ const ModalTerminal = ({
: ""}
</Text>
</Flex>
<Box>
<Box display={line?.runningPhysical ? "none" : ""}>
<Text size="md" mr={"sm"} fw={"bold"}>
Warning from test report: AI
</Text>
@ -762,6 +769,65 @@ const ModalTerminal = ({
/>
</Box>
</Box>
<Box display={line?.runningPhysical ? "" : "none"}>
<fieldset
style={
{
// width: "300px",
}
}
>
<legend>
<Flex>
<Text>
List ports{" "}
{line?.ports?.length
? `(${listPortsPhysical.length}/${line?.ports?.length})`
: ""}
</Text>
<Button
fw={400}
disabled={isDisable}
variant="outline"
color="green"
miw={"80px"}
h={"24px"}
size="xs"
fz={"xs"}
ms={"8px"}
onClick={() => {
socket?.emit("end_run_physical_test", {
lineId: line?.id,
stationId: Number(stationItem?.id),
});
setIsDisable(true);
setTimeout(() => {
setIsDisable(false);
}, 2000);
}}
>
Done/End
</Button>
</Flex>
</legend>
<ScrollArea h={"300px"} p={"4px"}>
<Flex wrap={"wrap"} gap={"xs"}>
{line?.ports?.map((port, i) => (
<Text
key={i}
c={
listPortsPhysical?.includes(port)
? "#19bc4f"
: "#dedede"
}
>
{port}
</Text>
))}
</Flex>
</ScrollArea>
</fieldset>
</Box>
<Box
style={{
display: "flex",
@ -902,10 +968,11 @@ const ModalTerminal = ({
line_id={Number(line?.id)}
line={line}
station_id={Number(stationItem?.id)}
isDisabled={
typeof line?.userOpenCLI !== "undefined" &&
line?.userOpenCLI !== user?.userName
}
// isDisabled={
// typeof line?.userOpenCLI !== "undefined" &&
// line?.userOpenCLI !== user?.userName
// }
isDisabled={line?.runningScenario ? true : false}
line_status={line?.status || ""}
loadingClearTerminal={line?.loadingClearTerminal}
isClearKeepScrollBack={isClearKeepScrollBack}
@ -1040,6 +1107,25 @@ const ModalTerminal = ({
</Box>
</Menu.Dropdown>
</Menu>
<Button
disabled={isDisable || line?.runningPhysical}
fw={400}
variant="filled"
color="green"
size="xs"
onClick={() => {
socket?.emit("run_physical_test", {
lineId: line?.id,
stationId: Number(stationItem?.id),
});
setIsDisable(true);
setTimeout(() => {
setIsDisable(false);
}, 5000);
}}
>
Physical Test
</Button>
<Button
disabled={true}
fw={400}

View File

@ -97,7 +97,7 @@ export type TLine = {
issue: string[];
};
};
commands?: string[];
ports?: string[];
interface?: string;
baud?: number;
tickets?: TDataTicket[];
@ -105,6 +105,7 @@ export type TLine = {
runningScenario?: string;
scenario?: IScenario;
loadingClearTerminal?: boolean;
runningPhysical?: boolean;
};
export type TUser = {