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** - **Python3**
- Flask - Flask
@ -11,10 +15,6 @@
- Framer-motion - Framer-motion
- chakra-ui - chakra-ui
- HTML5 - 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) [逻辑处理后端](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 应用实例 │ ├── __init__.py # 创建 Flask 应用实例
│ ├── api/ # API 路由模块 │ ├── api/ # API 路由模块
│ │ ├—── __init__.py # 注册 API 蓝图 │ │ ├—── __init__.py # 注册 API 蓝图
│ │ ├── command_parser.py # /api/parse_command 接口
│ │ └── network_config.py # /api/apply_config 接口 │ │ └── network_config.py # /api/apply_config 接口
│ └── services/ # 核心服务逻辑 │ └── services/ # 核心服务逻辑
│ └── ai_services.py # 调用外部 AI 服务生成配置 │ └── 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 typing import List
from pydantic import BaseModel from pydantic import BaseModel
import asyncio import asyncio
from fastapi.responses import HTMLResponse from fastapi.responses import HTMLResponse, JSONResponse
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import io import io
import base64 import base64
import psutil import psutil
import ipaddress import ipaddress
from ..models.requests import CLICommandRequest, ConfigRequest
from ..services.switch_traffic_monitor import get_switch_monitor from ..services.switch_traffic_monitor import get_switch_monitor
from ..utils import logger from ..utils import logger
from ...app.services.ai_services import AIService 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 ...app.models.traffic_models import TrafficRecord, SwitchTrafficRecord
from src.backend.app.api.database import SessionLocal from src.backend.app.api.database import SessionLocal
router = APIRouter(prefix="", tags=["API"]) router = APIRouter(prefix="", tags=["API"])
scanner = NetworkScanner() 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) @router.get("/", include_in_schema=False)
async def root(): 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="获取交换机的网络接口") @router.get("/traffic/switch/interfaces", summary="获取交换机的网络接口")
async def get_switch_interfaces(switch_ip: str): async def get_switch_interfaces(switch_ip: str):
"""获取指定交换机的所有接口"""
try: try:
monitor = get_switch_monitor(switch_ip) monitor = get_switch_monitor(switch_ip)
interfaces = list(monitor.interface_oids.keys()) interfaces = list(monitor.interface_oids.keys())
@ -268,13 +213,45 @@ async def get_switch_interfaces(switch_ip: str):
raise HTTPException(500, f"获取接口失败: {str(e)}") 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="获取交换机的当前流量数据") @router.get("/traffic/switch/current", summary="获取交换机的当前流量数据")
async def get_switch_current_traffic(switch_ip: str, interface: str = None): async def get_switch_current_traffic(switch_ip: str, interface: str = None):
"""获取交换机的当前流量数据"""
try: try:
monitor = get_switch_monitor(switch_ip) monitor = get_switch_monitor(switch_ip)
if not interface: if not interface:
traffic_data = {} traffic_data = {}
for iface in monitor.interface_oids: 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, "switch_ip": switch_ip,
"traffic": traffic_data "traffic": traffic_data
} }
return await get_interface_current_traffic(switch_ip, interface) return await get_interface_current_traffic(switch_ip, interface)
except Exception as e: except Exception as e:
logger.error(f"获取交换机流量失败: {str(e)}") logger.error(f"获取交换机流量失败: {str(e)}")
raise HTTPException(500, 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="获取交换机的流量历史数据") @router.get("/traffic/switch/history", summary="获取交换机的流量历史数据")
async def get_switch_traffic_history(switch_ip: str, interface: str = None, minutes: int = 10): async def get_switch_traffic_history(switch_ip: str, interface: str = None, minutes: int = 10):
"""获取交换机的流量历史数据"""
try: try:
monitor = get_switch_monitor(switch_ip) monitor = get_switch_monitor(switch_ip)
if not interface: if not interface:
return { return {
"switch_ip": switch_ip, "switch_ip": switch_ip,
"history": monitor.get_traffic_history() "history": await asyncio.to_thread(monitor.get_traffic_history)
} }
with SessionLocal() as session: def sync_get_history():
time_threshold = datetime.now() - timedelta(minutes=minutes) 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( history_data = {
SwitchTrafficRecord.switch_ip == switch_ip, "in": [record.rate_in for record in records],
SwitchTrafficRecord.interface == interface, "out": [record.rate_out for record in records],
SwitchTrafficRecord.timestamp >= time_threshold "time": [record.timestamp.isoformat() for record in records]
).order_by(SwitchTrafficRecord.timestamp.asc()).all() }
return history_data
history_data = { history_data = await asyncio.to_thread(sync_get_history)
"in": [record.rate_in for record in records], return {
"out": [record.rate_out for record in records], "switch_ip": switch_ip,
"time": [record.timestamp.isoformat() for record in records] "interface": interface,
} "history": history_data
}
return {
"switch_ip": switch_ip,
"interface": interface,
"history": history_data
}
except Exception as e: except Exception as e:
logger.error(f"获取历史流量失败: {str(e)}") logger.error(f"获取历史流量失败: {str(e)}")
raise HTTPException(500, 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") @router.websocket("/ws/traffic/switch")
async def websocket_switch_traffic(websocket: WebSocket, switch_ip: str, interface: str = None): async def websocket_switch_traffic(websocket: WebSocket, switch_ip: str, interface: str = None):
"""交换机实时流量WebSocket"""
await websocket.accept() await websocket.accept()
try: try:
monitor = get_switch_monitor(switch_ip) monitor = get_switch_monitor(switch_ip)
while True: while True:
if interface: if interface:
traffic_data = await get_interface_current_traffic(switch_ip, 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 = {} traffic_data = {}
for iface in monitor.interface_oids: for iface in monitor.interface_oids:
traffic_data[iface] = await get_interface_current_traffic(switch_ip, iface) traffic_data[iface] = await get_interface_current_traffic(switch_ip, iface)
await websocket.send_json({ await websocket.send_json({
"switch_ip": switch_ip, "switch_ip": switch_ip,
"traffic": traffic_data "traffic": traffic_data
}) })
await asyncio.sleep(1) await asyncio.sleep(1)
except WebSocketDisconnect: except WebSocketDisconnect:
logger.info(f"客户端断开交换机流量连接: {switch_ip}") 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)}") logger.error(f"交换机流量WebSocket错误: {str(e)}")
await websocket.close(code=1011, reason=str(e)) await websocket.close(code=1011, reason=str(e))
@router.get("/traffic/switch/plot", response_class=HTMLResponse, summary="交换机流量可视化") @router.get("/traffic/switch/plot", response_class=HTMLResponse, summary="交换机流量可视化")
async def plot_switch_traffic(switch_ip: str, interface: str, minutes: int = 10): async def plot_switch_traffic(switch_ip: str, interface: str, minutes: int = 10):
"""生成交换机流量图表"""
try: try:
history = await get_switch_traffic_history(switch_ip, interface, minutes) history = await get_switch_traffic_history(switch_ip, interface, minutes)
history_data = history["history"] history_data = history["history"]
time_points = [datetime.fromisoformat(t) for t in history_data["time"]] time_points = [datetime.fromisoformat(t) for t in history_data["time"]]
in_rates = history_data["in"] in_rates = history_data["in"]
out_rates = history_data["out"] out_rates = history_data["out"]
plt.figure(figsize=(12, 6)) def generate_plot():
plt.plot(time_points, in_rates, label="流入流量 (B/s)") plt.figure(figsize=(12, 6))
plt.plot(time_points, out_rates, label="流出流量 (B/s)") plt.plot(time_points, in_rates, label="流入流量 (B/s)")
plt.title(f"交换机 {switch_ip} 接口 {interface} 流量监控 - 最近 {minutes} 分钟") plt.plot(time_points, out_rates, label="流出流量 (B/s)")
plt.xlabel("时间") plt.title(f"交换机 {switch_ip} 接口 {interface} 流量监控 - 最近 {minutes} 分钟")
plt.ylabel("流量 (字节/秒)") plt.xlabel("时间")
plt.legend() plt.ylabel("流量 (字节/秒)")
plt.grid(True) plt.legend()
plt.xticks(rotation=45) plt.grid(True)
plt.tight_layout() plt.xticks(rotation=45)
buf = io.BytesIO() plt.tight_layout()
plt.savefig(buf, format="png") buf = io.BytesIO()
buf.seek(0) plt.savefig(buf, format="png")
image_base64 = base64.b64encode(buf.read()).decode("utf-8") buf.seek(0)
plt.close() image_base64 = base64.b64encode(buf.read()).decode("utf-8")
plt.close()
return image_base64
image_base64 = await asyncio.to_thread(generate_plot)
return f""" return f"""
<html> <html>
<head> <head>
@ -441,24 +382,24 @@ async def plot_switch_traffic(switch_ip: str, interface: str, minutes: int = 10)
@router.get("/network_adapters", summary="获取网络适配器网段") @router.get("/network_adapters", summary="获取网络适配器网段")
async def get_network_adapters(): async def get_network_adapters():
try: try:
net_if_addrs = psutil.net_if_addrs() def sync_get_adapters():
net_if_addrs = psutil.net_if_addrs()
networks = [] networks = []
for interface, addrs in net_if_addrs.items(): for interface, addrs in net_if_addrs.items():
for addr in addrs: for addr in addrs:
if addr.family == socket.AF_INET: if addr.family == socket.AF_INET:
ip = addr.address ip = addr.address
netmask = addr.netmask netmask = addr.netmask
network = ipaddress.IPv4Network(f"{ip}/{netmask}", strict=False)
network = ipaddress.IPv4Network(f"{ip}/{netmask}", strict=False) networks.append({
networks.append({ "adapter": interface,
"adapter": interface, "network": str(network),
"network": str(network), "ip": ip,
"ip": ip, "subnet_mask": netmask
"subnet_mask": netmask })
}) return networks
networks = await asyncio.to_thread(sync_get_adapters)
return {"networks": networks} return {"networks": networks}
except Exception as e: except Exception as e:
return {"error": f"获取网络适配器信息失败: {str(e)}"} return {"error": f"获取网络适配器信息失败: {str(e)}"}

View File

@ -1,19 +1,14 @@
import asyncio import asyncio
import logging import logging
import telnetlib3 import telnetlib3
from datetime import datetime
from pathlib import Path 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 pydantic import BaseModel
from tenacity import retry, stop_after_attempt, wait_exponential
from src.backend.app.utils.logger import logger from src.backend.app.utils.logger import logger
from src.backend.config import settings from src.backend.config import settings
# ---------------------- # ----------------------
# 数据模型 # 数据模型
# ---------------------- # ----------------------
@ -25,36 +20,31 @@ class SwitchConfig(BaseModel):
ip_address: Optional[str] = None ip_address: Optional[str] = None
vlan: Optional[int] = None vlan: Optional[int] = None
# ---------------------- # ----------------------
# 异常类 # 异常类
# ---------------------- # ----------------------
class SwitchConfigException(Exception): class SwitchConfigException(Exception):
pass pass
class EnspConnectionException(SwitchConfigException): class EnspConnectionException(SwitchConfigException):
pass pass
class SSHConnectionException(SwitchConfigException):
pass
# ---------------------- # ----------------------
# 核心配置器(完整双模式) # 核心配置器
# ---------------------- # ----------------------
class SwitchConfigurator: class SwitchConfigurator:
connection_pool: Dict[str, tuple] = {}
def __init__( def __init__(
self, self,
username: str = None, username: str = None,
password: str = None, password: str = None,
timeout: int = None, timeout: int = None,
max_workers: int = 5, max_workers: int = 5,
ensp_mode: bool = False, ensp_mode: bool = False,
ensp_port: int = 2000, ensp_port: int = 2000,
ensp_command_delay: float = 0.5, ensp_command_delay: float = 0.5,
**ssh_options **ssh_options
): ):
self.username = username if username is not None else settings.SWITCH_USERNAME self.username = username if username is not None else settings.SWITCH_USERNAME
self.password = password if password is not None else settings.SWITCH_PASSWORD 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.ensp_delay = ensp_command_delay
self.ssh_options = ssh_options self.ssh_options = ssh_options
async def apply_config(self, ip: str, config: Union[Dict, SwitchConfig]) -> str: async def _get_or_create_connection(self, ip: 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:
""" """
通过 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: try:
logger.info(f"连接设备 {ip}端口23") reader, writer = await self._get_or_create_connection(ip)
reader, writer = await telnetlib3.open_connection(host=ip, port=23)
logger.debug("连接成功,开始登录流程")
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: 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") writer.write(f"{cmd}\n")
await writer.drain() await writer.drain()
await asyncio.sleep(self.ensp_delay)
command_output = "" logger.info(f"[{ip}] 所有命令发送完成")
try: return True
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
except asyncio.TimeoutError as e: except asyncio.TimeoutError as e:
logger.error(f"连接或读取超时: {e}") logger.error(f"[{ip}] 连接或读取超时: {e}")
raise EnspConnectionException(f"eNSP连接超时: {e}") return False
except Exception as e: except Exception as e:
logger.error(f"连接或执行异常: {e}", exc_info=True) logger.error(f"[{ip}] 命令发送异常: {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)}")
return False return False
async def execute_raw_commands(self, ip: str, commands: List[str]) -> bool:
@retry( """
stop=stop_after_attempt(2), 对外接口单台交换机执行命令
wait=wait_exponential(multiplier=1, min=4, max=10) """
) async with self.semaphore:
async def safe_apply( success = await self._send_ensp_commands(ip, commands)
self, return success
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())

View File

@ -1,5 +1,6 @@
from pydantic import BaseModel from pydantic import BaseModel
from typing import Optional from typing import Optional, List
class BaseResponse(BaseModel): class BaseResponse(BaseModel):
success: bool success: bool
@ -19,5 +20,21 @@ class ConfigHistory(BaseModel):
timestamp: float timestamp: float
status: str # success/failed status: str # success/failed
error: Optional[str] = None 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"] __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 from typing import Any, List, Dict
from openai import AsyncOpenAI
import httpx
from openai import OpenAI
import json import json
from src.backend.app.utils.exceptions import SiliconFlowAPIException from src.backend.app.utils.exceptions import SiliconFlowAPIException
from openai.types.chat import ChatCompletionSystemMessageParam, ChatCompletionUserMessageParam from openai.types.chat import ChatCompletionSystemMessageParam, ChatCompletionUserMessageParam
from src.backend.app.utils.logger import logger
class AIService: class AIService:
def __init__(self, api_key: str, api_url: str): def __init__(self, api_key: str, api_url: str):
self.api_key = api_key self.client = AsyncOpenAI(api_key=api_key, base_url=api_url)
self.api_url = api_url
self.client = OpenAI(
api_key=self.api_key,
base_url=self.api_url,
# timeout=httpx.Timeout(30.0)
)
async def parse_command(self, command: str) -> Any | None: async def parse_command(self, command: str, devices: List[Dict]) -> Dict[str, Any]:
""" """
调用硅基流动API解析中文命令 针对一组设备和一条自然语言命令生成每台设备的配置 JSON
"""
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到用户视图
""" """
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 = [ messages = [
ChatCompletionSystemMessageParam(role="system", content=prompt), ChatCompletionSystemMessageParam(role="system", content=prompt),
@ -42,32 +49,21 @@ class AIService:
] ]
try: try:
response = self.client.chat.completions.create( response = await self.client.chat.completions.create(
model="deepseek-ai/DeepSeek-V3", model="deepseek-ai/DeepSeek-V3",
messages=messages, messages=messages,
temperature=0.3, temperature=0.2,
max_tokens=1000, max_tokens=1500,
response_format={"type": "json_object"} response_format={"type": "json_object"}
) )
logger.debug(response)
config_str = response.choices[0].message.content.strip() config_str = response.choices[0].message.content.strip()
configs = json.loads(config_str)
try: return {"success": True, "results": configs}
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")
except Exception as e: except Exception as e:
raise SiliconFlowAPIException( raise SiliconFlowAPIException(
detail=f"API请求失败: {str(e)}", detail=f"AI 解析配置失败: {str(e)}",
status_code=getattr(e, "status_code", 500) 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 [] return []
with open(self.cache_path) as f: 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 src.backend.app.api.database import SessionLocal
from ..utils.logger import logger from ..utils.logger import logger
#V=ΔQ'-ΔQ/Δt (B/s)
class SwitchTrafficMonitor: class SwitchTrafficMonitor:
def __init__( def __init__(
self, 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 ..models.traffic_models import TrafficRecord
from src.backend.app.api.database import SessionLocal from src.backend.app.api.database import SessionLocal
from ..utils.logger import logger
class TrafficMonitor: class TrafficMonitor:
@ -33,7 +34,7 @@ class TrafficMonitor:
if not self.running: if not self.running:
self.running = True self.running = True
self.task = asyncio.create_task(self._monitor_loop()) self.task = asyncio.create_task(self._monitor_loop())
print("流量监控已启动") logger.info("流量监控已启动")
async def stop_monitoring(self): async def stop_monitoring(self):
"""停止流量监控""" """停止流量监控"""
@ -44,7 +45,7 @@ class TrafficMonitor:
await self.task await self.task
except asyncio.CancelledError: except asyncio.CancelledError:
pass pass
print("流量监控已停止") logger.info("流量监控已停止")
async def _monitor_loop(self): async def _monitor_loop(self):
"""监控主循环""" """监控主循环"""

View File

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

View File

@ -12,29 +12,29 @@ import {
Field, Field,
Input, Input,
Stack, Stack,
Portal,
Select,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { FiCheck } from 'react-icons/fi'; import { FiCheck } from 'react-icons/fi';
import Notification from '@/libs/system/Notification'; import { createListCollection } from '@chakra-ui/react';
const MotionBox = motion(Box); const MotionBox = motion(Box);
/** const vendors = ['huawei', 'cisco', 'h3c', 'ruijie', 'zte'];
* 设备配置弹窗
* @param isOpen 是否打开
* @param onClose 关闭弹窗
* @param onSave 保存修改
* @param device 当前设备
* @returns {JSX.Element}
* @constructor
*/
const DeviceConfigModal = ({ isOpen, onClose, onSave, device }) => { const DeviceConfigModal = ({ isOpen, onClose, onSave, device }) => {
const [username, setUsername] = useState(device.username || ''); const [username, setUsername] = useState(device.username || '');
const [password, setPassword] = useState(device.password || ''); const [password, setPassword] = useState(device.password || '');
const [vendor, setVendor] = useState(device.vendor || '');
const [saved, setSaved] = useState(false); const [saved, setSaved] = useState(false);
const vendorCollection = createListCollection({
items: vendors.map((v) => ({ label: v.toUpperCase(), value: v })),
});
const handleSave = () => { const handleSave = () => {
const updatedDevice = { ...device, username, password }; const updatedDevice = { ...device, username, password, vendor };
onSave(updatedDevice); onSave(updatedDevice);
setSaved(true); setSaved(true);
setTimeout(() => { setTimeout(() => {
@ -82,6 +82,40 @@ const DeviceConfigModal = ({ isOpen, onClose, onSave, device }) => {
type={'password'} type={'password'}
/> />
</Field.Root> </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> </Stack>
</DialogBody> </DialogBody>

View File

@ -22,22 +22,16 @@ import ConfigTool from '@/libs/config/ConfigTool';
import { api } from '@/services/api/api'; import { api } from '@/services/api/api';
import Notification from '@/libs/system/Notification'; import Notification from '@/libs/system/Notification';
import Common from '@/libs/common'; import Common from '@/libs/common';
import configEffect from '@/libs/script/configPage/configEffect';
const testMode = ConfigTool.load().testMode; const testMode = ConfigTool.load().testMode;
const ConfigPage = () => { const ConfigPage = () => {
const [devices, setDevices] = useState([]); const [devices, setDevices] = useState([]);
const [selectedDevice, setSelectedDevice] = useState(''); const [selectedDevices, setSelectedDevices] = useState([]);
const [selectedDeviceConfig, setSelectedDeviceConfig] = useState(''); const [deviceConfigs, setDeviceConfigs] = useState({});
const [inputText, setInputText] = useState(''); const [inputText, setInputText] = useState('');
const [parsedConfig, setParsedConfig] = useState('');
const [editableConfig, setEditableConfig] = useState('');
const [applying, setApplying] = useState(false); const [applying, setApplying] = useState(false);
const [hasParsed, setHasParsed] = useState(false); const [hasParsed, setHasParsed] = useState(false);
const [isPeizhi, setisPeizhi] = useState(false);
const [isApplying, setIsApplying] = useState(false);
const [applyStatus, setApplyStatus] = useState([]);
const deviceCollection = createListCollection({ const deviceCollection = createListCollection({
items: devices.map((device) => ({ items: devices.map((device) => ({
@ -52,18 +46,30 @@ const ConfigPage = () => {
}, []); }, []);
const handleParse = async () => { const handleParse = async () => {
if (!selectedDevice || !inputText.trim()) { if (selectedDevices.length === 0 || !inputText.trim()) {
Notification.error({ Notification.error({
title: '操作失败', 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; return;
} }
try { try {
const performParse = async () => { const performParse = async () =>
return await api.parseCommand(inputText); await api.parseCommand({
}; command: inputText,
devices: selectedConfigs,
});
const resultWrapper = await Notification.promise({ const resultWrapper = await Notification.promise({
promise: performParse(), promise: performParse(),
@ -82,11 +88,15 @@ const ConfigPage = () => {
}); });
let result = await resultWrapper.unwrap(); let result = await resultWrapper.unwrap();
if (result?.data) { if (result?.data?.config) {
setParsedConfig(JSON.stringify(result.data)); const configMap = {};
setEditableConfig(JSON.stringify(result.data)); result.data.config.forEach((item) => {
if (item.device?.ip) {
configMap[item.device.ip] = item;
}
});
setDeviceConfigs(configMap);
setHasParsed(true); setHasParsed(true);
setisPeizhi(true);
} }
} catch (error) { } catch (error) {
console.error('配置解析异常:', error); console.error('配置解析异常:', error);
@ -98,62 +108,80 @@ const ConfigPage = () => {
}; };
const handleApply = async () => { const handleApply = async () => {
if (!editableConfig) { if (!hasParsed) {
Notification.warn({ Notification.warn({
title: '配置为空', title: '未解析配置',
description: '请先解析或编辑有效配置', description: '请先解析配置再应用',
}); });
return; return;
} }
setApplying(true); setApplying(true);
setIsApplying(true);
try { try {
const applyOperation = async () => { const applyOperation = async () => {
if (testMode) { if (testMode) {
Common.sleep(1000).then(() => ({ success: true })); await Common.sleep(1000);
} else { Notification.success({
let commands = JSON.parse(editableConfig)?.config?.commands; title: '测试模式成功',
console.log(`commands:${JSON.stringify(commands)}`); description: '配置已模拟应用',
const deviceConfig = JSON.parse(selectedDeviceConfig); });
console.log(`deviceConfig:${JSON.stringify(deviceConfig)}`); return;
if (!deviceConfig.password) { }
const applyPromises = selectedDevices.map(async (ip) => {
const deviceItem = deviceConfigs[ip];
if (!deviceItem) return;
const deviceConfig = deviceItem.config;
if (!deviceItem.device.password) {
Notification.warn({ Notification.warn({
title: '所选交换机暂未配置用户名(可选)和密码', title: `交换机 ${deviceItem.device.name} 暂未配置密码`,
description: '请前往交换机设备处配置username和password', description: '请前往交换机设备处配置用户名和密码',
}); });
return false; console.log(JSON.stringify(deviceItem));
return;
} }
if (deviceConfig.username || deviceConfig.username.toString() !== '') {
commands.push(`!username=${deviceConfig.username.toString()}`); if (!deviceItem.device.username) {
} else { Notification.warn({
commands.push(`!username=NONE`); title: `交换机 ${deviceItem.device.name} 暂未配置用户名,将使用NONE作为用户名`,
});
deviceItem.device.username = 'NONE';
} }
commands.push(`!password=${deviceConfig.password.toString()}`);
const res = await api.applyConfig(selectedDevice, commands); const commands = [...deviceConfig.commands];
if (res) {
try {
const res = await api.applyConfig(
ip,
commands,
deviceItem.device.username,
deviceItem.device.password
);
Notification.success({ Notification.success({
title: '配置完毕', title: `配置完毕 - ${deviceItem.device.name}`,
description: JSON.stringify(res), description: JSON.stringify(res),
}); });
} else { } catch (err) {
Notification.error({ Notification.error({
title: '配置过程出现错误', title: `配置过程出现错误 - ${deviceItem.device.name}`,
description: '请检查API提示', description: err.message || '请检查API提示',
}); });
} }
} });
await Promise.all(applyPromises);
}; };
await Notification.promise({ await Notification.promise({
promise: applyOperation, promise: applyOperation(),
loading: { loading: {
title: '配置应用中', title: '配置应用中',
description: '正在推送配置到设备...', description: '正在推送配置到设备...',
}, },
success: { success: {
title: '应用', title: '应用成',
description: '配置已成功生效', description: '所有设备配置已推送',
}, },
error: { error: {
title: '应用失败', title: '应用失败',
@ -174,19 +202,16 @@ const ConfigPage = () => {
<Heading fontSize={'xl'} color={'teal.300'}> <Heading fontSize={'xl'} color={'teal.300'}>
交换机配置中心 交换机配置中心
</Heading> </Heading>
<Field.Root> <Field.Root>
<Field.Label fontWeight={'bold'} mb={1} fontSize="sm"> <Field.Label fontWeight={'bold'} mb={1} fontSize="sm">
选择交换机设备 选择交换机设备
</Field.Label> </Field.Label>
<Select.Root <Select.Root
multiple
collection={deviceCollection} collection={deviceCollection}
value={selectedDevice ? [selectedDevice] : []} value={selectedDevices}
onValueChange={({ value }) => { onValueChange={({ value }) => setSelectedDevices(value)}
const selectedIp = value[0] ?? '';
setSelectedDevice(selectedIp);
const fullDeviceConfig = devices.find((device) => device.ip === selectedIp);
setSelectedDeviceConfig(JSON.stringify(fullDeviceConfig));
}}
placeholder={'请选择交换机设备'} placeholder={'请选择交换机设备'}
size={'sm'} size={'sm'}
colorPalette={'teal'} colorPalette={'teal'}
@ -202,7 +227,7 @@ const ConfigPage = () => {
</Select.IndicatorGroup> </Select.IndicatorGroup>
</Select.Control> </Select.Control>
<Portal> <Portal>
<Select.Positioner> <Select.Positioner style={{ zIndex: 1500 }}>
<Select.Content> <Select.Content>
{deviceCollection.items.map((item) => ( {deviceCollection.items.map((item) => (
<Select.Item key={item.value} item={item}> <Select.Item key={item.value} item={item}>
@ -214,13 +239,14 @@ const ConfigPage = () => {
</Portal> </Portal>
</Select.Root> </Select.Root>
</Field.Root> </Field.Root>
<Field.Root> <Field.Root>
<Field.Label fontWeight={'bold'} mb={1} fontSize="sm"> <Field.Label fontWeight={'bold'} mb={1} fontSize="sm">
配置指令输入 配置指令输入
</Field.Label> </Field.Label>
<Textarea <Textarea
rows={4} 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} value={inputText}
colorPalette={'teal'} colorPalette={'teal'}
orientation={'vertical'} orientation={'vertical'}
@ -229,30 +255,27 @@ const ConfigPage = () => {
size={'sm'} size={'sm'}
/> />
</Field.Root> </Field.Root>
<Button <Button
colorScheme={'teal'} colorScheme={'teal'}
variant={'solid'} variant={'solid'}
size={'sm'} size={'sm'}
onClick={handleParse} onClick={handleParse}
isDisabled={!selectedDevice || !inputText.trim()} isDisabled={selectedDevices.length === 0 || !inputText.trim()}
> >
解析配置 解析配置
</Button> </Button>
{isPeizhi && parsedConfig && (
{hasParsed && selectedDevices.length > 0 && (
<FadeInWrapper delay={0.2}> <FadeInWrapper delay={0.2}>
<VStack spacing={4} align={'stretch'}> <VStack spacing={4} align={'stretch'}>
{(() => { {selectedDevices.map((ip) => {
let parsed; const item = deviceConfigs[ip];
try { if (!item) return null;
parsed = JSON.parse(editableConfig); const cfg = item.config;
} catch (e) { return (
return <Text color={'red.300'}>配置 JSON 格式错误无法解析</Text>;
}
const config = parsed.config ? [parsed.config] : parsed;
return config.map((cfg, idx) => (
<Box <Box
key={idx} key={ip}
p={4} p={4}
bg={'whiteAlpha.100'} bg={'whiteAlpha.100'}
borderRadius={'xl'} borderRadius={'xl'}
@ -260,7 +283,7 @@ const ConfigPage = () => {
borderColor={'whiteAlpha.300'} borderColor={'whiteAlpha.300'}
> >
<Text fontSize={'lg'} fontWeight={'bold'} mb={2}> <Text fontSize={'lg'} fontWeight={'bold'} mb={2}>
配置类型: {cfg.type} 设备: {item.device.name} ({ip}) - 配置类型: {cfg.type}
</Text> </Text>
{Object.entries(cfg).map(([key, value]) => { {Object.entries(cfg).map(([key, value]) => {
@ -278,9 +301,16 @@ const ConfigPage = () => {
value={value} value={value}
onChange={(e) => { onChange={(e) => {
const newVal = e.target.value; const newVal = e.target.value;
const updated = JSON.parse(editableConfig); setDeviceConfigs((prev) => ({
updated.config[key] = newVal; ...prev,
setEditableConfig(JSON.stringify(updated, null, 2)); [ip]: {
...prev[ip],
config: {
...prev[ip].config,
[key]: newVal,
},
},
}));
}} }}
/> />
</Field.Root> </Field.Root>
@ -299,20 +329,26 @@ const ConfigPage = () => {
value={cmd} value={cmd}
onChange={(e) => { onChange={(e) => {
const newCmd = e.target.value; const newCmd = e.target.value;
const updated = JSON.parse(editableConfig); setDeviceConfigs((prev) => {
updated.config.commands[i] = newCmd; const updated = { ...prev };
setEditableConfig(JSON.stringify(updated, null, 2)); updated[ip].config.commands[i] = newCmd;
return updated;
});
}} }}
/> />
</Field.Root> </Field.Root>
))} ))}
<HStack mt={4} spacing={3} justify={'flex-end'}> <HStack mt={4} spacing={3} justify={'flex-end'}>
<Button <Button
variant={'outline'} variant={'outline'}
colorScheme={'gray'} colorScheme={'gray'}
size={'sm'} size={'sm'}
onClick={() => { onClick={() => {
setEditableConfig(parsedConfig); setDeviceConfigs((prev) => ({
...prev,
[ip]: item,
}));
Notification.success({ Notification.success({
title: '成功重置配置!', title: '成功重置配置!',
description: '现在您可以重新审查生成的配置', description: '现在您可以重新审查生成的配置',
@ -341,63 +377,14 @@ const ConfigPage = () => {
size={'sm'} size={'sm'}
onClick={handleApply} onClick={handleApply}
isLoading={applying} isLoading={applying}
isDisabled={!editableConfig} isDisabled={!cfg.commands || cfg.commands.length === 0}
> >
应用到交换机 应用到交换机
</Button> </Button>
</HStack> </HStack>
</Box> </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> </VStack>
</FadeInWrapper> </FadeInWrapper>
)} )}

View File

@ -25,7 +25,7 @@ export const api = {
/** /**
* 扫描网络 * 扫描网络
* @param subnet 子网地址 * @param {string} subnet 子网地址
* @returns {Promise<axios.AxiosResponse<any>>} * @returns {Promise<axios.AxiosResponse<any>>}
*/ */
scan: (subnet) => axios.get(buildUrl('/api/scan_network'), { params: { subnet } }), 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>>} * @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 {string} switch_ip 交换机IP
* @param commands 配置,为数组[] * @param {Array<string>} commands 配置命令数组
* @param username 用户名无时使用NONE
* @param password 密码
* @returns {Promise<axios.AxiosResponse<any>>} * @returns {Promise<axios.AxiosResponse<any>>}
*/ */
applyConfig: (switch_ip, commands) => applyConfig: (switch_ip, commands, username, password) =>
axios.post(buildUrl('/api/execute_cli_commands'), { switch_ip: switch_ip, commands: commands }), axios.post(buildUrl('/api/execute_cli_commands'), { switch_ip, commands, username, password }),
/** /**
* 获取网络适配器信息 * 获取网络适配器信息
@ -60,7 +66,7 @@ export const api = {
/** /**
* 更新基础URL * 更新基础URL
* @param url * @param {string} url
*/ */
updateBaseUrl: (url) => { updateBaseUrl: (url) => {
const config = ConfigTool.load(); const config = ConfigTool.load();
@ -77,7 +83,7 @@ export const getConfig = () => ConfigTool.load();
/** /**
* 获取基础URL * 获取基础URL
* @returns {string|string} * @returns {string}
*/ */
export const getBaseUrl = () => ConfigTool.load().backendUrl || ''; export const getBaseUrl = () => ConfigTool.load().backendUrl || '';

View File

@ -21,6 +21,7 @@
"@nestjs/platform-express": "^11.0.1", "@nestjs/platform-express": "^11.0.1",
"@nestjs/swagger": "^11.2.0", "@nestjs/swagger": "^11.2.0",
"axios": "^1.10.0", "axios": "^1.10.0",
"ip": "^2.0.1",
"reflect-metadata": "^0.2.2", "reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1", "rxjs": "^7.8.1",
"ssh2": "^1.16.0" "ssh2": "^1.16.0"
@ -34,6 +35,7 @@
"@swc/cli": "^0.6.0", "@swc/cli": "^0.6.0",
"@swc/core": "^1.10.7", "@swc/core": "^1.10.7",
"@types/express": "^5.0.0", "@types/express": "^5.0.0",
"@types/ip": "^1.1.3",
"@types/jest": "^29.5.14", "@types/jest": "^29.5.14",
"@types/node": "^22.16.4", "@types/node": "^22.16.4",
"@types/supertest": "^6.0.2", "@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 { RootModule } from './root/root.module';
import { HttpMiddleware } from './common/middleware/http.middleware';
import { NetworkModule } from './modules/network/network.module';
@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) => ({ map((data) => ({
success: true, success: true,
data, 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> { export interface ApiResponse<T = any> {
success: boolean; success: boolean;
data: T; data: T;
message: string;
} }

View File

@ -3,13 +3,11 @@ import { AppModule } from './app.module';
import { Logger } from '@nestjs/common'; import { Logger } from '@nestjs/common';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { ResponseInterceptor } from './common/interceptors/response.interceptor'; import { ResponseInterceptor } from './common/interceptors/response.interceptor';
import { AllExceptionsFilter } from './common/filters/all-exception.filter';
async function bootstrap() { async function bootstrap() {
const app = await NestFactory.create(AppModule); const app = await NestFactory.create(AppModule);
app.setGlobalPrefix('api'); app.setGlobalPrefix('api');
app.useGlobalInterceptors(new ResponseInterceptor()); app.useGlobalInterceptors(new ResponseInterceptor());
app.useGlobalFilters(new AllExceptionsFilter());
const config = new DocumentBuilder() const config = new DocumentBuilder()
.setTitle('交换机API平台') .setTitle('交换机API平台')
.setDescription('自动化交换机配置和流量监控接口') .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() @Controller()
export class RootController { export class RootController {
@Get() @Get()
getWelcome() { async getWelcome() {
return { return {
message: '欢迎使用交换机管理平台 API', 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 { RootController } from './root.controller';
import { HttpMiddleware } from '../common/middleware/http.middleware';
@Module({ @Module({
controllers: [RootController], controllers: [RootController],