mirror of
https://github.com/Jerryplusy/AI-powered-switches.git
synced 2025-10-14 01:39:18 +00:00
Merge remote-tracking branch 'origin/main'
This commit is contained in:
commit
8f5ac21be1
23
README.md
23
README.md
@ -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月
|
||||
|
||||
|
@ -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 服务生成配置
|
||||
|
@ -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)
|
@ -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)}"}
|
@ -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
|
||||
|
@ -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"]
|
||||
|
26
src/backend/app/models/requests.py
Normal file
26
src/backend/app/models/requests.py
Normal 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"
|
@ -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)
|
||||
)
|
||||
|
@ -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
|
@ -48,4 +48,4 @@ class NetworkScanner:
|
||||
return []
|
||||
|
||||
with open(self.cache_path) as f:
|
||||
return json.load(f)
|
||||
return json.load(f)
|
@ -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,
|
||||
|
9
src/backend/app/services/test.py
Normal file
9
src/backend/app/services/test.py
Normal 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)
|
@ -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):
|
||||
"""监控主循环"""
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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>
|
||||
)}
|
||||
|
@ -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 || '';
|
||||
|
||||
|
@ -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",
|
||||
|
8448
src/nest-backend/pnpm-lock.yaml
generated
8448
src/nest-backend/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -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');
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
@ -19,7 +19,6 @@ export class ResponseInterceptor<T>
|
||||
map((data) => ({
|
||||
success: true,
|
||||
data,
|
||||
message: '操作成功',
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
25
src/nest-backend/src/common/middleware/http.middleware.ts
Normal file
25
src/nest-backend/src/common/middleware/http.middleware.ts
Normal 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();
|
||||
}
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
export interface ApiResponse<T = any> {
|
||||
success: boolean;
|
||||
data: T;
|
||||
message: string;
|
||||
}
|
||||
|
@ -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('自动化交换机配置和流量监控接口')
|
||||
|
18
src/nest-backend/src/modules/network/network.controller.ts
Normal file
18
src/nest-backend/src/modules/network/network.controller.ts
Normal 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();
|
||||
}
|
||||
}
|
10
src/nest-backend/src/modules/network/network.module.ts
Normal file
10
src/nest-backend/src/modules/network/network.module.ts
Normal 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 {}
|
44
src/nest-backend/src/modules/network/network.service.ts
Normal file
44
src/nest-backend/src/modules/network/network.service.ts
Normal 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}` };
|
||||
}
|
||||
}
|
||||
}
|
@ -3,7 +3,7 @@ import { Controller, Get } from '@nestjs/common';
|
||||
@Controller()
|
||||
export class RootController {
|
||||
@Get()
|
||||
getWelcome() {
|
||||
async getWelcome() {
|
||||
return {
|
||||
message: '欢迎使用交换机管理平台 API',
|
||||
};
|
||||
|
@ -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],
|
||||
|
Loading…
x
Reference in New Issue
Block a user