diff --git a/src/backend/app/api/endpoints.py b/src/backend/app/api/endpoints.py index 20ed723..697e687 100644 --- a/src/backend/app/api/endpoints.py +++ b/src/backend/app/api/endpoints.py @@ -11,6 +11,7 @@ import base64 import psutil import ipaddress +from ..models.requests import CLICommandRequest, ConfigRequest from ..services.switch_traffic_monitor import get_switch_monitor from ..utils import logger from ...app.services.ai_services import AIService @@ -55,22 +56,6 @@ class BatchConfigRequest(BaseModel): password: str = None timeout: int = None - -@router.post("/batch_apply_config") -async def batch_apply_config(request: BatchConfigRequest): - results = {} - for ip in request.switch_ips: - try: - configurator = SwitchConfigurator( - username=request.username, - password=request.password, - timeout=request.timeout) - results[ip] = await configurator.apply_config(ip, request.config) - except Exception as e: - results[ip] = str(e) - return {"results": results} - - @router.get("/test") async def test_endpoint(): return {"message": "Hello World"} @@ -96,27 +81,29 @@ async def list_devices(): } +class DeviceItem(BaseModel): + name: str + ip: str + vendor: str + class CommandRequest(BaseModel): command: str - vendor: str = "huawei" - - -class ConfigRequest(BaseModel): - config: dict - switch_ip: str - username: str = None - password: str = None - timeout: int = None - vendor: str = "huawei" - + devices: List[DeviceItem] @router.post("/parse_command", response_model=dict) async def parse_command(request: CommandRequest): - """解析中文命令并返回JSON配置""" + """解析中文命令并返回每台设备的配置 JSON""" + missing_vendor = [d for d in request.devices if not d.vendor or d.vendor.strip() == ""] + if missing_vendor: + names = ", ".join([d.name for d in missing_vendor]) + raise HTTPException( + status_code=400, + detail=f"以下设备未配置厂商: {names}" + ) try: ai_service = AIService(settings.SILICONFLOW_API_KEY, settings.SILICONFLOW_API_URL) - config = await ai_service.parse_command(request.command, request.vendor) - return {"success": True, "config": config} + config = await ai_service.parse_command(request.command, [d.dict() for d in request.devices]) + return {"success": True, "config": config.get("results", [])} except Exception as e: raise HTTPException( status_code=400, @@ -141,44 +128,16 @@ async def apply_config(request: ConfigRequest): status_code=500, detail=f"Failed to apply config: {str(e)}" ) - - -class CLICommandRequest(BaseModel): - switch_ip: str - commands: List[str] - is_ensp: bool = False - - def extract_credentials(self) -> tuple: - """从commands中提取用户名和密码""" - username = None - password = None - - for cmd in self.commands: - if cmd.startswith("!username="): - username = cmd.split("=")[1] - elif cmd.startswith("!password="): - password = cmd.split("=")[1] - - return username, password - - def get_clean_commands(self) -> List[str]: - """获取去除凭据后的实际命令""" - return [cmd for cmd in self.commands - if not (cmd.startswith("!username=") or cmd.startswith("!password="))] - - @router.post("/execute_cli_commands", response_model=dict) async def execute_cli_commands(request: CLICommandRequest): """执行前端生成的CLI命令""" try: username, password = request.extract_credentials() - clean_commands = request.get_clean_commands() configurator = SwitchConfigurator( username=username, password=password, timeout=settings.SWITCH_TIMEOUT, - ensp_mode=request.is_ensp ) result = await configurator.execute_raw_commands( @@ -188,7 +147,6 @@ async def execute_cli_commands(request: CLICommandRequest): return { "success": True, "output": result, - "mode": "eNSP" if request.is_ensp else "SSH" } except Exception as e: raise HTTPException(500, detail=str(e)) diff --git a/src/backend/app/api/network_config.py b/src/backend/app/api/network_config.py index bd0c225..b512273 100644 --- a/src/backend/app/api/network_config.py +++ b/src/backend/app/api/network_config.py @@ -1,19 +1,14 @@ import asyncio import logging import telnetlib3 -from datetime import datetime from pathlib import Path -from typing import Dict, List, Optional, Union +from typing import Dict, List, Optional -import aiofiles -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 - # ---------------------- # 数据模型 # ---------------------- @@ -25,36 +20,31 @@ class SwitchConfig(BaseModel): ip_address: Optional[str] = None vlan: Optional[int] = None - # ---------------------- # 异常类 # ---------------------- class SwitchConfigException(Exception): pass - class EnspConnectionException(SwitchConfigException): pass - -class SSHConnectionException(SwitchConfigException): - pass - - # ---------------------- -# 核心配置器(完整双模式) +# 核心配置器 # ---------------------- class SwitchConfigurator: + connection_pool: Dict[str, tuple] = {} + def __init__( - self, - username: str = None, - password: str = None, - timeout: int = None, - max_workers: int = 5, - ensp_mode: bool = False, - ensp_port: int = 2000, - ensp_command_delay: float = 0.5, - **ssh_options + self, + username: str = None, + password: str = None, + timeout: int = None, + max_workers: int = 5, + ensp_mode: bool = False, + ensp_port: int = 2000, + ensp_command_delay: float = 0.5, + **ssh_options ): self.username = username if username is not None else settings.SWITCH_USERNAME self.password = password if password is not None else settings.SWITCH_PASSWORD @@ -67,253 +57,66 @@ class SwitchConfigurator: self.ensp_delay = ensp_command_delay self.ssh_options = ssh_options - async def apply_config(self, ip: str, config: Union[Dict, SwitchConfig]) -> str: - """实际配置逻辑""" - if isinstance(config, dict): - config = SwitchConfig(**config) - - commands = ( - self._generate_ensp_commands(config) - if self.ensp_mode - else self._generate_standard_commands(config) - ) - return await self._send_commands(ip, commands) - - async def _send_commands(self, ip: str, commands: List[str]) -> str: - """双模式命令发送""" - return ( - await self._send_ensp_commands(ip, commands) - ) - - async def _send_ensp_commands(self, ip: str, commands: List[str]) -> str: + async def _get_or_create_connection(self, ip: str): """ - 通过 Telnet 协议连接 eNSP 设备 + 从连接池获取连接,如果没有则新建 Telnet 连接 + """ + if ip in self.connection_pool: + logger.debug(f"复用已有连接: {ip}") + return self.connection_pool[ip] + + logger.info(f"建立新连接: {ip}") + reader, writer = await telnetlib3.open_connection(host=ip, port=23) + + try: + if self.username != 'NONE' : + await asyncio.wait_for(reader.readuntil(b"Username:"), timeout=self.timeout) + writer.write(f"{self.username}\n") + + await asyncio.wait_for(reader.readuntil(b"Password:"), timeout=self.timeout) + writer.write(f"{self.password}\n") + + await asyncio.sleep(1) + except asyncio.TimeoutError: + writer.close() + raise EnspConnectionException("登录超时,未收到用户名或密码提示") + except Exception as e: + writer.close() + raise EnspConnectionException(f"登录异常: {e}") + + self.connection_pool[ip] = (reader, writer) + return reader, writer + + async def _send_ensp_commands(self, ip: str, commands: List[str]) -> bool: + """ + 通过 Telnet 协议发送命令 """ try: - logger.info(f"连接设备 {ip},端口23") - reader, writer = await telnetlib3.open_connection(host=ip, port=23) - logger.debug("连接成功,开始登录流程") + reader, writer = await self._get_or_create_connection(ip) - try: - 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("登录超时,未收到用户名或密码提示") - - output = "" for cmd in commands: if cmd.startswith("!"): logger.debug(f"跳过特殊命令: {cmd}") continue - logger.info(f"发送命令: {cmd}") + logger.info(f"[{ip}] 发送命令: {cmd}") writer.write(f"{cmd}\n") await writer.drain() + await asyncio.sleep(self.ensp_delay) - command_output = "" - try: - while True: - data = await asyncio.wait_for(reader.read(1024), timeout=1) - if not data: - logger.debug("读取到空数据,结束当前命令读取") - break - command_output += data - logger.debug(f"收到数据: {repr(data)}") - except asyncio.TimeoutError: - logger.debug("命令输出读取超时,继续执行下一条命令") - - output += f"\n[命令: {cmd} 输出开始]\n{command_output}\n[命令: {cmd} 输出结束]\n" - - logger.info("所有命令执行完毕,关闭连接") - writer.close() - - return output + logger.info(f"[{ip}] 所有命令发送完成") + return True except asyncio.TimeoutError as e: - logger.error(f"连接或读取超时: {e}") - raise EnspConnectionException(f"eNSP连接超时: {e}") + logger.error(f"[{ip}] 连接或读取超时: {e}") + return False except Exception as e: - logger.error(f"连接或执行异常: {e}", exc_info=True) - raise EnspConnectionException(f"eNSP连接失败: {e}") - - @staticmethod - def _generate_ensp_commands(config: SwitchConfig) -> List[str]: - """生成eNSP命令序列""" - commands = ["system-view"] - if config.type == "vlan": - commands.extend([ - f"vlan {config.vlan_id}", - f"description {config.name or ''}" - ]) - elif config.type == "interface": - commands.extend([ - f"interface {config.interface}", - "port link-type access", - f"port default vlan {config.vlan}" if config.vlan else "", - f"ip address {config.ip_address}" if config.ip_address else "" - ]) - commands.append("return") - return [c for c in commands if c.strip()] - - async def _send_ssh_commands(self, ip: str, commands: List[str]) -> str: - """AsyncSSH执行命令""" - async with self.semaphore: - try: - async with asyncssh.connect( - host=ip, - username=self.username, - password=self.password, - connect_timeout=self.timeout, - **self.ssh_options - ) as conn: - results = [] - for cmd in commands: - result = await conn.run(cmd, check=True) - results.append(result.stdout) - return "\n".join(results) - except asyncssh.Error as e: - raise SSHConnectionException(f"SSH操作失败: {str(e)}") - except Exception as e: - raise SSHConnectionException(f"连接异常: {str(e)}") - - async def execute_raw_commands(self, ip: str, commands: List[str]) -> str: - """ - 执行原始CLI命令 - """ - return await self._send_commands(ip, commands) - - - @staticmethod - def _generate_standard_commands(config: SwitchConfig) -> List[str]: - """生成标准CLI命令""" - commands = [] - if config.type == "vlan": - commands.extend([ - f"vlan {config.vlan_id}", - f"name {config.name or ''}" - ]) - elif config.type == "interface": - commands.extend([ - f"interface {config.interface}", - f"switchport access vlan {config.vlan}" if config.vlan else "", - f"ip address {config.ip_address}" if config.ip_address else "" - ]) - return commands - - async def _validate_config(self, ip: str, config: SwitchConfig) -> bool: - """验证配置是否生效""" - current = await self._get_current_config(ip) - if config.type == "vlan": - return f"vlan {config.vlan_id}" in current - elif config.type == "interface" and config.vlan: - return f"switchport access vlan {config.vlan}" in current - return True - - async def _get_current_config(self, ip: str) -> str: - """获取当前配置""" - commands = ( - ["display current-configuration"] - if self.ensp_mode - else ["show running-config"] - ) - try: - return await self._send_commands(ip, commands) - except (EnspConnectionException, SSHConnectionException) as e: - raise SwitchConfigException(f"配置获取失败: {str(e)}") - - async def _backup_config(self, ip: str) -> Path: - """备份配置到文件""" - backup_path = self.backup_dir / f"{ip}_{datetime.now().isoformat()}.cfg" - config = await self._get_current_config(ip) - async with aiofiles.open(backup_path, "w") as f: - await f.write(config) - return backup_path - - async def _restore_config(self, ip: str, backup_path: Path) -> bool: - """从备份恢复配置""" - try: - async with aiofiles.open(backup_path) as f: - config = await f.read() - commands = ( - ["system-view", config, "return"] - if self.ensp_mode - else [f"configure terminal\n{config}\nend"] - ) - await self._send_commands(ip, commands) - return True - except Exception as e: - logging.error(f"恢复失败: {str(e)}") + logger.error(f"[{ip}] 命令发送异常: {e}", exc_info=True) return False - - @retry( - stop=stop_after_attempt(2), - wait=wait_exponential(multiplier=1, min=4, max=10) - ) - async def safe_apply( - self, - ip: str, - config: Union[Dict, SwitchConfig] - ) -> Dict[str, Union[str, bool, Path]]: - """安全配置应用(自动回滚)""" - backup_path = await self._backup_config(ip) - try: - result = await self.apply_config(ip, config) - if not await self._validate_config(ip, config): - raise SwitchConfigException("配置验证失败") - return { - "status": "success", - "output": result, - "backup_path": str(backup_path) - } - except (EnspConnectionException, SSHConnectionException, SwitchConfigException) as e: - restore_status = await self._restore_config(ip, backup_path) - return { - "status": "failed", - "error": str(e), - "backup_path": str(backup_path), - "restore_success": restore_status - } - - -# ---------------------- -# 使用示例 -# ---------------------- -async def demo(): - ensp_configurator = SwitchConfigurator( - ensp_mode=True, - ensp_port=2000, - username="admin", - password="admin", - timeout=15 - ) - ensp_result = await ensp_configurator.safe_apply("127.0.0.1", { - "type": "interface", - "interface": "GigabitEthernet0/0/1", - "vlan": 100, - "ip_address": "192.168.1.2 255.255.255.0" - }) - print("eNSP配置结果:", ensp_result) - - ssh_configurator = SwitchConfigurator( - username="cisco", - password="cisco123", - timeout=15 - ) - ssh_result = await ssh_configurator.safe_apply("192.168.1.1", { - "type": "vlan", - "vlan_id": 200, - "name": "Production" - }) - print("SSH配置结果:", ssh_result) - - -if __name__ == "__main__": - asyncio.run(demo()) + async def execute_raw_commands(self, ip: str, commands: List[str]) -> bool: + """ + 对外接口:单台交换机执行命令 + """ + async with self.semaphore: + success = await self._send_ensp_commands(ip, commands) + return success diff --git a/src/backend/app/models/requests.py b/src/backend/app/models/requests.py new file mode 100644 index 0000000..4c696f1 --- /dev/null +++ b/src/backend/app/models/requests.py @@ -0,0 +1,26 @@ +from typing import List, Optional +from pydantic import BaseModel + +class BatchConfigRequest(BaseModel): + config: dict + switch_ips: List[str] + username: Optional[str] = None + password: Optional[str] = None + timeout: Optional[int] = None + +class ConfigRequest(BaseModel): + config: dict + switch_ip: str + username: Optional[str] = None + password: Optional[str] = None + timeout: Optional[int] = None + vendor: str = "huawei" + +class CLICommandRequest(BaseModel): + switch_ip: str + commands: List[str] + username: Optional[str] = None + password: Optional[str] = None + + def extract_credentials(self): + return self.username or "NONE", self.password or "NONE" diff --git a/src/backend/app/services/ai_services.py b/src/backend/app/services/ai_services.py index 2987bd0..74e48c9 100644 --- a/src/backend/app/services/ai_services.py +++ b/src/backend/app/services/ai_services.py @@ -1,54 +1,47 @@ -from typing import Any +from typing import Any, List, Dict from openai import AsyncOpenAI import json from src.backend.app.utils.exceptions import SiliconFlowAPIException from openai.types.chat import ChatCompletionSystemMessageParam, ChatCompletionUserMessageParam -from src.backend.app.utils.logger import logger - class AIService: def __init__(self, api_key: str, api_url: str): - self.api_key = api_key - self.api_url = api_url - self.client = AsyncOpenAI( - api_key=self.api_key, - base_url=self.api_url, - # timeout=httpx.Timeout(30.0) - ) + self.client = AsyncOpenAI(api_key=api_key, base_url=api_url) - async def parse_command(self, command: str, vendor: str = "huawei") -> Any | None: + async def parse_command(self, command: str, devices: List[Dict]) -> Dict[str, Any]: """ - 调用硅基流动API解析中文命令 + 针对一组设备和一条自然语言命令,生成每台设备的配置 JSON """ - vendor_prompts = { - "huawei": "华为交换机配置命令", - "cisco": "思科交换机配置命令", - "h3c": "H3C交换机配置命令", - "ruijie": "锐捷交换机配置命令", - "zte": "中兴交换机配置命令" - } + devices_str = json.dumps(devices, ensure_ascii=False, indent=2) + + example = """[{"device": {"name": "sw1","ip": "192.168.1.10","vendor": "huawei","username": "NONE", "password": "Huawei"},"config": {"type": "vlan","vlan_id": 300,"name": "Sales","commands": ["system-view","vlan 300","name Sales","quit","quit","save","Y"]}}]""" prompt = f""" - 你是一个网络设备配置专家,精通各种类型的路由器的配置,请将以下用户的中文命令转换为{vendor_prompts.get(vendor, '网络设备')}配置JSON。 - 但是请注意,由于贪婪的人们追求极高的效率,所以你必须严格按照 JSON 格式返回数据,不要包含任何额外文本或 Markdown 代码块。 - 返回格式要求: - 1. 必须包含'type'字段指明配置类型(vlan/interface/acl/route等) - 2. 必须包含'commands'字段,包含可直接执行的命令列表 - 3. 其他参数根据配置类型动态添加 - 4. 不要包含解释性文本、步骤说明或注释 - 5. 要包含使用ssh连接交换机后的完整命令包括但不完全包括system-view,退出,保存等完整操作,注意保存还需要输入Y +你是一个网络设备配置专家。现在有以下设备: +{devices_str} - 根据厂商{vendor}的不同,命令格式如下: - - 华为: system-view → quit → save Y - - 思科: enable → configure terminal → exit → write memory - - H3C: system-view → quit → save - - 锐捷: enable → configure terminal → exit → write - - 中兴: enable → configure terminal → exit → write memory +用户输入了一条命令:{command} - 示例命令:'创建VLAN 100,名称为TEST' - 华为示例返回:{{"type": "vlan", "vlan_id": 100, "name": "TEST", "commands": ["system-view","vlan 100", "name TEST","quit","quit","save","Y"]}} - 思科示例返回:{{"type": "vlan", "vlan_id": 100, "name": "TEST", "commands": ["enable","configure terminal","vlan 100", "name TEST","exit","exit","write memory"]}} - """ +你的任务: +- 为每台设备分别生成配置 +- 输出一个 JSON 数组,每个元素对应一台设备 +- 每个对象必须包含: + - device: 原始设备信息 (name, ip, vendor,username,password) + - config: 配置详情 + - type: 配置类型 (如 vlan/interface/acl/route) + - commands: 可直接执行的命令数组 (必须包含进入配置、退出、保存命令) + - 其他字段: 根据配置类型动态添加 +- 严格返回 JSON,不要包含解释说明或 markdown + +各厂商保存命令规则: +- 华为: system-view → quit → save Y +- 思科: enable → configure terminal → exit → write memory +- H3C: system-view → quit → save +- 锐捷: enable → configure terminal → exit → write +- 中兴: enable → configure terminal → exit → write memory + +返回示例(仅作为格式参考,不要照抄 VLAN ID 和命令内容,请根据实际命令生成):{example} +""" messages = [ ChatCompletionSystemMessageParam(role="system", content=prompt), @@ -59,29 +52,18 @@ class AIService: response = await self.client.chat.completions.create( model="deepseek-ai/DeepSeek-V3", messages=messages, - temperature=0.3, - max_tokens=1000, + temperature=0.2, + max_tokens=1500, response_format={"type": "json_object"} ) - logger.debug(response) - config_str = response.choices[0].message.content.strip() + configs = json.loads(config_str) - try: - config = json.loads(config_str) - return config - except json.JSONDecodeError: - if config_str.startswith("```json"): - config_str = config_str[7:-3].strip() - return json.loads(config_str) - raise SiliconFlowAPIException("Invalid JSON format returned from AI") - except KeyError: - logger.error(KeyError) - raise SiliconFlowAPIException("errrrrrrro") + return {"success": True, "results": configs} except Exception as e: raise SiliconFlowAPIException( - detail=f"API请求失败: {str(e)}", + detail=f"AI 解析配置失败: {str(e)}", status_code=getattr(e, "status_code", 500) ) diff --git a/src/frontend/src/components/pages/config/DeviceConfigModal.jsx b/src/frontend/src/components/pages/config/DeviceConfigModal.jsx index 744ad81..cd495d0 100644 --- a/src/frontend/src/components/pages/config/DeviceConfigModal.jsx +++ b/src/frontend/src/components/pages/config/DeviceConfigModal.jsx @@ -12,29 +12,29 @@ import { Field, Input, Stack, + Portal, + Select, } from '@chakra-ui/react'; import { motion } from 'framer-motion'; import { FiCheck } from 'react-icons/fi'; -import Notification from '@/libs/system/Notification'; +import { createListCollection } from '@chakra-ui/react'; const MotionBox = motion(Box); -/** - * 设备配置弹窗 - * @param isOpen 是否打开 - * @param onClose 关闭弹窗 - * @param onSave 保存修改 - * @param device 当前设备 - * @returns {JSX.Element} - * @constructor - */ +const vendors = ['huawei', 'cisco', 'h3c', 'ruijie', 'zte']; + const DeviceConfigModal = ({ isOpen, onClose, onSave, device }) => { const [username, setUsername] = useState(device.username || ''); const [password, setPassword] = useState(device.password || ''); + const [vendor, setVendor] = useState(device.vendor || ''); const [saved, setSaved] = useState(false); + const vendorCollection = createListCollection({ + items: vendors.map((v) => ({ label: v.toUpperCase(), value: v })), + }); + const handleSave = () => { - const updatedDevice = { ...device, username, password }; + const updatedDevice = { ...device, username, password, vendor }; onSave(updatedDevice); setSaved(true); setTimeout(() => { @@ -82,6 +82,40 @@ const DeviceConfigModal = ({ isOpen, onClose, onSave, device }) => { type={'password'} /> + + + 交换机厂商 + setVendor(value[0] || '')} + placeholder={'请选择厂商'} + size={'sm'} + colorPalette={'teal'} + > + + + + + + + + + + + + + + {vendorCollection.items.map((item) => ( + + {item.label} + + ))} + + + + + diff --git a/src/frontend/src/pages/ConfigPage.jsx b/src/frontend/src/pages/ConfigPage.jsx index 8591ce9..4a0ff67 100644 --- a/src/frontend/src/pages/ConfigPage.jsx +++ b/src/frontend/src/pages/ConfigPage.jsx @@ -22,22 +22,16 @@ import ConfigTool from '@/libs/config/ConfigTool'; import { api } from '@/services/api/api'; import Notification from '@/libs/system/Notification'; import Common from '@/libs/common'; -import configEffect from '@/libs/script/configPage/configEffect'; const testMode = ConfigTool.load().testMode; const ConfigPage = () => { const [devices, setDevices] = useState([]); - const [selectedDevice, setSelectedDevice] = useState(''); - const [selectedDeviceConfig, setSelectedDeviceConfig] = useState(''); + const [selectedDevices, setSelectedDevices] = useState([]); + const [deviceConfigs, setDeviceConfigs] = useState({}); const [inputText, setInputText] = useState(''); - const [parsedConfig, setParsedConfig] = useState(''); - const [editableConfig, setEditableConfig] = useState(''); const [applying, setApplying] = useState(false); const [hasParsed, setHasParsed] = useState(false); - const [isPeizhi, setisPeizhi] = useState(false); - const [isApplying, setIsApplying] = useState(false); - const [applyStatus, setApplyStatus] = useState([]); const deviceCollection = createListCollection({ items: devices.map((device) => ({ @@ -52,18 +46,30 @@ const ConfigPage = () => { }, []); const handleParse = async () => { - if (!selectedDevice || !inputText.trim()) { + if (selectedDevices.length === 0 || !inputText.trim()) { Notification.error({ title: '操作失败', - description: '请选择设备并输入配置指令', + description: '请选择至少一个设备并输入配置指令', + }); + return; + } + + const selectedConfigs = devices.filter((device) => selectedDevices.includes(device.ip)); + const deviceWithoutVendor = selectedConfigs.find((d) => !d.vendor || d.vendor.trim() === ''); + if (deviceWithoutVendor) { + Notification.error({ + title: '操作失败', + description: `设备 ${deviceWithoutVendor.name} 暂未配置厂商,请先配置厂商`, }); return; } try { - const performParse = async () => { - return await api.parseCommand(inputText); - }; + const performParse = async () => + await api.parseCommand({ + command: inputText, + devices: selectedConfigs, + }); const resultWrapper = await Notification.promise({ promise: performParse(), @@ -82,11 +88,15 @@ const ConfigPage = () => { }); let result = await resultWrapper.unwrap(); - if (result?.data) { - setParsedConfig(JSON.stringify(result.data)); - setEditableConfig(JSON.stringify(result.data)); + if (result?.data?.config) { + const configMap = {}; + result.data.config.forEach((item) => { + if (item.device?.ip) { + configMap[item.device.ip] = item; + } + }); + setDeviceConfigs(configMap); setHasParsed(true); - setisPeizhi(true); } } catch (error) { console.error('配置解析异常:', error); @@ -98,62 +108,80 @@ const ConfigPage = () => { }; const handleApply = async () => { - if (!editableConfig) { + if (!hasParsed) { Notification.warn({ - title: '配置为空', - description: '请先解析或编辑有效配置', + title: '未解析配置', + description: '请先解析配置再应用', }); return; } setApplying(true); - setIsApplying(true); try { const applyOperation = async () => { if (testMode) { - Common.sleep(1000).then(() => ({ success: true })); - } else { - let commands = JSON.parse(editableConfig)?.config?.commands; - console.log(`commands:${JSON.stringify(commands)}`); - const deviceConfig = JSON.parse(selectedDeviceConfig); - console.log(`deviceConfig:${JSON.stringify(deviceConfig)}`); - if (!deviceConfig.password) { + await Common.sleep(1000); + Notification.success({ + title: '测试模式成功', + description: '配置已模拟应用', + }); + return; + } + const applyPromises = selectedDevices.map(async (ip) => { + const deviceItem = deviceConfigs[ip]; + if (!deviceItem) return; + + const deviceConfig = deviceItem.config; + + if (!deviceItem.device.password) { Notification.warn({ - title: '所选交换机暂未配置用户名(可选)和密码', - description: '请前往交换机设备处配置username和password', + title: `交换机 ${deviceItem.device.name} 暂未配置密码`, + description: '请前往交换机设备处配置用户名和密码', }); - return false; + console.log(JSON.stringify(deviceItem)); + return; } - if (deviceConfig.username || deviceConfig.username.toString() !== '') { - commands.push(`!username=${deviceConfig.username.toString()}`); - } else { - commands.push(`!username=NONE`); + + if (!deviceItem.device.username) { + Notification.warn({ + title: `交换机 ${deviceItem.device.name} 暂未配置用户名,将使用NONE作为用户名`, + }); + deviceItem.device.username = 'NONE'; } - commands.push(`!password=${deviceConfig.password.toString()}`); - const res = await api.applyConfig(selectedDevice, commands); - if (res) { + + const commands = [...deviceConfig.commands]; + + try { + const res = await api.applyConfig( + ip, + commands, + deviceItem.device.username, + deviceItem.device.password + ); Notification.success({ - title: '配置完毕', + title: `配置完毕 - ${deviceItem.device.name}`, description: JSON.stringify(res), }); - } else { + } catch (err) { Notification.error({ - title: '配置过程出现错误', - description: '请检查API提示', + title: `配置过程出现错误 - ${deviceItem.device.name}`, + description: err.message || '请检查API提示', }); } - } + }); + + await Promise.all(applyPromises); }; await Notification.promise({ - promise: applyOperation, + promise: applyOperation(), loading: { title: '配置应用中', description: '正在推送配置到设备...', }, success: { - title: '应用成功', - description: '配置已成功生效', + title: '应用完成', + description: '所有设备配置已推送', }, error: { title: '应用失败', @@ -174,19 +202,16 @@ const ConfigPage = () => { 交换机配置中心 + 选择交换机设备 { - const selectedIp = value[0] ?? ''; - setSelectedDevice(selectedIp); - const fullDeviceConfig = devices.find((device) => device.ip === selectedIp); - setSelectedDeviceConfig(JSON.stringify(fullDeviceConfig)); - }} + value={selectedDevices} + onValueChange={({ value }) => setSelectedDevices(value)} placeholder={'请选择交换机设备'} size={'sm'} colorPalette={'teal'} @@ -202,7 +227,7 @@ const ConfigPage = () => { - + {deviceCollection.items.map((item) => ( @@ -214,13 +239,14 @@ const ConfigPage = () => { + 配置指令输入