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

@ -186,7 +186,7 @@ async def execute_cli_commands(request: CLICommandRequest):
)
return {
"success": True,
"output": result,
"output": result,
"mode": "eNSP" if request.is_ensp else "SSH"
}
except Exception as e:

View File

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

View File

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

View File

@ -117,16 +117,31 @@ const ConfigPage = () => {
console.log(`commands:${JSON.stringify(commands)}`);
const deviceConfig = JSON.parse(selectedDeviceConfig);
console.log(`deviceConfig:${JSON.stringify(deviceConfig)}`);
if (!deviceConfig.username || !deviceConfig.password) {
if (!deviceConfig.password) {
Notification.warn({
title: '所选交换机暂未配置用户名和密码',
title: '所选交换机暂未配置用户名(可选)和密码',
description: '请前往交换机设备处配置username和password',
});
return false;
}
commands.push(`username=${deviceConfig.username.toString()}`);
commands.push(`password=${deviceConfig.password.toString()}`);
await api.applyConfig(selectedDevice, commands);
if (deviceConfig.username || deviceConfig.username.toString() !== '') {
commands.push(`!username=${deviceConfig.username.toString()}`);
} 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>
{inputMode ? (
<FadeInWrapper delay={0.1} yOffset={-2}>
<FadeInWrapper delay={0} yOffset={-1}>
<Input
mb={4}
placeholder={'输入子网 (如 192.168.1.0/24)'}
@ -229,14 +229,16 @@ const ScanPage = () => {
</HStack>
{loading && (
<HStack>
<Spinner />
<Text>{'正在加载,请稍候..'}</Text>
</HStack>
<FadeInWrapper delay={0} yOffset={-1}>
<HStack>
<Spinner />
<Text>{'正在扫描,请稍候..'}</Text>
</HStack>
</FadeInWrapper>
)}
{!loading && scannedDevices.length > 0 && (
<FadeInWrapper delay={0.2} yOffset={-5}>
<FadeInWrapper delay={0} yOffset={-3}>
<Table.Root
variant={'outline'}
colorPalette={'teal'}
@ -283,7 +285,9 @@ const ScanPage = () => {
)}
{!loading && scannedDevices.length === 0 && (
<Text color={'gray.400'}>{'暂无扫描结果,请执行扫描..'}</Text>
<FadeInWrapper delay={0} yOffset={-1}>
<Text color={'gray.400'}>{'暂无扫描结果,请执行扫描..'}</Text>
</FadeInWrapper>
)}
</Box>
</VStack>