ATC_SIMPLE/BACKEND/app/services/switch_connection.ts

362 lines
9.8 KiB
TypeScript

import net from 'node:net'
type PromptCallback = {
prompt: string
callback: (data: string) => void
}
type PortInfo = {
name: string
status: string
poe: string
}
interface SwitchControllerOptions {
host: string
port?: number
username: string
password: string
onData?: (data?: PortInfo[][], status?: string) => void
keep_connect?: boolean
}
export default class SwitchController {
private host: string
private port: number
private username: string
private password: string
private onData: (data?: PortInfo[][], status?: string) => void
private socket: net.Socket
public status: 'CONNECTED' | 'DISCONNECTED'
public buffer: string
private promptCallbacks: PromptCallback[]
public ports: PortInfo[]
public portGroups: PortInfo[][]
private isEnable: boolean
private retryConnect: number
constructor({ host, port = 23, username, password, onData }: SwitchControllerOptions) {
this.host = host
this.port = port
this.username = username
this.password = password
this.onData = onData || (() => {})
this.socket = new net.Socket()
this.status = 'DISCONNECTED'
this.buffer = ''
this.promptCallbacks = []
this.ports = []
this.portGroups = []
this.isEnable = false
this.retryConnect = 0
}
private sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms))
}
private _handleData(data: string) {
this.buffer += data
if (this.promptCallbacks.length > 0) {
const { prompt, callback } = this.promptCallbacks[0]
if (this.buffer.includes(prompt)) {
this.promptCallbacks.shift()?.callback(this.buffer)
this.buffer = ''
}
}
}
private async _handleClose(err: boolean) {
console.log('[SWITCH CONNECTION CLOSE]', err)
this.status = 'DISCONNECTED'
this.isEnable = false
this.onData(this.portGroups, this.status)
if (this.retryConnect <= 5) {
await this.sleep(15000)
console.log('Retry connect times', this.retryConnect)
this.retryConnect += 1
await this.reconnect()
}
}
private _handleError(err: Error & { code?: string }) {
console.log('[SWITCH CONNECTION ERROR]:', err.message)
this.onData(this.portGroups, this.status)
}
private _handleTimeout() {
this.onData(this.portGroups, this.status)
}
private _waitFor(prompt: string, timeout = 5000): Promise<string> {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
resolve(`Timeout waiting for: ${prompt}`)
}, timeout)
this.promptCallbacks.push({
prompt,
callback: (data: string) => {
clearTimeout(timer)
resolve(data)
},
})
})
}
private _send(command: string) {
if (this.socket && !this.socket.destroyed && this.status === 'CONNECTED') {
this.socket.write(command + '\r\n')
}
}
public connect(): Promise<void> {
return new Promise((resolve, reject) => {
try {
this.socket.connect(this.port, this.host, () => {
this.status = 'CONNECTED'
this.isEnable = false
this.socket.setEncoding('utf8')
this.checkStatusAllPorts()
this.socket.on('data', (data) => this._handleData(data.toString()))
resolve()
})
this.socket.on('close', (e) => {
this._handleClose(e)
resolve()
})
this.socket.on('error', (err) => {
this._handleError(err)
resolve()
})
this.socket.on('timeout', () => {
this._handleTimeout()
resolve()
})
} catch (error: any) {
console.log('[ERROR CONNECT SWITCH]:', error.message)
reject(error)
}
})
}
public disconnect() {
this.socket.end()
this.status = 'DISCONNECTED'
this.isEnable = false
}
public checkStatusAllPorts() {
const interval = setInterval(async () => {
try {
if (this.status !== 'CONNECTED') {
clearInterval(interval)
return
}
await this.getPorts()
if (this.promptCallbacks.length === 0) this.buffer = ''
} catch (err: any) {
console.error('Error checking port status:', err)
this.onData(this.portGroups, this.status)
}
}, 5000)
}
public async enterEnableMode() {
if (this.isEnable) return
await this.sleep(1000)
this._send('')
await this.sleep(1000)
this._send('')
await this.sleep(1000)
this._send('enable')
await this.sleep(1000)
this._send(this.password)
this.isEnable = true
}
public async login() {
await this.enterEnableMode()
this._send('terminal length 0')
}
public async checkPortStatus(port: string) {
this._send(`show interface ${port}`)
const result = await this._waitFor('#')
return result
}
public async checkPoEStatus(port: string) {
this._send(`show power inline ${port}`)
const result = await this._waitFor('#')
return result
}
public async turnPortOff(port: string) {
await this.enterEnableMode()
this._send(`configure terminal`)
// await this._waitFor('(config)#')
await this.sleep(500)
this._send(`interface ${port}`)
// await this._waitFor('(config-if)#')
await this.sleep(500)
this._send(`shutdown`)
// await this._waitFor('(config-if)#')
await this.sleep(500)
this._send(`end`)
}
public async turnPortOn(port: string) {
await this.enterEnableMode()
this._send(`configure terminal`)
// await this._waitFor('(config)#')
await this.sleep(500)
this._send(`interface ${port}`)
// await this._waitFor('(config-if)#')
await this.sleep(500)
this._send(`no shutdown`)
// await this._waitFor('(config-if)#')
await this.sleep(500)
this._send(`end`)
}
public async restartPort(port: string) {
// await this.enterEnableMode()
// this._send(`configure terminal`)
// // await this._waitFor('(config)#')
// await this.sleep(500)
// this._send(`interface ${port}`)
// // await this._waitFor('(config-if)#')
// await this.sleep(500)
// this._send(`shutdown`)
// // await this._waitFor('(config-if)#')
// await this.sleep(500)
// await this.sleep(2000)
// this._send(`no shutdown`)
// // await this._waitFor('(config-if)#')
// await this.sleep(500)
// this._send(`end`)
await this.turnPortOff(port)
await this.sleep(300)
await this.getPorts()
await this.sleep(300)
await this.turnPortOn(port)
}
public async disablePoE(port: string) {
await this.enterEnableMode()
this._send(`configure terminal`)
// await this._waitFor('(config)#')
await this.sleep(500)
this._send(`interface ${port}`)
// await this._waitFor('(config-if)#')
await this.sleep(500)
this._send(`power inline never`)
// await this._waitFor('(config-if)#')
await this.sleep(500)
this._send(`end`)
}
public async enablePoE(port: string) {
await this.enterEnableMode()
this._send(`configure terminal`)
// await this._waitFor('(config)#')
await this.sleep(500)
this._send(`interface ${port}`)
// await this._waitFor('(config-if)#')
await this.sleep(500)
this._send(`power inline auto`)
// await this._waitFor('(config-if)#')
await this.sleep(500)
this._send(`end`)
}
public async restartPoE(port: string) {
await this.disablePoE(port)
await this.sleep(3000)
await this.enablePoE(port)
}
public async getPorts(): Promise<boolean> {
this._send(' terminal length 0')
this._send('show interface')
this._send(' ')
await this.sleep(2000)
const statusOutput = this.buffer
this.buffer = ''
const lines = statusOutput.split('\n')
const ports = this.ports?.length > 0 ? [...this.ports] : []
for (const line of lines) {
// Match: "Gi0/1 is up, line protocol is up"
const match = line.match(
/^(TenGigabitEthernet|GigabitEthernet|FastEthernet|Ethernet)\S*\s+is\s+(\S+),\s+line protocol is\s+(\S+)/i
)
if (match) {
const name = match[1] + line.split(' ')[0].replace(match[1], '')
const status1 = match[2].toLowerCase() // up / down / administratively
const status2 = match[3].toLowerCase() // up / down
// Rule: interface is considered ON only when both are "up"
const status = status1 === 'up' && status2 === 'up' ? 'ON' : 'OFF'
const port = ports.find((p) => p.name === name)
if (port) {
port.status = status
} else {
ports.push({ name, status, poe: 'UNKNOWN' })
}
}
}
// PoE check
this._send('show power inline')
await this.sleep(2000)
const poeOutput = this.buffer
this.buffer = ''
const poeLines = poeOutput.split('\n')
for (const line of poeLines) {
const match = line.match(/^(\S+)\s+\S+\s+(on|off)/i)
if (match) {
const name = match[1]
const poeStatus = match[2].toLowerCase() === 'on' ? 'ON' : 'OFF'
const port = ports.find((p) => p.name === name)
if (port) port.poe = poeStatus
}
}
const grouped: Record<string, PortInfo[]> = {}
for (const port of ports) {
const prefixMatch = port.name.match(/^([A-Za-z]+)/)
const prefix = prefixMatch ? prefixMatch[1] : 'Unknown'
if (!grouped[prefix]) grouped[prefix] = []
const existing = grouped[prefix].find((el) => el.name === port.name)
if (!existing) grouped[prefix].push(port)
}
const groupedArray = Object.values(grouped)
this.ports = ports
this.portGroups = groupedArray
this.onData(this.portGroups, this.status)
return true
}
public async reconnect(): Promise<boolean> {
try {
this.disconnect()
this.socket = new net.Socket()
await this.sleep(1000)
await this.connect()
await this.login()
await this.getPorts()
return true
} catch (err: any) {
this._handleError(err)
return false
}
}
}