feat:telnet发送配置命令

This commit is contained in:
Jerry 2025-07-18 20:46:49 +08:00
parent c74d55e62b
commit 2af76f8a54
5 changed files with 63 additions and 37 deletions

View File

@ -10,6 +10,7 @@ import asyncssh
from pydantic import BaseModel from pydantic import BaseModel
from tenacity import retry, stop_after_attempt, wait_exponential from tenacity import retry, stop_after_attempt, wait_exponential
from src.backend.app.utils.logger import logger
from src.backend.config import settings from src.backend.config import settings
@ -82,56 +83,62 @@ class SwitchConfigurator:
"""双模式命令发送""" """双模式命令发送"""
return ( return (
await self._send_ensp_commands(ip, commands) await self._send_ensp_commands(ip, commands)
if self.ensp_mode
else await self._send_ssh_commands(ip, commands)
) )
# --------- eNSP模式专用 ---------
async def _send_ensp_commands(self, ip: str, commands: List[str]) -> str: async def _send_ensp_commands(self, ip: str, commands: List[str]) -> str:
"""Telnet协议执行eNSP""" """
通过 Telnet 协议连接 eNSP 设备
"""
try: try:
reader, writer = await telnetlib3.open_connection( logger.info(f"连接设备 {ip}端口23")
host=ip, reader, writer = await telnetlib3.open_connection(host=ip, port=23)
port=self.ensp_port, logger.debug("连接成功,开始登录流程")
connect_minwait=self.timeout, # telnetlib3的实际可用参数
connect_maxwait=self.timeout
)
try: try:
if self.username != 'NONE':
await asyncio.wait_for(reader.readuntil(b"Username:"), timeout=self.timeout) await asyncio.wait_for(reader.readuntil(b"Username:"), timeout=self.timeout)
logger.debug("收到 'Username:' 提示,发送用户名")
writer.write(f"{self.username}\n") writer.write(f"{self.username}\n")
await asyncio.wait_for(reader.readuntil(b"Password:"), timeout=self.timeout) await asyncio.wait_for(reader.readuntil(b"Password:"), timeout=self.timeout)
logger.debug("收到 'Password:' 提示,发送密码")
writer.write(f"{self.password}\n") writer.write(f"{self.password}\n")
await asyncio.sleep(1) await asyncio.sleep(1)
except asyncio.TimeoutError: except asyncio.TimeoutError:
raise EnspConnectionException("登录超时") raise EnspConnectionException("登录超时,未收到用户名或密码提示")
output = "" output = ""
for cmd in commands: for cmd in commands:
logger.info(f"发送命令: {cmd}")
writer.write(f"{cmd}\n") writer.write(f"{cmd}\n")
await writer.drain() await writer.drain()
command_output = ""
try: try:
while True: while True:
data = await asyncio.wait_for(reader.read(1024), timeout=1) data = await asyncio.wait_for(reader.read(1024), timeout=1)
if not data: if not data:
logger.debug("读取到空数据,结束当前命令读取")
break break
output += data command_output += data
logger.debug(f"收到数据: {repr(data)}")
except asyncio.TimeoutError: except asyncio.TimeoutError:
continue logger.debug("命令输出读取超时,继续执行下一条命令")
output += f"\n[命令: {cmd} 输出开始]\n{command_output}\n[命令: {cmd} 输出结束]\n"
logger.info("所有命令执行完毕,关闭连接")
writer.close() writer.close()
try:
await writer.wait_closed()
except:
logging.debug("连接关闭时出现异常", exc_info=True)
pass
return output return output
except asyncio.TimeoutError as e:
logger.error(f"连接或读取超时: {e}")
raise EnspConnectionException(f"eNSP连接超时: {e}")
except Exception as e: except Exception as e:
raise EnspConnectionException(f"eNSP连接失败: {str(e)}") logger.error(f"连接或执行异常: {e}", exc_info=True)
raise EnspConnectionException(f"eNSP连接失败: {e}")
@staticmethod @staticmethod
def _generate_ensp_commands(config: SwitchConfig) -> List[str]: def _generate_ensp_commands(config: SwitchConfig) -> List[str]:

View File

@ -30,10 +30,10 @@ class AIService:
2. 必须包含'commands'字段包含可直接执行的命令列表 2. 必须包含'commands'字段包含可直接执行的命令列表
3. 其他参数根据配置类型动态添加 3. 其他参数根据配置类型动态添加
4. 不要包含解释性文本步骤说明或注释 4. 不要包含解释性文本步骤说明或注释
5.要包含使用ssh连接交换机后的完整命令包括但不完全包括system-view退出保存等完整操作注意保存还需要输入Y和回车 5.要包含使用ssh连接交换机后的完整命令包括但不完全包括system-view退出保存等完整操作注意保存还需要输入Y
示例命令'创建VLAN 100名称为TEST' 示例命令'创建VLAN 100名称为TEST'
示例返回{"type": "vlan", "vlan_id": 100, "name": "TEST", "commands": ["system-view\n","vlan 100\n", "name TEST\n","quit\n","\x1A\n","save\n","Y\n"]} 示例返回{"type": "vlan", "vlan_id": 100, "name": "TEST", "commands": ["system-view","vlan 100", "name TEST","quit","quit","save","Y"]}
注意这里生成的commands中需包含登录交换机和保存等所有操作命令我们使ssh连接交换机你不需要给出连接ssh的命令你只需要给出使用ssh连接到交换机后所输入的全部命令 注意这里生成的commands中需包含登录交换机和保存等所有操作命令我们使ssh连接交换机你不需要给出连接ssh的命令你只需要给出使用ssh连接到交换机后所输入的全部命令并且注意在system-view状态下是不能save的需要再quit到用户视图
""" """
messages = [ messages = [

View File

@ -117,16 +117,31 @@ const ConfigPage = () => {
console.log(`commands:${JSON.stringify(commands)}`); console.log(`commands:${JSON.stringify(commands)}`);
const deviceConfig = JSON.parse(selectedDeviceConfig); const deviceConfig = JSON.parse(selectedDeviceConfig);
console.log(`deviceConfig:${JSON.stringify(deviceConfig)}`); console.log(`deviceConfig:${JSON.stringify(deviceConfig)}`);
if (!deviceConfig.username || !deviceConfig.password) { if (!deviceConfig.password) {
Notification.warn({ Notification.warn({
title: '所选交换机暂未配置用户名和密码', title: '所选交换机暂未配置用户名(可选)和密码',
description: '请前往交换机设备处配置username和password', description: '请前往交换机设备处配置username和password',
}); });
return false; return false;
} }
commands.push(`username=${deviceConfig.username.toString()}`); if (deviceConfig.username || deviceConfig.username.toString() !== '') {
commands.push(`password=${deviceConfig.password.toString()}`); commands.push(`!username=${deviceConfig.username.toString()}`);
await api.applyConfig(selectedDevice, commands); } else {
commands.push(`!username=NONE`);
}
commands.push(`!password=${deviceConfig.password.toString()}`);
const res = await api.applyConfig(selectedDevice, commands);
if (res) {
Notification.success({
title: '配置完毕',
description: JSON.stringify(res),
});
} else {
Notification.error({
title: '配置过程出现错误',
description: '请检查API提示',
});
}
} }
}; };

View File

@ -197,7 +197,7 @@ const ScanPage = () => {
</HStack> </HStack>
{inputMode ? ( {inputMode ? (
<FadeInWrapper delay={0.1} yOffset={-2}> <FadeInWrapper delay={0} yOffset={-1}>
<Input <Input
mb={4} mb={4}
placeholder={'输入子网 (如 192.168.1.0/24)'} placeholder={'输入子网 (如 192.168.1.0/24)'}
@ -229,14 +229,16 @@ const ScanPage = () => {
</HStack> </HStack>
{loading && ( {loading && (
<FadeInWrapper delay={0} yOffset={-1}>
<HStack> <HStack>
<Spinner /> <Spinner />
<Text>{'正在加载,请稍候..'}</Text> <Text>{'正在扫描,请稍候..'}</Text>
</HStack> </HStack>
</FadeInWrapper>
)} )}
{!loading && scannedDevices.length > 0 && ( {!loading && scannedDevices.length > 0 && (
<FadeInWrapper delay={0.2} yOffset={-5}> <FadeInWrapper delay={0} yOffset={-3}>
<Table.Root <Table.Root
variant={'outline'} variant={'outline'}
colorPalette={'teal'} colorPalette={'teal'}
@ -283,7 +285,9 @@ const ScanPage = () => {
)} )}
{!loading && scannedDevices.length === 0 && ( {!loading && scannedDevices.length === 0 && (
<FadeInWrapper delay={0} yOffset={-1}>
<Text color={'gray.400'}>{'暂无扫描结果,请执行扫描..'}</Text> <Text color={'gray.400'}>{'暂无扫描结果,请执行扫描..'}</Text>
</FadeInWrapper>
)} )}
</Box> </Box>
</VStack> </VStack>