366 lines
10 KiB
TypeScript
366 lines
10 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) {
|
|
this.retryConnect = 0
|
|
return
|
|
}
|
|
await this.sleep(15000)
|
|
console.log('SWITCH 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.retryConnect = 0
|
|
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.socket.destroy()
|
|
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+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?.sort((a, b) => b.length - a.length)
|
|
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
|
|
}
|
|
}
|
|
}
|