Merge remote-tracking branch 'origin/main'

This commit is contained in:
Jerry 2025-08-24 16:15:55 +08:00
commit 8f5ac21be1
30 changed files with 4451 additions and 5569 deletions

View File

@ -1,5 +1,9 @@
# 基于人工智能实现的交换机自动或半自动配置
这只是一个用于应付实验论文的奇葩储存库
猎奇卡顿 · 龟速更新 · 随时跑路
### 技术栈
- **Python3**
- Flask
@ -11,10 +15,6 @@
- Framer-motion
- chakra-ui
- HTML5
### 项目分工
- **后端api人工智能算法** : `3`(主要) & `log_out` & `Jerry`(maybe) 使用python
- **前端管理后台设计**`Jerry`使用react
- **论文撰写**`log_out`
### 各部分说明
@ -22,18 +22,3 @@
[逻辑处理后端](https://github.com/Jerryplusy/AI-powered-switches/blob/main/src/backend/README.md)
### 贡献流程
- **后端api**:
- 对于`3`:直接推送到`main`分支
- 对于`Jerry`&`log_out`:新建额外的`feat``fix`分支,提交推送到自己的分支,然后提交`pullrequest``main`分支并指定`3`审核
- **前端管理后台**:
- 对于`Jerry`:直接推送更新到`main`分支
- 对于`3`&`log_out`:新建额外的`feat``fix`分支,提交推送到自己的分支,然后提交`pullrequest``main`分支并指定`Jerry`审核
- **论文(thesis)**
- 提交`pullrequest`并指定`log_out`审核
### 项目活动时间
2025 6 - 8月

View File

@ -10,7 +10,6 @@ src/backend/
│ ├── __init__.py # 创建 Flask 应用实例
│ ├── api/ # API 路由模块
│ │ ├—── __init__.py # 注册 API 蓝图
│ │ ├── command_parser.py # /api/parse_command 接口
│ │ └── network_config.py # /api/apply_config 接口
│ └── services/ # 核心服务逻辑
│ └── ai_services.py # 调用外部 AI 服务生成配置

View File

@ -1,14 +0,0 @@
from typing import Dict, Any
from src.backend.app.services.ai_services import AIService
from src.backend.config import settings
class CommandParser:
def __init__(self):
self.ai_service = AIService(settings.SILICONFLOW_API_KEY, settings.SILICONFLOW_API_URL)
async def parse(self, command: str) -> Dict[str, Any]:
"""
解析中文命令并返回配置
"""
return await self.ai_service.parse_command(command)

View File

@ -4,13 +4,14 @@ from fastapi import (APIRouter, HTTPException, Response, WebSocket, WebSocketDis
from typing import List
from pydantic import BaseModel
import asyncio
from fastapi.responses import HTMLResponse
from fastapi.responses import HTMLResponse, JSONResponse
import matplotlib.pyplot as plt
import io
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
@ -21,217 +22,9 @@ from ...app.services.traffic_monitor import traffic_monitor
from ...app.models.traffic_models import TrafficRecord, SwitchTrafficRecord
from src.backend.app.api.database import SessionLocal
router = APIRouter(prefix="", tags=["API"])
scanner = NetworkScanner()
@router.get("/", include_in_schema=False)
async def root():
return {
"message": "欢迎使用AI交换机配置系统",
"docs": f"{settings.API_PREFIX}/docs",
"redoc": f"{settings.API_PREFIX}/redoc",
"endpoints": [
"/parse_command",
"/apply_config",
"/scan_network",
"/list_devices",
"/batch_apply_config"
"/traffic/switch/current",
"/traffic/switch/history"
]
}
@router.get("/favicon.ico", include_in_schema=False)
async def favicon():
return Response(status_code=204)
class BatchConfigRequest(BaseModel):
config: dict
switch_ips: List[str]
username: str = None
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"}
@router.get("/scan_network", summary="扫描网络中的交换机")
async def scan_network(subnet: str = "192.168.1.0/24"):
try:
devices = scanner.scan_subnet(subnet)
return {
"success": True,
"devices": devices,
"count": len(devices)
}
except Exception as e:
raise HTTPException(500, f"扫描失败: {str(e)}")
@router.get("/list_devices", summary="列出已发现的交换机")
async def list_devices():
return {
"devices": scanner.load_cached_devices()
}
class CommandRequest(BaseModel):
command: str
class ConfigRequest(BaseModel):
config: dict
switch_ip: str
username: str = None
password: str = None
timeout: int = None
@router.post("/parse_command", response_model=dict)
async def parse_command(request: CommandRequest):
"""
解析中文命令并返回JSON配置
"""
try:
ai_service = AIService(settings.SILICONFLOW_API_KEY, settings.SILICONFLOW_API_URL)
config = await ai_service.parse_command(request.command)
return {"success": True, "config": config}
except Exception as e:
raise HTTPException(
status_code=400,
detail=f"Failed to parse command: {str(e)}"
)
@router.post("/apply_config", response_model=dict)
async def apply_config(request: ConfigRequest):
"""
应用配置到交换机
"""
try:
configurator = SwitchConfigurator(
username=request.username,
password=request.password,
timeout=request.timeout
)
result = await configurator.safe_apply(request.switch_ip, request.config)
return {"success": True, "result": result}
except Exception as e:
raise HTTPException(
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命令
支持在commands中嵌入凭据:
!username=admin
!password=cisco123
"""
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(
ip=request.switch_ip,
commands=request.commands
)
return {
"success": True,
"output": result,
"mode": "eNSP" if request.is_ensp else "SSH"
}
except Exception as e:
raise HTTPException(500, detail=str(e))
@router.get("/traffic/interfaces", summary="获取所有网络接口")
async def get_network_interfaces():
return {
"interfaces": traffic_monitor.get_interfaces()
}
@router.get("/traffic/current", summary="获取当前流量数据")
async def get_current_traffic(interface: str = None):
return traffic_monitor.get_current_traffic(interface)
@router.get("/traffic/history", summary="获取流量历史数据")
async def get_traffic_history(interface: str = None, limit: int = 100):
history = traffic_monitor.get_traffic_history(interface)
return {
"sent": history["sent"][-limit:],
"recv": history["recv"][-limit:],
"time": [t.isoformat() for t in history["time"]][-limit:]
}
@router.get("/traffic/records", summary="获取流量记录")
async def get_traffic_records(interface: str = None, limit: int = 100):
with SessionLocal() as session:
query = session.query(TrafficRecord)
if interface:
query = query.filter(TrafficRecord.interface == interface)
records = query.order_by(TrafficRecord.timestamp.desc()).limit(limit).all()
return [record.to_dict() for record in records]
@router.websocket("/ws/traffic")
async def websocket_traffic(websocket: WebSocket):
"""实时流量WebSocket"""
await websocket.accept()
try:
while True:
traffic_data = traffic_monitor.get_current_traffic()
await websocket.send_json(traffic_data)
await asyncio.sleep(1)
except WebSocketDisconnect:
print("客户端断开连接")
@router.get("/", include_in_schema=False)
async def root():
@ -251,11 +44,163 @@ async def root():
}
@router.get("/favicon.ico", include_in_schema=False)
async def favicon():
return Response(status_code=204)
class BatchConfigRequest(BaseModel):
config: dict
switch_ips: List[str]
username: str = None
password: str = None
timeout: int = None
@router.get("/test")
async def test_endpoint():
return {"message": "Hello World"}
@router.get("/scan_network", summary="扫描网络中的交换机")
async def scan_network(subnet: str = "192.168.1.0/24"):
try:
devices = await asyncio.to_thread(scanner.scan_subnet, subnet)
return {
"success": True,
"devices": devices,
"count": len(devices)
}
except Exception as e:
raise HTTPException(500, f"扫描失败: {str(e)}")
@router.get("/list_devices", summary="列出已发现的交换机")
async def list_devices():
return {
"devices": await asyncio.to_thread(scanner.load_cached_devices)
}
class DeviceItem(BaseModel):
name: str
ip: str
vendor: str
class CommandRequest(BaseModel):
command: str
devices: List[DeviceItem]
@router.post("/parse_command", response_model=dict)
async def parse_command(request: CommandRequest):
"""解析中文命令并返回每台设备的配置 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, [d.dict() for d in request.devices])
return {"success": True, "config": config.get("results", [])}
except Exception as e:
raise HTTPException(
status_code=400,
detail=f"Failed to parse command: {str(e)}"
)
@router.post("/apply_config", response_model=dict)
async def apply_config(request: ConfigRequest):
"""应用配置到交换机"""
try:
configurator = SwitchConfigurator(
username=request.username,
password=request.password,
timeout=request.timeout,
vendor=request.vendor
)
result = await configurator.safe_apply(request.switch_ip, request.config)
return {"success": True, "result": result}
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"Failed to apply config: {str(e)}"
)
@router.post("/execute_cli_commands", response_model=dict)
async def execute_cli_commands(request: CLICommandRequest):
"""执行前端生成的CLI命令"""
try:
username, password = request.extract_credentials()
configurator = SwitchConfigurator(
username=username,
password=password,
timeout=settings.SWITCH_TIMEOUT,
)
result = await configurator.execute_raw_commands(
ip=request.switch_ip,
commands=request.commands
)
return {
"success": True,
"output": result,
}
except Exception as e:
raise HTTPException(500, detail=str(e))
@router.get("/traffic/interfaces", summary="获取所有网络接口")
async def get_network_interfaces():
return {
"interfaces": await asyncio.to_thread(traffic_monitor.get_interfaces)
}
@router.get("/traffic/current", summary="获取当前流量数据")
async def get_current_traffic(interface: str = None):
return await asyncio.to_thread(traffic_monitor.get_current_traffic, interface)
@router.get("/traffic/history", summary="获取流量历史数据")
async def get_traffic_history(interface: str = None, limit: int = 100):
history = await asyncio.to_thread(traffic_monitor.get_traffic_history, interface)
return {
"sent": history["sent"][-limit:],
"recv": history["recv"][-limit:],
"time": [t.isoformat() for t in history["time"]][-limit:]
}
@router.get("/traffic/records", summary="获取流量记录")
async def get_traffic_records(interface: str = None, limit: int = 100):
def sync_get_records():
with SessionLocal() as session:
query = session.query(TrafficRecord)
if interface:
query = query.filter(TrafficRecord.interface == interface)
records = query.order_by(TrafficRecord.timestamp.desc()).limit(limit).all()
return [record.to_dict() for record in records]
return await asyncio.to_thread(sync_get_records)
@router.websocket("/ws/traffic")
async def websocket_traffic(websocket: WebSocket):
await websocket.accept()
try:
while True:
traffic_data = await asyncio.to_thread(traffic_monitor.get_current_traffic)
await websocket.send_json(traffic_data)
await asyncio.sleep(1)
except WebSocketDisconnect:
print("客户端断开连接")
@router.get("/traffic/switch/interfaces", summary="获取交换机的网络接口")
async def get_switch_interfaces(switch_ip: str):
"""获取指定交换机的所有接口"""
try:
monitor = get_switch_monitor(switch_ip)
interfaces = list(monitor.interface_oids.keys())
@ -268,13 +213,45 @@ async def get_switch_interfaces(switch_ip: str):
raise HTTPException(500, f"获取接口失败: {str(e)}")
async def get_interface_current_traffic(switch_ip: str, interface: str) -> dict:
"""获取指定交换机接口的当前流量数据"""
try:
def sync_get_record():
with SessionLocal() as session:
record = session.query(SwitchTrafficRecord).filter(
SwitchTrafficRecord.switch_ip == switch_ip,
SwitchTrafficRecord.interface == interface
).order_by(SwitchTrafficRecord.timestamp.desc()).first()
if not record:
return {
"switch_ip": switch_ip,
"interface": interface,
"rate_in": 0.0,
"rate_out": 0.0,
"bytes_in": 0,
"bytes_out": 0
}
return {
"switch_ip": switch_ip,
"interface": interface,
"rate_in": record.rate_in,
"rate_out": record.rate_out,
"bytes_in": record.bytes_in,
"bytes_out": record.bytes_out
}
return await asyncio.to_thread(sync_get_record)
except Exception as e:
logger.error(f"获取接口流量失败: {str(e)}")
raise HTTPException(500, f"获取接口流量失败: {str(e)}")
@router.get("/traffic/switch/current", summary="获取交换机的当前流量数据")
async def get_switch_current_traffic(switch_ip: str, interface: str = None):
"""获取交换机的当前流量数据"""
try:
monitor = get_switch_monitor(switch_ip)
if not interface:
traffic_data = {}
for iface in monitor.interface_oids:
@ -283,78 +260,44 @@ async def get_switch_current_traffic(switch_ip: str, interface: str = None):
"switch_ip": switch_ip,
"traffic": traffic_data
}
return await get_interface_current_traffic(switch_ip, interface)
except Exception as e:
logger.error(f"获取交换机流量失败: {str(e)}")
raise HTTPException(500, f"获取流量失败: {str(e)}")
async def get_interface_current_traffic(switch_ip: str, interface: str) -> dict:
"""获取指定交换机接口的当前流量数据"""
try:
with SessionLocal() as session:
record = session.query(SwitchTrafficRecord).filter(
SwitchTrafficRecord.switch_ip == switch_ip,
SwitchTrafficRecord.interface == interface
).order_by(SwitchTrafficRecord.timestamp.desc()).first()
if not record:
return {
"switch_ip": switch_ip,
"interface": interface,
"rate_in": 0.0,
"rate_out": 0.0,
"bytes_in": 0,
"bytes_out": 0
}
return {
"switch_ip": switch_ip,
"interface": interface,
"rate_in": record.rate_in,
"rate_out": record.rate_out,
"bytes_in": record.bytes_in,
"bytes_out": record.bytes_out
}
except Exception as e:
logger.error(f"获取接口流量失败: {str(e)}")
raise HTTPException(500, f"获取接口流量失败: {str(e)}")
@router.get("/traffic/switch/history", summary="获取交换机的流量历史数据")
async def get_switch_traffic_history(switch_ip: str, interface: str = None, minutes: int = 10):
"""获取交换机的流量历史数据"""
try:
monitor = get_switch_monitor(switch_ip)
if not interface:
return {
"switch_ip": switch_ip,
"history": monitor.get_traffic_history()
"history": await asyncio.to_thread(monitor.get_traffic_history)
}
with SessionLocal() as session:
time_threshold = datetime.now() - timedelta(minutes=minutes)
def sync_get_history():
with SessionLocal() as session:
time_threshold = datetime.now() - timedelta(minutes=minutes)
records = session.query(SwitchTrafficRecord).filter(
SwitchTrafficRecord.switch_ip == switch_ip,
SwitchTrafficRecord.interface == interface,
SwitchTrafficRecord.timestamp >= time_threshold
).order_by(SwitchTrafficRecord.timestamp.asc()).all()
records = session.query(SwitchTrafficRecord).filter(
SwitchTrafficRecord.switch_ip == switch_ip,
SwitchTrafficRecord.interface == interface,
SwitchTrafficRecord.timestamp >= time_threshold
).order_by(SwitchTrafficRecord.timestamp.asc()).all()
history_data = {
"in": [record.rate_in for record in records],
"out": [record.rate_out for record in records],
"time": [record.timestamp.isoformat() for record in records]
}
return history_data
history_data = {
"in": [record.rate_in for record in records],
"out": [record.rate_out for record in records],
"time": [record.timestamp.isoformat() for record in records]
}
return {
"switch_ip": switch_ip,
"interface": interface,
"history": history_data
}
history_data = await asyncio.to_thread(sync_get_history)
return {
"switch_ip": switch_ip,
"interface": interface,
"history": history_data
}
except Exception as e:
logger.error(f"获取历史流量失败: {str(e)}")
raise HTTPException(500, f"获取历史流量失败: {str(e)}")
@ -362,11 +305,9 @@ async def get_switch_traffic_history(switch_ip: str, interface: str = None, minu
@router.websocket("/ws/traffic/switch")
async def websocket_switch_traffic(websocket: WebSocket, switch_ip: str, interface: str = None):
"""交换机实时流量WebSocket"""
await websocket.accept()
try:
monitor = get_switch_monitor(switch_ip)
while True:
if interface:
traffic_data = await get_interface_current_traffic(switch_ip, interface)
@ -375,12 +316,10 @@ async def websocket_switch_traffic(websocket: WebSocket, switch_ip: str, interfa
traffic_data = {}
for iface in monitor.interface_oids:
traffic_data[iface] = await get_interface_current_traffic(switch_ip, iface)
await websocket.send_json({
"switch_ip": switch_ip,
"traffic": traffic_data
})
await asyncio.sleep(1)
except WebSocketDisconnect:
logger.info(f"客户端断开交换机流量连接: {switch_ip}")
@ -388,33 +327,35 @@ async def websocket_switch_traffic(websocket: WebSocket, switch_ip: str, interfa
logger.error(f"交换机流量WebSocket错误: {str(e)}")
await websocket.close(code=1011, reason=str(e))
@router.get("/traffic/switch/plot", response_class=HTMLResponse, summary="交换机流量可视化")
async def plot_switch_traffic(switch_ip: str, interface: str, minutes: int = 10):
"""生成交换机流量图表"""
try:
history = await get_switch_traffic_history(switch_ip, interface, minutes)
history_data = history["history"]
time_points = [datetime.fromisoformat(t) for t in history_data["time"]]
in_rates = history_data["in"]
out_rates = history_data["out"]
plt.figure(figsize=(12, 6))
plt.plot(time_points, in_rates, label="流入流量 (B/s)")
plt.plot(time_points, out_rates, label="流出流量 (B/s)")
plt.title(f"交换机 {switch_ip} 接口 {interface} 流量监控 - 最近 {minutes} 分钟")
plt.xlabel("时间")
plt.ylabel("流量 (字节/秒)")
plt.legend()
plt.grid(True)
plt.xticks(rotation=45)
plt.tight_layout()
buf = io.BytesIO()
plt.savefig(buf, format="png")
buf.seek(0)
image_base64 = base64.b64encode(buf.read()).decode("utf-8")
plt.close()
def generate_plot():
plt.figure(figsize=(12, 6))
plt.plot(time_points, in_rates, label="流入流量 (B/s)")
plt.plot(time_points, out_rates, label="流出流量 (B/s)")
plt.title(f"交换机 {switch_ip} 接口 {interface} 流量监控 - 最近 {minutes} 分钟")
plt.xlabel("时间")
plt.ylabel("流量 (字节/秒)")
plt.legend()
plt.grid(True)
plt.xticks(rotation=45)
plt.tight_layout()
buf = io.BytesIO()
plt.savefig(buf, format="png")
buf.seek(0)
image_base64 = base64.b64encode(buf.read()).decode("utf-8")
plt.close()
return image_base64
image_base64 = await asyncio.to_thread(generate_plot)
return f"""
<html>
<head>
@ -441,24 +382,24 @@ async def plot_switch_traffic(switch_ip: str, interface: str, minutes: int = 10)
@router.get("/network_adapters", summary="获取网络适配器网段")
async def get_network_adapters():
try:
net_if_addrs = psutil.net_if_addrs()
networks = []
for interface, addrs in net_if_addrs.items():
for addr in addrs:
if addr.family == socket.AF_INET:
ip = addr.address
netmask = addr.netmask
network = ipaddress.IPv4Network(f"{ip}/{netmask}", strict=False)
networks.append({
"adapter": interface,
"network": str(network),
"ip": ip,
"subnet_mask": netmask
})
def sync_get_adapters():
net_if_addrs = psutil.net_if_addrs()
networks = []
for interface, addrs in net_if_addrs.items():
for addr in addrs:
if addr.family == socket.AF_INET:
ip = addr.address
netmask = addr.netmask
network = ipaddress.IPv4Network(f"{ip}/{netmask}", strict=False)
networks.append({
"adapter": interface,
"network": str(network),
"ip": ip,
"subnet_mask": netmask
})
return networks
networks = await asyncio.to_thread(sync_get_adapters)
return {"networks": networks}
except Exception as e:
return {"error": f"获取网络适配器信息失败: {str(e)}"}

View File

@ -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,250 +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:
logger.info(f"发送命令: {cmd}")
if cmd.startswith("!"):
logger.debug(f"跳过特殊命令: {cmd}")
continue
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

View File

@ -1,5 +1,6 @@
from pydantic import BaseModel
from typing import Optional
from typing import Optional, List
class BaseResponse(BaseModel):
success: bool
@ -19,5 +20,21 @@ class ConfigHistory(BaseModel):
timestamp: float
status: str # success/failed
error: Optional[str] = None
class TrafficReport(BaseModel):
report_type: str
period: str
interface_stats: List[dict]
trend_chart: Optional[str] = None # base64 encoded image
class TopologyVisualization(BaseModel):
nodes: List[dict]
edges: List[dict]
image: Optional[str] = None # base64 encoded image
class ConfigValidationResult(BaseModel):
valid: bool
errors: List[str]
has_security_risks: bool
warnings: Optional[List[str]] = None
__all__ = ["BaseResponse", "SwitchInfo", "ConfigHistory"]

View File

@ -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"

View File

@ -1,40 +1,47 @@
from typing import Dict, Any, Coroutine
import httpx
from openai import OpenAI
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 = OpenAI(
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) -> Any | None:
async def parse_command(self, command: str, devices: List[Dict]) -> Dict[str, Any]:
"""
调用硅基流动API解析中文命令
"""
prompt = """
你是一个网络设备配置专家精通各种类型的路由器的配置,请将以下用户的中文命令转换为网络设备配置JSON
但是请注意由于贪婪的人们追求极高的效率所以你必须严格按照 JSON 格式返回数据不要包含任何额外文本或 Markdown 代码块
返回格式要求
1. 必须包含'type'字段指明配置类型(vlan/interface/acl/route等)
2. 必须包含'commands'字段包含可直接执行的命令列表
3. 其他参数根据配置类型动态添加
4. 不要包含解释性文本步骤说明或注释
5.要包含使用ssh连接交换机后的完整命令包括但不完全包括system-view退出保存等完整操作注意保存还需要输入Y
示例命令'创建VLAN 100名称为TEST'
示例返回{"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到用户视图
针对一组设备和一条自然语言命令生成每台设备的配置 JSON
"""
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"""
你是一个网络设备配置专家现在有以下设备
{devices_str}
用户输入了一条命令{command}
你的任务
- 为每台设备分别生成配置
- 输出一个 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),
@ -42,32 +49,21 @@ class AIService:
]
try:
response = self.client.chat.completions.create(
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)
)

View File

@ -1,34 +0,0 @@
import networkx as nx
from scipy.optimize import minimize
class NetworkOptimizer:
def __init__(self, devices):
"""基于图论的网络优化模型"""
self.graph = self.build_topology_graph(devices)
def build_topology_graph(self, devices):
"""构建网络拓扑图"""
G = nx.Graph()
for device in devices:
G.add_node(device['ip'], type=device['type'])
G.add_edge('192.168.1.1', '192.168.1.2', bandwidth=1000)
return G
def optimize_path(self, source, target):
"""计算最优路径"""
return nx.shortest_path(self.graph, source, target)
def bandwidth_optimization(self):
"""带宽优化模型"""
def objective(x):
return max(x)
constraints = (
{'type': 'eq', 'fun': lambda x: sum(x) - total_bandwidth}
)
result = minimize(objective, initial_guess, constraints=constraints)
return result.x

View File

@ -48,4 +48,4 @@ class NetworkScanner:
return []
with open(self.cache_path) as f:
return json.load(f)
return json.load(f)

View File

@ -7,7 +7,7 @@ from ..models.traffic_models import SwitchTrafficRecord
from src.backend.app.api.database import SessionLocal
from ..utils.logger import logger
#V=ΔQ'-ΔQ/Δt (B/s)
class SwitchTrafficMonitor:
def __init__(
self,

View File

@ -0,0 +1,9 @@
# test_linprog.py
import numpy as np
from scipy.optimize import linprog
c = np.array([-1, -2])
A_ub = np.array([[1, 1]])
b_ub = np.array([3])
res = linprog(c, A_ub=A_ub, b_ub=b_ub, method='highs')
print(res)

View File

@ -8,6 +8,7 @@ from typing import Dict, Optional, List
from ..models.traffic_models import TrafficRecord
from src.backend.app.api.database import SessionLocal
from ..utils.logger import logger
class TrafficMonitor:
@ -33,7 +34,7 @@ class TrafficMonitor:
if not self.running:
self.running = True
self.task = asyncio.create_task(self._monitor_loop())
print("流量监控已启动")
logger.info("流量监控已启动")
async def stop_monitoring(self):
"""停止流量监控"""
@ -44,7 +45,7 @@ class TrafficMonitor:
await self.task
except asyncio.CancelledError:
pass
print("流量监控已停止")
logger.info("流量监控已停止")
async def _monitor_loop(self):
"""监控主循环"""

View File

@ -10,13 +10,14 @@ httpx==0.27.0
python-nmap==0.7.1
pysnmp==7.1.21
aiofiles==23.2.1
pandas==2.3.1
loguru==0.7.2
tenacity==8.2.3
networkx==3.5
asyncio==3.4.3
typing_extensions==4.10.0
scapy
scipy==1.16.1
scapy==2.6.1
psutil==5.9.8
matplotlib==3.8.3
sqlalchemy==2.0.28

View File

@ -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'}
/>
</Field.Root>
<Field.Root>
<Field.Label>交换机厂商</Field.Label>
<Select.Root
collection={vendorCollection}
value={vendor ? [vendor] : []}
onValueChange={({ value }) => setVendor(value[0] || '')}
placeholder={'请选择厂商'}
size={'sm'}
colorPalette={'teal'}
>
<Select.HiddenSelect />
<Select.Control>
<Select.Trigger>
<Select.ValueText />
</Select.Trigger>
<Select.IndicatorGroup>
<Select.Indicator />
<Select.ClearTrigger />
</Select.IndicatorGroup>
</Select.Control>
<Portal>
<Select.Positioner style={{ zIndex: 1500 }}>
<Select.Content>
{vendorCollection.items.map((item) => (
<Select.Item key={item.value} item={item}>
{item.label}
</Select.Item>
))}
</Select.Content>
</Select.Positioner>
</Portal>
</Select.Root>
</Field.Root>
</Stack>
</DialogBody>

View File

@ -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 = () => {
<Heading fontSize={'xl'} color={'teal.300'}>
交换机配置中心
</Heading>
<Field.Root>
<Field.Label fontWeight={'bold'} mb={1} fontSize="sm">
选择交换机设备
</Field.Label>
<Select.Root
multiple
collection={deviceCollection}
value={selectedDevice ? [selectedDevice] : []}
onValueChange={({ value }) => {
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 = () => {
</Select.IndicatorGroup>
</Select.Control>
<Portal>
<Select.Positioner>
<Select.Positioner style={{ zIndex: 1500 }}>
<Select.Content>
{deviceCollection.items.map((item) => (
<Select.Item key={item.value} item={item}>
@ -214,13 +239,14 @@ const ConfigPage = () => {
</Portal>
</Select.Root>
</Field.Root>
<Field.Root>
<Field.Label fontWeight={'bold'} mb={1} fontSize="sm">
配置指令输入
</Field.Label>
<Textarea
rows={4}
placeholder={'例创建VLAN 10并配置IP 192.168.10.1/24并在端口1启用SSH访问"'}
placeholder={'例创建VLAN 10并配置IP 192.168.10.1/24并在端口1启用SSH访问'}
value={inputText}
colorPalette={'teal'}
orientation={'vertical'}
@ -229,30 +255,27 @@ const ConfigPage = () => {
size={'sm'}
/>
</Field.Root>
<Button
colorScheme={'teal'}
variant={'solid'}
size={'sm'}
onClick={handleParse}
isDisabled={!selectedDevice || !inputText.trim()}
isDisabled={selectedDevices.length === 0 || !inputText.trim()}
>
解析配置
</Button>
{isPeizhi && parsedConfig && (
{hasParsed && selectedDevices.length > 0 && (
<FadeInWrapper delay={0.2}>
<VStack spacing={4} align={'stretch'}>
{(() => {
let parsed;
try {
parsed = JSON.parse(editableConfig);
} catch (e) {
return <Text color={'red.300'}>配置 JSON 格式错误无法解析</Text>;
}
const config = parsed.config ? [parsed.config] : parsed;
return config.map((cfg, idx) => (
{selectedDevices.map((ip) => {
const item = deviceConfigs[ip];
if (!item) return null;
const cfg = item.config;
return (
<Box
key={idx}
key={ip}
p={4}
bg={'whiteAlpha.100'}
borderRadius={'xl'}
@ -260,7 +283,7 @@ const ConfigPage = () => {
borderColor={'whiteAlpha.300'}
>
<Text fontSize={'lg'} fontWeight={'bold'} mb={2}>
配置类型: {cfg.type}
设备: {item.device.name} ({ip}) - 配置类型: {cfg.type}
</Text>
{Object.entries(cfg).map(([key, value]) => {
@ -278,9 +301,16 @@ const ConfigPage = () => {
value={value}
onChange={(e) => {
const newVal = e.target.value;
const updated = JSON.parse(editableConfig);
updated.config[key] = newVal;
setEditableConfig(JSON.stringify(updated, null, 2));
setDeviceConfigs((prev) => ({
...prev,
[ip]: {
...prev[ip],
config: {
...prev[ip].config,
[key]: newVal,
},
},
}));
}}
/>
</Field.Root>
@ -299,20 +329,26 @@ const ConfigPage = () => {
value={cmd}
onChange={(e) => {
const newCmd = e.target.value;
const updated = JSON.parse(editableConfig);
updated.config.commands[i] = newCmd;
setEditableConfig(JSON.stringify(updated, null, 2));
setDeviceConfigs((prev) => {
const updated = { ...prev };
updated[ip].config.commands[i] = newCmd;
return updated;
});
}}
/>
</Field.Root>
))}
<HStack mt={4} spacing={3} justify={'flex-end'}>
<Button
variant={'outline'}
colorScheme={'gray'}
size={'sm'}
onClick={() => {
setEditableConfig(parsedConfig);
setDeviceConfigs((prev) => ({
...prev,
[ip]: item,
}));
Notification.success({
title: '成功重置配置!',
description: '现在您可以重新审查生成的配置',
@ -341,63 +377,14 @@ const ConfigPage = () => {
size={'sm'}
onClick={handleApply}
isLoading={applying}
isDisabled={!editableConfig}
isDisabled={!cfg.commands || cfg.commands.length === 0}
>
应用到交换机
</Button>
</HStack>
</Box>
));
})()}
{
<FadeInWrapper delay={0.2}>
<VStack spacing={4} align={'stretch'}>
<Box
p={4}
bg={'whiteAlpha.100'}
borderRadius={'xl'}
border={'1px solid'}
borderColor={'whiteAlpha.300'}
>
<Text fontSize={'lg'} fontWeight={'bold'} mb={2}>
应用配置命令
</Text>
<Box>
{JSON.parse(editableConfig).config?.commands.map((command, index) => (
<HStack key={index} mb={2}>
<Text fontSize={'sm'} flex={1}>
{command}
</Text>
<Spinner
size={'sm'}
color={applyStatus[index] === 'success' ? 'green.500' : 'red.500'}
display={
applyStatus[index] === 'pending' ||
applyStatus[index] === 'in-progress'
? 'inline-block'
: 'none'
}
/>
<Text
color={applyStatus[index] === 'success' ? 'green.500' : 'red.500'}
ml={2}
>
{applyStatus[index] === 'success'
? '成功'
: applyStatus[index] === 'failed'
? '失败'
: applyStatus[index] === 'in-progress'
? '正在应用'
: ''}
</Text>
</HStack>
))}
</Box>
</Box>
</VStack>
</FadeInWrapper>
}
);
})}
</VStack>
</FadeInWrapper>
)}

View File

@ -25,7 +25,7 @@ export const api = {
/**
* 扫描网络
* @param subnet 子网地址
* @param {string} subnet 子网地址
* @returns {Promise<axios.AxiosResponse<any>>}
*/
scan: (subnet) => axios.get(buildUrl('/api/scan_network'), { params: { subnet } }),
@ -38,19 +38,25 @@ export const api = {
/**
* 解析命令
* @param text 文本
* @param {Object} payload
* @param {string} payload.command - 自然语言命令
* @param {Array<Object>} payload.devices - 设备列表
* 每个对象包含 { id: string, ip: string, vendor: string(huawei/cisco/h3c/ruijie/zte) }
* @returns {Promise<axios.AxiosResponse<any>>}
*/
parseCommand: (text) => axios.post(buildUrl('/api/parse_command'), { command: text }),
parseCommand: ({ command, devices }) =>
axios.post(buildUrl('/api/parse_command'), { command, devices }),
/**
* 应用配置
* @param switch_ip 交换机ip
* @param commands 配置,为数组[]
* @param {string} switch_ip 交换机IP
* @param {Array<string>} commands 配置命令数组
* @param username 用户名无时使用NONE
* @param password 密码
* @returns {Promise<axios.AxiosResponse<any>>}
*/
applyConfig: (switch_ip, commands) =>
axios.post(buildUrl('/api/execute_cli_commands'), { switch_ip: switch_ip, commands: commands }),
applyConfig: (switch_ip, commands, username, password) =>
axios.post(buildUrl('/api/execute_cli_commands'), { switch_ip, commands, username, password }),
/**
* 获取网络适配器信息
@ -60,7 +66,7 @@ export const api = {
/**
* 更新基础URL
* @param url
* @param {string} url
*/
updateBaseUrl: (url) => {
const config = ConfigTool.load();
@ -77,7 +83,7 @@ export const getConfig = () => ConfigTool.load();
/**
* 获取基础URL
* @returns {string|string}
* @returns {string}
*/
export const getBaseUrl = () => ConfigTool.load().backendUrl || '';

View File

@ -21,6 +21,7 @@
"@nestjs/platform-express": "^11.0.1",
"@nestjs/swagger": "^11.2.0",
"axios": "^1.10.0",
"ip": "^2.0.1",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1",
"ssh2": "^1.16.0"
@ -34,6 +35,7 @@
"@swc/cli": "^0.6.0",
"@swc/core": "^1.10.7",
"@types/express": "^5.0.0",
"@types/ip": "^1.1.3",
"@types/jest": "^29.5.14",
"@types/node": "^22.16.4",
"@types/supertest": "^6.0.2",

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,13 @@
import { Module } from '@nestjs/common';
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import { RootModule } from './root/root.module';
import { HttpMiddleware } from './common/middleware/http.middleware';
import { NetworkModule } from './modules/network/network.module';
@Module({
imports: [RootModule],
imports: [RootModule, NetworkModule],
})
export class AppModule {}
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer): any {
consumer.apply(HttpMiddleware).forRoutes('api');
}
}

View File

@ -1,28 +0,0 @@
import {
ArgumentsHost,
Catch,
ExceptionFilter,
HttpException,
HttpStatus,
} from '@nestjs/common';
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const status =
exception instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;
const message =
exception instanceof HttpException ? exception.message : '服务器内部错误';
response.status(status).json({
success: false,
data: null,
message,
});
}
}

View File

@ -19,7 +19,6 @@ export class ResponseInterceptor<T>
map((data) => ({
success: true,
data,
message: '操作成功',
})),
);
}

View File

@ -0,0 +1,25 @@
import { Injectable, Logger, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
@Injectable()
export class HttpMiddleware implements NestMiddleware {
private readonly logger = new Logger('HTTP');
use(req: Request, res: Response, next: NextFunction): void {
const { method, originalUrl } = req;
this.logger.log(`HTTP request for ${method} ${originalUrl}`);
const startTime = Date.now();
res.on('finish', () => {
const statusCode = res.statusCode;
const contentLength = res.get('content-length') || 0;
const duration = Date.now() - startTime;
this.logger.log(
`${method} ${originalUrl} ${statusCode} ${contentLength}B - ${duration}ms`,
);
});
next();
}
}

View File

@ -1,5 +1,4 @@
export interface ApiResponse<T = any> {
success: boolean;
data: T;
message: string;
}

View File

@ -3,13 +3,11 @@ import { AppModule } from './app.module';
import { Logger } from '@nestjs/common';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { ResponseInterceptor } from './common/interceptors/response.interceptor';
import { AllExceptionsFilter } from './common/filters/all-exception.filter';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.setGlobalPrefix('api');
app.useGlobalInterceptors(new ResponseInterceptor());
app.useGlobalFilters(new AllExceptionsFilter());
const config = new DocumentBuilder()
.setTitle('交换机API平台')
.setDescription('自动化交换机配置和流量监控接口')

View File

@ -0,0 +1,18 @@
import { Controller, Get, Inject } from '@nestjs/common';
import { NetworkService } from './network.service';
import { ApiTags, ApiOperation } from '@nestjs/swagger';
@ApiTags('Network')
@Controller('network')
export class NetworkController {
constructor(
@Inject(NetworkService)
private readonly networkService: NetworkService,
) {}
@Get('adapters')
@ApiOperation({ summary: '获取网络适配器网段' })
async getNetworkAdapters() {
return this.networkService.getNetworkAdapters();
}
}

View File

@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { NetworkService } from './network.service';
import { NetworkController } from './network.controller';
@Module({
controllers: [NetworkController],
providers: [NetworkService],
exports: [NetworkService],
})
export class NetworkModule {}

View File

@ -0,0 +1,44 @@
import { Injectable, Logger } from '@nestjs/common';
import os from 'os';
import ip from 'ip';
@Injectable()
export class NetworkService {
private readonly logger = new Logger(NetworkService.name);
/**
*
*/
async getNetworkAdapters(): Promise<any> {
try {
this.logger.log('Getting network adapters');
const interfaces = os.networkInterfaces();
const networks: any[] = [];
for (const [adapter, addrs] of Object.entries(interfaces)) {
if (!addrs) continue;
for (const addr of addrs) {
if (addr.family === 'IPv4' && !addr.internal) {
const ipAddress = addr.address;
const subnetMask = addr.netmask;
const cidr = ip.subnet(ipAddress, subnetMask);
const networkCidr = `${cidr.networkAddress}/${cidr.subnetMaskLength}`;
networks.push({
adapter,
network: networkCidr,
ip: ipAddress,
subnet_mask: subnetMask,
});
}
}
}
return { networks };
} catch (error) {
this.logger.error('获取网络适配器信息失败', error);
return { error: `获取网络适配器信息失败: ${error.message}` };
}
}
}

View File

@ -3,7 +3,7 @@ import { Controller, Get } from '@nestjs/common';
@Controller()
export class RootController {
@Get()
getWelcome() {
async getWelcome() {
return {
message: '欢迎使用交换机管理平台 API',
};

View File

@ -1,5 +1,6 @@
import { Module } from '@nestjs/common';
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import { RootController } from './root.controller';
import { HttpMiddleware } from '../common/middleware/http.middleware';
@Module({
controllers: [RootController],