mirror of
https://github.com/Jerryplusy/AI-powered-switches.git
synced 2025-07-04 21:29:18 +00:00
Merge remote-tracking branch 'origin/main'
This commit is contained in:
commit
ccd37ce4e6
191
src/backend/.gitignore
vendored
191
src/backend/.gitignore
vendored
@ -1,52 +1,7 @@
|
|||||||
# Python
|
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
*$py.class
|
*$py.class
|
||||||
.Python
|
|
||||||
env/
|
|
||||||
venv/
|
|
||||||
ENV/
|
|
||||||
env.bak/
|
|
||||||
venv.bak/
|
|
||||||
|
|
||||||
# IDE
|
|
||||||
.idea/
|
|
||||||
.vscode/
|
|
||||||
*.swp
|
|
||||||
*.swo
|
|
||||||
|
|
||||||
# Logs
|
|
||||||
*.log
|
|
||||||
logs/
|
|
||||||
|
|
||||||
# Environment variables
|
|
||||||
.env
|
|
||||||
.env.local
|
|
||||||
.env.development
|
|
||||||
.env.test
|
|
||||||
.env.production
|
|
||||||
|
|
||||||
# Docker
|
|
||||||
docker-compose.override.yml
|
|
||||||
|
|
||||||
# Test
|
|
||||||
.coverage
|
|
||||||
htmlcov/
|
|
||||||
.pytest_cache/
|
|
||||||
|
|
||||||
# Build
|
|
||||||
dist/
|
|
||||||
build/
|
|
||||||
*.egg-info/
|
|
||||||
# Byte-compiled / optimized / DLL files
|
|
||||||
__pycache__/
|
|
||||||
*.py[cod]
|
|
||||||
*$py.class
|
|
||||||
|
|
||||||
# C extensions
|
|
||||||
*.so
|
*.so
|
||||||
|
|
||||||
# Distribution / packaging
|
|
||||||
.Python
|
.Python
|
||||||
build/
|
build/
|
||||||
develop-eggs/
|
develop-eggs/
|
||||||
@ -60,155 +15,23 @@ parts/
|
|||||||
sdist/
|
sdist/
|
||||||
var/
|
var/
|
||||||
wheels/
|
wheels/
|
||||||
share/python-wheels/
|
|
||||||
*.egg-info/
|
*.egg-info/
|
||||||
.installed.cfg
|
.installed.cfg
|
||||||
*.egg
|
*.egg
|
||||||
MANIFEST
|
MANIFEST
|
||||||
|
|
||||||
# PyInstaller
|
# IDE
|
||||||
# Usually these files are written by a python script from a template
|
.idea/
|
||||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
.vscode/
|
||||||
*.manifest
|
|
||||||
*.spec
|
|
||||||
|
|
||||||
# Installer logs
|
# Environment
|
||||||
pip-log.txt
|
|
||||||
pip-delete-this-directory.txt
|
|
||||||
|
|
||||||
# Unit test / coverage reports
|
|
||||||
htmlcov/
|
|
||||||
.tox/
|
|
||||||
.nox/
|
|
||||||
.coverage
|
|
||||||
.coverage.*
|
|
||||||
.cache
|
|
||||||
nosetests.xml
|
|
||||||
coverage.xml
|
|
||||||
*.cover
|
|
||||||
*.py,cover
|
|
||||||
.hypothesis/
|
|
||||||
.pytest_cache/
|
|
||||||
cover/
|
|
||||||
|
|
||||||
# Translations
|
|
||||||
*.mo
|
|
||||||
*.pot
|
|
||||||
|
|
||||||
# Django stuff:
|
|
||||||
*.log
|
|
||||||
local_settings.py
|
|
||||||
db.sqlite3
|
|
||||||
db.sqlite3-journal
|
|
||||||
|
|
||||||
# Flask stuff:
|
|
||||||
instance/
|
|
||||||
.webassets-cache
|
|
||||||
|
|
||||||
# Scrapy stuff:
|
|
||||||
.scrapy
|
|
||||||
|
|
||||||
# Sphinx documentation
|
|
||||||
docs/_build/
|
|
||||||
|
|
||||||
# PyBuilder
|
|
||||||
.pybuilder/
|
|
||||||
target/
|
|
||||||
|
|
||||||
# Jupyter Notebook
|
|
||||||
.ipynb_checkpoints
|
|
||||||
|
|
||||||
# IPython
|
|
||||||
profile_default/
|
|
||||||
ipython_config.py
|
|
||||||
|
|
||||||
# pyenv
|
|
||||||
# For a library or package, you might want to ignore these files since the code is
|
|
||||||
# intended to run in multiple environments; otherwise, check them in:
|
|
||||||
# .python-version
|
|
||||||
|
|
||||||
# pipenv
|
|
||||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
||||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
||||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
||||||
# install all needed dependencies.
|
|
||||||
#Pipfile.lock
|
|
||||||
|
|
||||||
# UV
|
|
||||||
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
|
||||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
||||||
# commonly ignored for libraries.
|
|
||||||
#uv.lock
|
|
||||||
|
|
||||||
# poetry
|
|
||||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
||||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
||||||
# commonly ignored for libraries.
|
|
||||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
||||||
#poetry.lock
|
|
||||||
|
|
||||||
# pdm
|
|
||||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
||||||
#pdm.lock
|
|
||||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
|
||||||
# in version control.
|
|
||||||
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
|
||||||
.pdm.toml
|
|
||||||
.pdm-python
|
|
||||||
.pdm-build/
|
|
||||||
|
|
||||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
||||||
__pypackages__/
|
|
||||||
|
|
||||||
# Celery stuff
|
|
||||||
celerybeat-schedule
|
|
||||||
celerybeat.pid
|
|
||||||
|
|
||||||
# SageMath parsed files
|
|
||||||
*.sage.py
|
|
||||||
|
|
||||||
# Environments
|
|
||||||
.env
|
.env
|
||||||
.venv
|
.venv/
|
||||||
env/
|
env/
|
||||||
venv/
|
venv/
|
||||||
ENV/
|
ENV/
|
||||||
env.bak/
|
env.bak/
|
||||||
venv.bak/
|
venv.bak/
|
||||||
|
|
||||||
# Spyder project settings
|
# Logs
|
||||||
.spyderproject
|
*.log
|
||||||
.spyproject
|
|
||||||
|
|
||||||
# Rope project settings
|
|
||||||
.ropeproject
|
|
||||||
|
|
||||||
# mkdocs documentation
|
|
||||||
/site
|
|
||||||
|
|
||||||
# mypy
|
|
||||||
.mypy_cache/
|
|
||||||
.dmypy.json
|
|
||||||
dmypy.json
|
|
||||||
|
|
||||||
# Pyre type checker
|
|
||||||
.pyre/
|
|
||||||
|
|
||||||
# pytype static type analyzer
|
|
||||||
.pytype/
|
|
||||||
|
|
||||||
# Cython debug symbols
|
|
||||||
cython_debug/
|
|
||||||
|
|
||||||
# PyCharm
|
|
||||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
||||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
||||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
||||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
||||||
#.idea/
|
|
||||||
|
|
||||||
# Ruff stuff:
|
|
||||||
.ruff_cache/
|
|
||||||
|
|
||||||
# PyPI configuration file
|
|
||||||
.pypirc
|
|
@ -1,22 +1,15 @@
|
|||||||
version: '3.13.2'
|
FROM python:3.13-slim
|
||||||
|
|
||||||
services:
|
WORKDIR /app
|
||||||
app:
|
|
||||||
build: .
|
|
||||||
ports:
|
|
||||||
- "8000:8000"
|
|
||||||
depends_on:
|
|
||||||
- redis
|
|
||||||
environment:
|
|
||||||
- REDIS_URL=redis://redis:6379
|
|
||||||
|
|
||||||
redis:
|
COPY ./requirements.txt /app/requirements.txt
|
||||||
image: redis:alpine
|
|
||||||
ports:
|
|
||||||
- "6379:6379"
|
|
||||||
|
|
||||||
worker:
|
RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt
|
||||||
build: .
|
|
||||||
command: celery -A app.services.task_service worker --loglevel=info
|
COPY . /app
|
||||||
depends_on:
|
|
||||||
- ONBUILD
|
ENV PYTHONPATH=/app
|
||||||
|
ENV PORT=8000
|
||||||
|
ENV HOST=0.0.0.0
|
||||||
|
|
||||||
|
CMD ["uvicorn", "run:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]
|
1
src/backend/__init__.py
Normal file
1
src/backend/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
#empty
|
23
src/backend/app/__init__.py
Normal file
23
src/backend/app/__init__.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
from fastapi import FastAPI
|
||||||
|
from src.backend.app.api.endpoints import router as api_router
|
||||||
|
from src.backend.app.utils.logger import setup_logging
|
||||||
|
from src.backend.config import settings
|
||||||
|
|
||||||
|
|
||||||
|
def create_app() -> FastAPI:
|
||||||
|
# 设置日志
|
||||||
|
setup_logging()
|
||||||
|
|
||||||
|
# 创建FastAPI应用
|
||||||
|
app = FastAPI(
|
||||||
|
title=settings.APP_NAME,
|
||||||
|
debug=settings.DEBUG,
|
||||||
|
docs_url=f"{settings.API_PREFIX}/docs",
|
||||||
|
redoc_url=f"{settings.API_PREFIX}/redoc",
|
||||||
|
openapi_url=f"{settings.API_PREFIX}/openapi.json"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 添加API路由
|
||||||
|
app.include_router(api_router, prefix=settings.API_PREFIX)
|
||||||
|
|
||||||
|
return app
|
@ -1,29 +0,0 @@
|
|||||||
from .base import BaseAdapter
|
|
||||||
from .cisco import CiscoAdapter
|
|
||||||
from .huawei import HuaweiAdapter
|
|
||||||
from .factory import AdapterFactory
|
|
||||||
|
|
||||||
# 自动注册所有适配器类
|
|
||||||
__all_adapters__ = {
|
|
||||||
'cisco': CiscoAdapter,
|
|
||||||
'huawei': HuaweiAdapter
|
|
||||||
}
|
|
||||||
|
|
||||||
def get_supported_vendors() -> list:
|
|
||||||
"""获取当前支持的设备厂商列表"""
|
|
||||||
return list(__all_adapters__.keys())
|
|
||||||
|
|
||||||
def init_adapters():
|
|
||||||
"""初始化适配器工厂"""
|
|
||||||
AdapterFactory.register_adapters(__all_adapters__)
|
|
||||||
|
|
||||||
# 应用启动时自动初始化
|
|
||||||
init_adapters()
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
'BaseAdapter',
|
|
||||||
'CiscoAdapter',
|
|
||||||
'HuaweiAdapter',
|
|
||||||
'AdapterFactory',
|
|
||||||
'get_supported_vendors'
|
|
||||||
]
|
|
@ -1,16 +0,0 @@
|
|||||||
# /backend/app/adapters/base.py
|
|
||||||
from abc import ABC, abstractmethod
|
|
||||||
from typing import Dict, Any
|
|
||||||
|
|
||||||
class BaseAdapter(ABC):
|
|
||||||
@abstractmethod
|
|
||||||
async def connect(self, ip: str, credentials: Dict[str, str]):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
async def deploy_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
async def get_status(self) -> Dict[str, Any]:
|
|
||||||
pass
|
|
@ -1,32 +0,0 @@
|
|||||||
# /backend/app/adapters/cisco.py
|
|
||||||
from netmiko import ConnectHandler
|
|
||||||
from .base import BaseAdapter
|
|
||||||
|
|
||||||
class CiscoAdapter(BaseAdapter):
|
|
||||||
def __init__(self):
|
|
||||||
self.connection = None
|
|
||||||
|
|
||||||
async def connect(self, ip: str, credentials: Dict[str, str]):
|
|
||||||
self.connection = ConnectHandler(
|
|
||||||
device_type='cisco_ios',
|
|
||||||
host=ip,
|
|
||||||
username=credentials['username'],
|
|
||||||
password=credentials['password'],
|
|
||||||
timeout=10
|
|
||||||
)
|
|
||||||
|
|
||||||
async def deploy_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
|
|
||||||
commands = self._generate_commands(config)
|
|
||||||
output = self.connection.send_config_set(commands)
|
|
||||||
return {'success': True, 'output': output}
|
|
||||||
|
|
||||||
def _generate_commands(self, config: Dict[str, Any]) -> list:
|
|
||||||
# 实际生产中应使用Jinja2模板
|
|
||||||
commands = []
|
|
||||||
if 'vlans' in config:
|
|
||||||
for vlan in config['vlans']:
|
|
||||||
commands.extend([
|
|
||||||
f"vlan {vlan['id']}",
|
|
||||||
f"name {vlan['name']}"
|
|
||||||
])
|
|
||||||
return commands
|
|
@ -1,21 +0,0 @@
|
|||||||
from . import BaseAdapter
|
|
||||||
from .cisco import CiscoAdapter
|
|
||||||
from .huawei import HuaweiAdapter
|
|
||||||
|
|
||||||
class AdapterFactory:
|
|
||||||
_adapters = {}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def register_adapters(cls, adapters: dict):
|
|
||||||
"""注册适配器字典"""
|
|
||||||
cls._adapters.update(adapters)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_adapter(vendor: str)->BaseAdapter:
|
|
||||||
adapters = {
|
|
||||||
'cisco': CiscoAdapter,
|
|
||||||
'huawei': HuaweiAdapter
|
|
||||||
}
|
|
||||||
if vendor not in cls._adapters:
|
|
||||||
raise ValueError(f"Unsupported vendor: {vendor}")
|
|
||||||
return cls._adapters[vendor]()
|
|
@ -1,26 +0,0 @@
|
|||||||
import httpx
|
|
||||||
from .base import BaseAdapter
|
|
||||||
|
|
||||||
class HuaweiAdapter(BaseAdapter):
|
|
||||||
def __init__(self):
|
|
||||||
self.client = None
|
|
||||||
self.base_url = None
|
|
||||||
|
|
||||||
async def connect(self, ip: str, credentials: dict):
|
|
||||||
self.base_url = f"https://{ip}/restconf"
|
|
||||||
self.client = httpx.AsyncClient(
|
|
||||||
auth=(credentials['username'], credentials['password']),
|
|
||||||
verify=False,
|
|
||||||
timeout=30.0
|
|
||||||
)
|
|
||||||
|
|
||||||
async def deploy_config(self, config: dict):
|
|
||||||
headers = {"Content-Type": "application/yang-data+json"}
|
|
||||||
url = f"{self.base_url}/data/ietf-restconf:operations/network-topology:deploy"
|
|
||||||
response = await self.client.post(url, json=config, headers=headers)
|
|
||||||
response.raise_for_status()
|
|
||||||
return response.json()
|
|
||||||
|
|
||||||
async def disconnect(self):
|
|
||||||
if self.client:
|
|
||||||
await self.client.aclose()
|
|
@ -1,7 +1,4 @@
|
|||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
from src.backend.app.api.command_parser import router as command_router
|
from .endpoints import router
|
||||||
from src.backend.app.api.network_config import router as config_router
|
|
||||||
|
|
||||||
router = APIRouter()
|
__all__ = ["router"]
|
||||||
router.include_router(command_router, prefix="/parse_command", tags=["Command Parsing"])
|
|
||||||
router.include_router(config_router, prefix="/apply_config", tags=["Configuration"])
|
|
@ -1,40 +0,0 @@
|
|||||||
from fastapi import APIRouter, HTTPException, BackgroundTasks
|
|
||||||
from pydantic import BaseModel
|
|
||||||
from typing import List
|
|
||||||
from app.services.batch import BatchService
|
|
||||||
from app.utils.decorators import async_retry
|
|
||||||
|
|
||||||
router = APIRouter()
|
|
||||||
|
|
||||||
class BulkDeviceConfig(BaseModel):
|
|
||||||
device_ips: List[str]
|
|
||||||
config: dict
|
|
||||||
credentials: dict
|
|
||||||
vendor: str = "cisco"
|
|
||||||
timeout: int = 30
|
|
||||||
|
|
||||||
@router.post("/config")
|
|
||||||
@async_retry(max_attempts=3, delay=1)
|
|
||||||
async def bulk_apply_config(request: BulkDeviceConfig, bg_tasks: BackgroundTasks):
|
|
||||||
"""
|
|
||||||
批量配置设备接口
|
|
||||||
示例请求体:
|
|
||||||
{
|
|
||||||
"device_ips": ["192.168.1.1", "192.168.1.2"],
|
|
||||||
"config": {"vlans": [{"id": 100, "name": "test"}]},
|
|
||||||
"credentials": {"username": "admin", "password": "secret"},
|
|
||||||
"vendor": "cisco"
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
devices = [{
|
|
||||||
"ip": ip,
|
|
||||||
"credentials": request.credentials,
|
|
||||||
"vendor": request.vendor
|
|
||||||
} for ip in request.device_ips]
|
|
||||||
|
|
||||||
try:
|
|
||||||
batch = BatchService()
|
|
||||||
bg_tasks.add_task(batch.deploy_batch, devices, request.config)
|
|
||||||
return {"message": "Batch job started", "device_count": len(devices)}
|
|
||||||
except Exception as e:
|
|
||||||
raise HTTPException(500, detail=str(e))
|
|
@ -1,68 +1,69 @@
|
|||||||
from fastapi import APIRouter, HTTPException
|
from typing import Dict, Any
|
||||||
from pydantic import BaseModel
|
from src.backend.app.services.ai_services import AIService
|
||||||
from typing import Optional
|
|
||||||
from src.backend.config import settings
|
from src.backend.config import settings
|
||||||
from src.backend.app.services.ai_service import call_ai_api
|
|
||||||
import logging
|
|
||||||
|
|
||||||
router = APIRouter()
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class CommandRequest(BaseModel):
|
class CommandParser:
|
||||||
command: str
|
def __init__(self):
|
||||||
device_type: Optional[str] = "switch"
|
self.ai_service = AIService(settings.SILICONFLOW_API_KEY, settings.SILICONFLOW_API_URL)
|
||||||
vendor: Optional[str] = "cisco"
|
|
||||||
|
|
||||||
|
async def parse(self, command: str) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
解析中文命令并返回配置
|
||||||
|
"""
|
||||||
|
# 首先尝试本地简单解析
|
||||||
|
local_parsed = self._try_local_parse(command)
|
||||||
|
if local_parsed:
|
||||||
|
return local_parsed
|
||||||
|
|
||||||
class CommandResponse(BaseModel):
|
# 本地无法解析则调用AI服务
|
||||||
original_command: str
|
return await self.ai_service.parse_command(command)
|
||||||
parsed_config: dict
|
|
||||||
success: bool
|
|
||||||
message: Optional[str] = None
|
|
||||||
|
|
||||||
|
def _try_local_parse(self, command: str) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
尝试本地解析常见命令
|
||||||
|
"""
|
||||||
|
command = command.lower().strip()
|
||||||
|
|
||||||
@router.post("", response_model=CommandResponse)
|
# VLAN配置
|
||||||
async def parse_command(request: CommandRequest):
|
if "vlan" in command and "创建" in command:
|
||||||
"""
|
parts = command.split()
|
||||||
解析中文网络配置命令,返回JSON格式的配置
|
vlan_id = next((p for p in parts if p.isdigit()), None)
|
||||||
|
if vlan_id:
|
||||||
|
return {
|
||||||
|
"type": "vlan",
|
||||||
|
"vlan_id": int(vlan_id),
|
||||||
|
"name": f"VLAN{vlan_id}",
|
||||||
|
"interfaces": []
|
||||||
|
}
|
||||||
|
|
||||||
参数:
|
# 接口配置
|
||||||
- command: 中文配置命令,如"创建VLAN 100,名称为财务部"
|
if any(word in command for word in ["接口", "端口"]) and any(
|
||||||
- device_type: 设备类型,默认为switch
|
word in command for word in ["启用", "关闭", "配置"]):
|
||||||
- vendor: 设备厂商,默认为cisco
|
parts = command.split()
|
||||||
|
interface = next((p for p in parts if p.startswith(("gi", "fa", "te", "eth"))), None)
|
||||||
|
if interface:
|
||||||
|
config = {
|
||||||
|
"type": "interface",
|
||||||
|
"interface": interface,
|
||||||
|
"state": "up" if "启用" in command else "down"
|
||||||
|
}
|
||||||
|
|
||||||
返回:
|
if "ip" in command and "地址" in command:
|
||||||
- 解析后的JSON配置
|
ip_part = next((p for p in parts if "." in p and p.count(".") == 3), None)
|
||||||
"""
|
if ip_part:
|
||||||
try:
|
config["ip_address"] = ip_part
|
||||||
logger.info(f"Received command: {request.command}")
|
|
||||||
|
|
||||||
# 调用AI服务解析命令
|
if "描述" in command:
|
||||||
ai_response = await call_ai_api(
|
desc_start = command.find("描述") + 2
|
||||||
command=request.command,
|
description = command[desc_start:].strip()
|
||||||
device_type=request.device_type,
|
config["description"] = description
|
||||||
vendor=request.vendor,
|
|
||||||
api_key=settings.ai_api_key
|
|
||||||
)
|
|
||||||
|
|
||||||
if not ai_response.get("success"):
|
if "vlan" in command:
|
||||||
raise HTTPException(
|
vlan_id = next((p for p in parts if p.isdigit()), None)
|
||||||
status_code=400,
|
if vlan_id:
|
||||||
detail=ai_response.get("message", "Failed to parse command")
|
config["vlan"] = int(vlan_id)
|
||||||
)
|
|
||||||
|
|
||||||
return CommandResponse(
|
return config
|
||||||
original_command=request.command,
|
|
||||||
parsed_config=ai_response["config"],
|
|
||||||
success=True,
|
|
||||||
message="Command parsed successfully"
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as e:
|
return None
|
||||||
logger.error(f"Error parsing command: {str(e)}")
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=500,
|
|
||||||
detail=f"Error processing command: {str(e)}"
|
|
||||||
)
|
|
50
src/backend/app/api/endpoints.py
Normal file
50
src/backend/app/api/endpoints.py
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
from typing import Any
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from src.backend.app.services.ai_services import AIService
|
||||||
|
from src.backend.app.api.network_config import SwitchConfigurator
|
||||||
|
from src.backend.config import settings
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
class CommandRequest(BaseModel):
|
||||||
|
command: str
|
||||||
|
|
||||||
|
class ConfigRequest(BaseModel):
|
||||||
|
config: dict
|
||||||
|
switch_ip: str
|
||||||
|
|
||||||
|
@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=settings.SWITCH_USERNAME,
|
||||||
|
password=settings.SWITCH_PASSWORD,
|
||||||
|
timeout=settings.SWITCH_TIMEOUT
|
||||||
|
)
|
||||||
|
result = await configurator.apply_config(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)}"
|
||||||
|
)
|
@ -1,17 +0,0 @@
|
|||||||
from fastapi import APIRouter
|
|
||||||
from ...monitoring.healthcheck import check_redis, check_ai_service
|
|
||||||
|
|
||||||
router = APIRouter()
|
|
||||||
|
|
||||||
@router.get("/live")
|
|
||||||
async def liveness_check():
|
|
||||||
return {"status": "alive"}
|
|
||||||
|
|
||||||
@router.get("/ready")
|
|
||||||
async def readiness_check():
|
|
||||||
redis_ok = await check_redis()
|
|
||||||
ai_ok = await check_ai_service()
|
|
||||||
return {
|
|
||||||
"redis": redis_ok,
|
|
||||||
"ai_service": ai_ok
|
|
||||||
}
|
|
@ -1,84 +1,174 @@
|
|||||||
from fastapi import APIRouter, HTTPException
|
import paramiko
|
||||||
from pydantic import BaseModel
|
import asyncio
|
||||||
from typing import Optional
|
from typing import Dict, Any
|
||||||
import logging
|
from ..utils.exceptions import SwitchConfigException
|
||||||
import requests
|
|
||||||
|
|
||||||
router = APIRouter()
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class ConfigRequest(BaseModel):
|
class SwitchConfigurator:
|
||||||
config: dict
|
def __init__(self, username: str, password: str, timeout: int = 10):
|
||||||
device_ip: str
|
self.username = username
|
||||||
credentials: dict
|
self.password = password
|
||||||
dry_run: Optional[bool] = True
|
self.timeout = timeout
|
||||||
|
|
||||||
|
async def apply_config(self, switch_ip: str, config: Dict[str, Any]) -> str:
|
||||||
|
"""
|
||||||
|
应用配置到交换机
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 根据配置类型调用不同的方法
|
||||||
|
config_type = config.get("type", "").lower()
|
||||||
|
|
||||||
class ConfigResponse(BaseModel):
|
if config_type == "vlan":
|
||||||
success: bool
|
return await self._configure_vlan(switch_ip, config)
|
||||||
message: str
|
elif config_type == "interface":
|
||||||
applied_config: Optional[dict] = None
|
return await self._configure_interface(switch_ip, config)
|
||||||
device_response: Optional[str] = None
|
elif config_type == "acl":
|
||||||
|
return await self._configure_acl(switch_ip, config)
|
||||||
|
elif config_type == "route":
|
||||||
|
return await self._configure_route(switch_ip, config)
|
||||||
|
else:
|
||||||
|
raise SwitchConfigException(f"Unsupported config type: {config_type}")
|
||||||
|
except Exception as e:
|
||||||
|
raise SwitchConfigException(str(e))
|
||||||
|
|
||||||
|
async def _send_commands(self, switch_ip: str, commands: list) -> str:
|
||||||
|
"""
|
||||||
|
发送命令到交换机
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 使用Paramiko建立SSH连接
|
||||||
|
ssh = paramiko.SSHClient()
|
||||||
|
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
|
||||||
@router.post("", response_model=ConfigResponse)
|
# 在异步上下文中运行阻塞操作
|
||||||
async def apply_config(request: ConfigRequest):
|
loop = asyncio.get_event_loop()
|
||||||
"""
|
await loop.run_in_executor(
|
||||||
将生成的配置应用到网络设备
|
None,
|
||||||
|
lambda: ssh.connect(
|
||||||
参数:
|
switch_ip,
|
||||||
- config: 生成的JSON配置
|
username=self.username,
|
||||||
- device_ip: 目标设备IP地址
|
password=self.password,
|
||||||
- credentials: 设备登录凭证 {username: str, password: str}
|
timeout=self.timeout
|
||||||
- dry_run: 是否仅测试而不实际应用,默认为True
|
)
|
||||||
|
|
||||||
返回:
|
|
||||||
- 应用结果和设备响应
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
logger.info(f"Applying config to device {request.device_ip}")
|
|
||||||
|
|
||||||
# 这里应该是实际与交换机交互的逻辑
|
|
||||||
# 由于不同厂商设备交互方式不同,这里只是一个示例
|
|
||||||
|
|
||||||
if request.dry_run:
|
|
||||||
logger.info("Dry run mode - not actually applying config")
|
|
||||||
return ConfigResponse(
|
|
||||||
success=True,
|
|
||||||
message="Dry run successful - config not applied",
|
|
||||||
applied_config=request.config
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# 模拟与设备交互
|
# 获取SSH shell
|
||||||
device_response = simulate_device_interaction(
|
shell = await loop.run_in_executor(None, ssh.invoke_shell)
|
||||||
request.device_ip,
|
|
||||||
request.credentials,
|
|
||||||
request.config
|
|
||||||
)
|
|
||||||
|
|
||||||
return ConfigResponse(
|
# 发送配置命令
|
||||||
success=True,
|
output = ""
|
||||||
message="Config applied successfully",
|
for cmd in commands:
|
||||||
applied_config=request.config,
|
await loop.run_in_executor(None, shell.send, cmd + "\n")
|
||||||
device_response=device_response
|
await asyncio.sleep(0.5)
|
||||||
)
|
while shell.recv_ready():
|
||||||
|
output += (await loop.run_in_executor(None, shell.recv, 1024)).decode("utf-8")
|
||||||
|
|
||||||
except Exception as e:
|
# 关闭连接
|
||||||
logger.error(f"Error applying config: {str(e)}")
|
await loop.run_in_executor(None, ssh.close)
|
||||||
raise HTTPException(
|
|
||||||
status_code=500,
|
|
||||||
detail=f"Error applying config: {str(e)}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
return output
|
||||||
|
except Exception as e:
|
||||||
|
raise SwitchConfigException(f"SSH connection failed: {str(e)}")
|
||||||
|
|
||||||
def simulate_device_interaction(device_ip: str, credentials: dict, config: dict) -> str:
|
async def _configure_vlan(self, switch_ip: str, config: Dict[str, Any]) -> str:
|
||||||
"""
|
"""
|
||||||
模拟与网络设备的交互
|
配置VLAN
|
||||||
|
"""
|
||||||
|
vlan_id = config["vlan_id"]
|
||||||
|
vlan_name = config.get("name", f"VLAN{vlan_id}")
|
||||||
|
interfaces = config.get("interfaces", [])
|
||||||
|
|
||||||
在实际实现中,这里会使用netmiko、paramiko或厂商特定的SDK
|
commands = [
|
||||||
与设备建立连接并推送配置
|
"configure terminal",
|
||||||
"""
|
f"vlan {vlan_id}",
|
||||||
# 这里只是一个模拟实现
|
f"name {vlan_name}",
|
||||||
return f"Config applied to {device_ip} successfully. {len(config)} commands executed."
|
]
|
||||||
|
|
||||||
|
# 配置接口
|
||||||
|
for intf in interfaces:
|
||||||
|
commands.extend([
|
||||||
|
f"interface {intf['interface']}",
|
||||||
|
f"switchport access vlan {vlan_id}",
|
||||||
|
"exit"
|
||||||
|
])
|
||||||
|
|
||||||
|
commands.append("end")
|
||||||
|
|
||||||
|
return await self._send_commands(switch_ip, commands)
|
||||||
|
|
||||||
|
async def _configure_interface(self, switch_ip: str, config: Dict[str, Any]) -> str:
|
||||||
|
"""
|
||||||
|
配置接口
|
||||||
|
"""
|
||||||
|
interface = config["interface"]
|
||||||
|
ip_address = config.get("ip_address")
|
||||||
|
description = config.get("description", "")
|
||||||
|
vlan = config.get("vlan")
|
||||||
|
state = config.get("state", "up")
|
||||||
|
|
||||||
|
commands = [
|
||||||
|
"configure terminal",
|
||||||
|
f"interface {interface}",
|
||||||
|
f"description {description}",
|
||||||
|
]
|
||||||
|
|
||||||
|
if ip_address:
|
||||||
|
commands.append(f"ip address {ip_address}")
|
||||||
|
|
||||||
|
if vlan:
|
||||||
|
commands.append(f"switchport access vlan {vlan}")
|
||||||
|
|
||||||
|
if state.lower() == "up":
|
||||||
|
commands.append("no shutdown")
|
||||||
|
else:
|
||||||
|
commands.append("shutdown")
|
||||||
|
|
||||||
|
commands.extend(["exit", "end"])
|
||||||
|
|
||||||
|
return await self._send_commands(switch_ip, commands)
|
||||||
|
|
||||||
|
async def _configure_acl(self, switch_ip: str, config: Dict[str, Any]) -> str:
|
||||||
|
"""
|
||||||
|
配置ACL
|
||||||
|
"""
|
||||||
|
acl_id = config["acl_id"]
|
||||||
|
acl_type = config.get("type", "standard")
|
||||||
|
rules = config.get("rules", [])
|
||||||
|
|
||||||
|
commands = ["configure terminal"]
|
||||||
|
|
||||||
|
if acl_type == "standard":
|
||||||
|
commands.append(f"access-list {acl_id} standard")
|
||||||
|
else:
|
||||||
|
commands.append(f"access-list {acl_id} extended")
|
||||||
|
|
||||||
|
for rule in rules:
|
||||||
|
action = rule.get("action", "permit")
|
||||||
|
source = rule.get("source", "any")
|
||||||
|
destination = rule.get("destination", "any")
|
||||||
|
protocol = rule.get("protocol", "ip")
|
||||||
|
|
||||||
|
if acl_type == "standard":
|
||||||
|
commands.append(f"{action} {source}")
|
||||||
|
else:
|
||||||
|
commands.append(f"{action} {protocol} {source} {destination}")
|
||||||
|
|
||||||
|
commands.append("end")
|
||||||
|
|
||||||
|
return await self._send_commands(switch_ip, commands)
|
||||||
|
|
||||||
|
async def _configure_route(self, switch_ip: str, config: Dict[str, Any]) -> str:
|
||||||
|
"""
|
||||||
|
配置路由
|
||||||
|
"""
|
||||||
|
network = config["network"]
|
||||||
|
mask = config["mask"]
|
||||||
|
next_hop = config["next_hop"]
|
||||||
|
|
||||||
|
commands = [
|
||||||
|
"configure terminal",
|
||||||
|
f"ip route {network} {mask} {next_hop}",
|
||||||
|
"end"
|
||||||
|
]
|
||||||
|
|
||||||
|
return await self._send_commands(switch_ip, commands)
|
@ -1,20 +0,0 @@
|
|||||||
from fastapi import APIRouter, BackgroundTasks
|
|
||||||
from pydantic import BaseModel
|
|
||||||
from ...services.task_service import deploy_to_device
|
|
||||||
|
|
||||||
router = APIRouter()
|
|
||||||
|
|
||||||
class TopologyRequest(BaseModel):
|
|
||||||
devices: list
|
|
||||||
config: dict
|
|
||||||
|
|
||||||
@router.post("/deploy")
|
|
||||||
async def deploy_topology(
|
|
||||||
request: TopologyRequest,
|
|
||||||
bg_tasks: BackgroundTasks
|
|
||||||
):
|
|
||||||
task_ids = []
|
|
||||||
for device in request.devices:
|
|
||||||
task = deploy_to_device.delay(device, request.config)
|
|
||||||
task_ids.append(task.id)
|
|
||||||
return {"task_ids": task_ids}
|
|
@ -1 +0,0 @@
|
|||||||
# 这个文件保持为空,用于标识app为一个Python包
|
|
@ -1,22 +0,0 @@
|
|||||||
from fastapi import FastAPI
|
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
|
||||||
from src.backend.app.api.command_parser import router as api_router
|
|
||||||
from src.backend.config import settings
|
|
||||||
|
|
||||||
app = FastAPI(title=settings.app_name)
|
|
||||||
|
|
||||||
# CORS配置
|
|
||||||
app.add_middleware(
|
|
||||||
CORSMiddleware,
|
|
||||||
allow_origins=["*"],
|
|
||||||
allow_credentials=True,
|
|
||||||
allow_methods=["*"],
|
|
||||||
allow_headers=["*"],
|
|
||||||
)
|
|
||||||
|
|
||||||
# 包含API路由
|
|
||||||
app.include_router(api_router, prefix="/api")
|
|
||||||
|
|
||||||
@app.get("/")
|
|
||||||
async def root():
|
|
||||||
return {"message": "Network Configuration API is running"}
|
|
@ -1,10 +1,30 @@
|
|||||||
from .device import DeviceCredentials, DeviceInfo
|
# 数据模型模块初始化文件
|
||||||
from .topology import TopologyType, DeviceRole, NetworkTopology
|
# 目前项目不使用数据库,此文件保持为空
|
||||||
|
# 未来如果需要添加数据库模型,可以在这里定义
|
||||||
|
|
||||||
__all__ = [
|
from pydantic import BaseModel
|
||||||
'DeviceCredentials',
|
from typing import Optional
|
||||||
'DeviceInfo',
|
|
||||||
'TopologyType',
|
# 示例:基础响应模型
|
||||||
'DeviceRole',
|
class BaseResponse(BaseModel):
|
||||||
'NetworkTopology'
|
success: bool
|
||||||
]
|
message: Optional[str] = None
|
||||||
|
data: Optional[dict] = None
|
||||||
|
|
||||||
|
# 示例:交换机连接信息模型(如果需要存储交换机信息)
|
||||||
|
class SwitchInfo(BaseModel):
|
||||||
|
ip: str
|
||||||
|
username: str
|
||||||
|
password: str
|
||||||
|
model: Optional[str] = None
|
||||||
|
description: Optional[str] = None
|
||||||
|
|
||||||
|
# 示例:配置历史记录模型
|
||||||
|
class ConfigHistory(BaseModel):
|
||||||
|
command: str
|
||||||
|
config: dict
|
||||||
|
timestamp: float
|
||||||
|
status: str # success/failed
|
||||||
|
error: Optional[str] = None
|
||||||
|
|
||||||
|
__all__ = ["BaseResponse", "SwitchInfo", "ConfigHistory"]
|
@ -1,14 +0,0 @@
|
|||||||
from pydantic import BaseModel
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
class DeviceCredentials(BaseModel):
|
|
||||||
username: str
|
|
||||||
password: str
|
|
||||||
enable_password: Optional[str] = None
|
|
||||||
|
|
||||||
class DeviceInfo(BaseModel):
|
|
||||||
ip: str
|
|
||||||
vendor: str
|
|
||||||
model: Optional[str] = None
|
|
||||||
os_version: Optional[str] = None
|
|
||||||
credentials: DeviceCredentials
|
|
@ -1,20 +0,0 @@
|
|||||||
#拓补数据结构
|
|
||||||
from enum import Enum
|
|
||||||
from typing import Dict, List
|
|
||||||
from pydantic import BaseModel
|
|
||||||
|
|
||||||
class TopologyType(str, Enum):
|
|
||||||
SPINE_LEAF = "spine-leaf"
|
|
||||||
CORE_ACCESS = "core-access"
|
|
||||||
RING = "ring"
|
|
||||||
|
|
||||||
class DeviceRole(str, Enum):
|
|
||||||
CORE = "core"
|
|
||||||
SPINE = "spine"
|
|
||||||
LEAF = "leaf"
|
|
||||||
ACCESS = "access"
|
|
||||||
|
|
||||||
class NetworkTopology(BaseModel):
|
|
||||||
type: TopologyType
|
|
||||||
devices: Dict[DeviceRole, List[str]]
|
|
||||||
links: Dict[str, List[str]]
|
|
@ -1,42 +0,0 @@
|
|||||||
from prometheus_client import (
|
|
||||||
Counter,
|
|
||||||
Gauge,
|
|
||||||
Histogram,
|
|
||||||
Summary
|
|
||||||
)
|
|
||||||
|
|
||||||
# API Metrics
|
|
||||||
API_REQUESTS = Counter(
|
|
||||||
'api_requests_total',
|
|
||||||
'Total API requests',
|
|
||||||
['method', 'endpoint', 'status']
|
|
||||||
)
|
|
||||||
|
|
||||||
API_LATENCY = Histogram(
|
|
||||||
'api_request_latency_seconds',
|
|
||||||
'API request latency',
|
|
||||||
['endpoint']
|
|
||||||
)
|
|
||||||
|
|
||||||
# Device Metrics
|
|
||||||
DEVICE_CONNECTIONS = Gauge(
|
|
||||||
'network_device_connections',
|
|
||||||
'Active device connections',
|
|
||||||
['vendor']
|
|
||||||
)
|
|
||||||
|
|
||||||
CONFIG_APPLY_TIME = Summary(
|
|
||||||
'config_apply_seconds',
|
|
||||||
'Time spent applying configurations'
|
|
||||||
)
|
|
||||||
|
|
||||||
# Error Metrics
|
|
||||||
CONFIG_ERRORS = Counter(
|
|
||||||
'config_errors_total',
|
|
||||||
'Configuration errors',
|
|
||||||
['error_type']
|
|
||||||
)
|
|
||||||
|
|
||||||
def observe_api_request(method: str, endpoint: str, status: int, duration: float):
|
|
||||||
API_REQUESTS.labels(method, endpoint, status).inc()
|
|
||||||
API_LATENCY.labels(endpoint).observe(duration)
|
|
@ -1,31 +0,0 @@
|
|||||||
from prometheus_client import Counter, Histogram
|
|
||||||
from fastapi import Request
|
|
||||||
|
|
||||||
REQUESTS = Counter(
|
|
||||||
'api_requests_total',
|
|
||||||
'Total API Requests',
|
|
||||||
['method', 'endpoint']
|
|
||||||
)
|
|
||||||
|
|
||||||
LATENCY = Histogram(
|
|
||||||
'api_request_latency_seconds',
|
|
||||||
'API Request Latency',
|
|
||||||
['endpoint']
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def monitor_requests(request: Request, call_next):
|
|
||||||
start_time = time.time()
|
|
||||||
response = await call_next(request)
|
|
||||||
latency = time.time() - start_time
|
|
||||||
|
|
||||||
REQUESTS.labels(
|
|
||||||
method=request.method,
|
|
||||||
endpoint=request.url.path
|
|
||||||
).inc()
|
|
||||||
|
|
||||||
LATENCY.labels(
|
|
||||||
endpoint=request.url.path
|
|
||||||
).observe(latency)
|
|
||||||
|
|
||||||
return response
|
|
@ -1,16 +0,0 @@
|
|||||||
from .task_service import celery_app
|
|
||||||
from .ai_service import AIService
|
|
||||||
from .topology import TopologyService
|
|
||||||
from .batch import BatchService
|
|
||||||
|
|
||||||
# 单例服务实例
|
|
||||||
ai_service = AIService()
|
|
||||||
topology_service = TopologyService()
|
|
||||||
batch_service = BatchService()
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
'celery_app',
|
|
||||||
'ai_service',
|
|
||||||
'topology_service',
|
|
||||||
'batch_service'
|
|
||||||
]
|
|
@ -1,59 +1,61 @@
|
|||||||
import aiohttp
|
import json
|
||||||
import logging
|
import httpx
|
||||||
from typing import Dict, Any
|
from typing import Dict, Any
|
||||||
from src.backend.config import settings
|
from src.backend.app.utils.exceptions import SiliconFlowAPIException
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
async def call_ai_api(command: str, device_type: str, vendor: str, api_key: str) -> Dict[str, Any]:
|
class AIService:
|
||||||
"""
|
def __init__(self, api_key: str, api_url: str):
|
||||||
调用硅基流动API解析中文命令
|
self.api_key = api_key
|
||||||
|
self.api_url = api_url
|
||||||
参数:
|
self.headers = {
|
||||||
- command: 中文配置命令
|
"Authorization": f"Bearer {self.api_key}",
|
||||||
- device_type: 设备类型
|
"Content-Type": "application/json"
|
||||||
- vendor: 设备厂商
|
|
||||||
- api_key: API密钥
|
|
||||||
|
|
||||||
返回:
|
|
||||||
- 解析后的配置和状态信息
|
|
||||||
"""
|
|
||||||
url = settings.ai_api_url
|
|
||||||
|
|
||||||
headers = {
|
|
||||||
"Authorization": f"Bearer {api_key}",
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
}
|
|
||||||
|
|
||||||
payload = {
|
|
||||||
"command": command,
|
|
||||||
"device_type": device_type,
|
|
||||||
"vendor": vendor,
|
|
||||||
"output_format": "json"
|
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
|
||||||
async with aiohttp.ClientSession() as session:
|
|
||||||
async with session.post(url, json=payload, headers=headers) as response:
|
|
||||||
if response.status != 200:
|
|
||||||
error = await response.text()
|
|
||||||
logger.error(f"AI API error: {error}")
|
|
||||||
return {
|
|
||||||
"success": False,
|
|
||||||
"message": f"AI API returned {response.status}: {error}"
|
|
||||||
}
|
|
||||||
|
|
||||||
data = await response.json()
|
|
||||||
return {
|
|
||||||
"success": True,
|
|
||||||
"config": data.get("config", {}),
|
|
||||||
"message": data.get("message", "Command parsed successfully")
|
|
||||||
}
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error calling AI API: {str(e)}")
|
|
||||||
return {
|
|
||||||
"success": False,
|
|
||||||
"message": f"Error calling AI API: {str(e)}"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async def parse_command(self, command: str) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
调用硅基流动API解析中文命令
|
||||||
|
"""
|
||||||
|
prompt = f"""
|
||||||
|
你是一个网络设备配置专家,请将以下中文命令转换为网络设备配置JSON。
|
||||||
|
支持的配置包括:VLAN、端口、路由、ACL等。
|
||||||
|
返回格式必须为JSON,包含配置类型和详细参数。
|
||||||
|
|
||||||
|
命令:{command}
|
||||||
|
"""
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"model": "text-davinci-003",
|
||||||
|
"prompt": prompt,
|
||||||
|
"max_tokens": 1000,
|
||||||
|
"temperature": 0.3
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
response = await client.post(
|
||||||
|
f"{self.api_url}/completions",
|
||||||
|
headers=self.headers,
|
||||||
|
json=data,
|
||||||
|
timeout=30
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code != 200:
|
||||||
|
raise SiliconFlowAPIException(response.text)
|
||||||
|
|
||||||
|
result = response.json()
|
||||||
|
config_str = result["choices"][0]["text"].strip()
|
||||||
|
|
||||||
|
# 确保返回的是有效的JSON
|
||||||
|
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 httpx.HTTPError as e:
|
||||||
|
raise SiliconFlowAPIException(str(e))
|
@ -1,48 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
from typing import List, Dict, Any
|
|
||||||
from app.adapters.factory import AdapterFactory
|
|
||||||
from app.utils.connection_pool import ConnectionPool
|
|
||||||
from app.monitoring.metrics import (
|
|
||||||
DEVICE_CONNECTIONS,
|
|
||||||
CONFIG_APPLY_TIME,
|
|
||||||
CONFIG_ERRORS
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class BatchService:
|
|
||||||
def __init__(self, max_workers: int = 10):
|
|
||||||
self.semaphore = asyncio.Semaphore(max_workers)
|
|
||||||
self.pool = ConnectionPool()
|
|
||||||
|
|
||||||
@CONFIG_APPLY_TIME.time()
|
|
||||||
async def deploy_batch(self, devices: List[Dict], config: Dict[str, Any]):
|
|
||||||
async def _deploy(device):
|
|
||||||
vendor = device.get('vendor', 'cisco')
|
|
||||||
async with self.semaphore:
|
|
||||||
try:
|
|
||||||
adapter = AdapterFactory.get_adapter(vendor)
|
|
||||||
await adapter.connect(device['ip'], device['credentials'])
|
|
||||||
DEVICE_CONNECTIONS.labels(vendor).inc()
|
|
||||||
|
|
||||||
result = await adapter.deploy_config(config)
|
|
||||||
return {
|
|
||||||
"device": device['ip'],
|
|
||||||
"status": "success",
|
|
||||||
"result": result
|
|
||||||
}
|
|
||||||
except ConnectionError as e:
|
|
||||||
CONFIG_ERRORS.labels("connection").inc()
|
|
||||||
return {
|
|
||||||
"device": device['ip'],
|
|
||||||
"status": "failed",
|
|
||||||
"error": str(e)
|
|
||||||
}
|
|
||||||
finally:
|
|
||||||
if adapter:
|
|
||||||
await adapter.disconnect()
|
|
||||||
DEVICE_CONNECTIONS.labels(vendor).dec()
|
|
||||||
|
|
||||||
return await asyncio.gather(
|
|
||||||
*[_deploy(device) for device in devices],
|
|
||||||
return_exceptions=True
|
|
||||||
)
|
|
@ -1,18 +0,0 @@
|
|||||||
#Celery任务定义
|
|
||||||
from celery import Celery
|
|
||||||
from src.backend.app.utils.connection_pool import ConnectionPool
|
|
||||||
from src.backend.config import settings
|
|
||||||
|
|
||||||
celery = Celery(__name__, broker=settings.REDIS_URL)
|
|
||||||
pool = ConnectionPool(max_size=settings.MAX_CONNECTIONS)
|
|
||||||
|
|
||||||
@celery.task
|
|
||||||
async def deploy_to_device(device_info: dict, config: dict):
|
|
||||||
adapter = await pool.get(device_info['vendor'])
|
|
||||||
try:
|
|
||||||
await adapter.connect(device_info['ip'], device_info['credentials'])
|
|
||||||
result = await adapter.deploy_config(config)
|
|
||||||
await pool.release(adapter)
|
|
||||||
return {'device': device_info['ip'], 'result': result}
|
|
||||||
except Exception as e:
|
|
||||||
return {'device': device_info['ip'], 'error': str(e)}
|
|
@ -1,23 +0,0 @@
|
|||||||
#拓补处理逻辑
|
|
||||||
def generate_multi_device_config(topology):
|
|
||||||
"""
|
|
||||||
topology示例:
|
|
||||||
{
|
|
||||||
"core_switches": [sw1, sw2],
|
|
||||||
"access_switches": {
|
|
||||||
"sw1": [sw3, sw4],
|
|
||||||
"sw2": [sw5, sw6]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
configs = {}
|
|
||||||
# 生成核心层配置(如MSTP根桥选举)
|
|
||||||
for sw in topology['core_switches']:
|
|
||||||
configs[sw] = generate_core_config(sw)
|
|
||||||
|
|
||||||
# 生成接入层配置(如端口绑定)
|
|
||||||
for core_sw, access_sws in topology['access_switches'].items():
|
|
||||||
for sw in access_sws:
|
|
||||||
configs[sw] = generate_access_config(sw, uplink=core_sw)
|
|
||||||
|
|
||||||
return configs
|
|
@ -1,22 +0,0 @@
|
|||||||
#连接池
|
|
||||||
# /backend/app/utils/connection_pool.py
|
|
||||||
import asyncio
|
|
||||||
from collections import deque
|
|
||||||
from ..adapters import cisco, huawei
|
|
||||||
|
|
||||||
class ConnectionPool:
|
|
||||||
def __init__(self, max_size=10):
|
|
||||||
self.max_size = max_size
|
|
||||||
self.pool = deque(maxlen=max_size)
|
|
||||||
self.lock = asyncio.Lock()
|
|
||||||
|
|
||||||
async def get(self, vendor: str):
|
|
||||||
async with self.lock:
|
|
||||||
if self.pool:
|
|
||||||
return self.pool.pop()
|
|
||||||
return CiscoAdapter() if vendor == 'cisco' else HuaweiAdapter()
|
|
||||||
|
|
||||||
async def release(self, adapter):
|
|
||||||
async with self.lock:
|
|
||||||
if len(self.pool) < self.max_size:
|
|
||||||
self.pool.append(adapter)
|
|
22
src/backend/app/utils/exceptions.py
Normal file
22
src/backend/app/utils/exceptions.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
from fastapi import HTTPException, status
|
||||||
|
|
||||||
|
class AICommandParseException(HTTPException):
|
||||||
|
def __init__(self, detail: str):
|
||||||
|
super().__init__(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail=f"AI command parse error: {detail}"
|
||||||
|
)
|
||||||
|
|
||||||
|
class SwitchConfigException(HTTPException):
|
||||||
|
def __init__(self, detail: str):
|
||||||
|
super().__init__(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail=f"Switch configuration error: {detail}"
|
||||||
|
)
|
||||||
|
|
||||||
|
class SiliconFlowAPIException(HTTPException):
|
||||||
|
def __init__(self, detail: str):
|
||||||
|
super().__init__(
|
||||||
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||||
|
detail=f"SiliconFlow API error: {detail}"
|
||||||
|
)
|
33
src/backend/app/utils/logger.py
Normal file
33
src/backend/app/utils/logger.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import logging
|
||||||
|
from loguru import logger
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
class InterceptHandler(logging.Handler):
|
||||||
|
def emit(self, record):
|
||||||
|
# Get corresponding Loguru level if it exists
|
||||||
|
try:
|
||||||
|
level = logger.level(record.levelname).name
|
||||||
|
except ValueError:
|
||||||
|
level = record.levelno
|
||||||
|
|
||||||
|
# Find caller from where originated the logged message
|
||||||
|
frame, depth = logging.currentframe(), 2
|
||||||
|
while frame.f_code.co_filename == logging.__file__:
|
||||||
|
frame = frame.f_back
|
||||||
|
depth += 1
|
||||||
|
|
||||||
|
logger.opt(depth=depth, exception=record.exc_info).log(level, record.getMessage())
|
||||||
|
|
||||||
|
|
||||||
|
def setup_logging():
|
||||||
|
# 拦截标准logging
|
||||||
|
logging.basicConfig(handlers=[InterceptHandler()], level=0)
|
||||||
|
|
||||||
|
# 配置loguru
|
||||||
|
logger.configure(
|
||||||
|
handlers=[
|
||||||
|
{"sink": sys.stdout,
|
||||||
|
"format": "<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | <level>{level: <8}</level> | <cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - <level>{message}</level>"}
|
||||||
|
]
|
||||||
|
)
|
@ -1,9 +0,0 @@
|
|||||||
broker_url = 'redis://redis:6379/0'
|
|
||||||
result_backend = 'redis://redis:6379/1'
|
|
||||||
task_serializer = 'json'
|
|
||||||
result_serializer = 'json'
|
|
||||||
accept_content = ['json']
|
|
||||||
timezone = 'UTC'
|
|
||||||
enable_utc = True
|
|
||||||
task_track_started = True
|
|
||||||
task_time_limit = 300
|
|
@ -1,17 +1,23 @@
|
|||||||
from pydantic_settings import BaseSettings
|
import os
|
||||||
from pydantic import Field
|
from pydantic import BaseSettings
|
||||||
|
|
||||||
|
|
||||||
class Settings(BaseSettings):
|
class Settings(BaseSettings):
|
||||||
app_name: str = "Network Automation API"
|
APP_NAME: str = "AI Network Configurator"
|
||||||
redis_url: str = Field("redis://localhost:6379", env="REDIS_URL")
|
DEBUG: bool = True
|
||||||
ai_api_key: str = Field(..., env="AI_API_KEY")
|
API_PREFIX: str = "/api"
|
||||||
max_connections: int = Field(50, env="MAX_CONNECTIONS")
|
|
||||||
default_timeout: int = Field(30, env="DEFAULT_TIMEOUT")
|
# 硅基流动API配置
|
||||||
|
SILICONFLOW_API_KEY: str = os.getenv("SILICONFLOW_API_KEY", "")
|
||||||
|
SILICONFLOW_API_URL: str = os.getenv("SILICONFLOW_API_URL", "https://api.siliconflow.ai/v1")
|
||||||
|
|
||||||
|
# 交换机配置
|
||||||
|
SWITCH_USERNAME: str = os.getenv("SWITCH_USERNAME", "admin")
|
||||||
|
SWITCH_PASSWORD: str = os.getenv("SWITCH_PASSWORD", "admin")
|
||||||
|
SWITCH_TIMEOUT: int = os.getenv("SWITCH_TIMEOUT", 10)
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
env_file = ".env"
|
env_file = ".env"
|
||||||
extra = "ignore"
|
|
||||||
|
|
||||||
|
|
||||||
settings = Settings()
|
settings = Settings()
|
@ -1,11 +0,0 @@
|
|||||||
class InvalidInputError(Exception):
|
|
||||||
"""输入验证异常"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
class NetworkDeviceError(Exception):
|
|
||||||
"""网络设备通信异常"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
class AIServiceError(Exception):
|
|
||||||
"""AI服务异常"""
|
|
||||||
pass
|
|
@ -1,8 +1,7 @@
|
|||||||
fastapi==0.109.1
|
fastapi==0.95.2
|
||||||
uvicorn==0.27.0
|
uvicorn==0.22.0
|
||||||
python-dotenv==1.0.0
|
python-dotenv==1.0.0
|
||||||
celery==5.3.6
|
requests==2.28.2
|
||||||
redis==4.6.0
|
paramiko==3.1.0
|
||||||
netmiko==4.2.0
|
pydantic==1.10.7
|
||||||
asyncssh==2.14.10.0
|
loguru==0.7.0
|
||||||
prometheus-client==0.2
|
|
@ -1,11 +1,13 @@
|
|||||||
import uvicorn
|
import uvicorn
|
||||||
from app.main import app
|
from src.backend.app import create_app
|
||||||
|
|
||||||
|
app = create_app()
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
uvicorn.run(
|
uvicorn.run(
|
||||||
"app.main:app",
|
app,
|
||||||
host="0.0.0.0",
|
host="0.0.0.0",
|
||||||
port=8000,
|
port=8000,
|
||||||
reload=True,
|
log_level="info",
|
||||||
workers=1
|
reload=True
|
||||||
)
|
)
|
Loading…
x
Reference in New Issue
Block a user