From 16eb9cf6dbc422961200d2f06bbf7ccf2f641515 Mon Sep 17 00:00:00 2001 From: 3 Date: Wed, 30 Jul 2025 00:31:08 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=8A=A0=E4=BA=86=E5=87=A0=E4=B8=AAap?= =?UTF-8?q?i=E7=AB=AF=E7=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/app/api/endpoints.py | 71 +++++- src/backend/app/models/__init__.py | 19 +- src/backend/app/services/config_validator.py | 85 +++++++ src/backend/app/services/failover_manager.py | 129 ++++++++++ src/backend/app/services/network_optimizer.py | 222 ++++++++++++++++-- .../app/services/network_visualizer.py | 104 ++++++++ src/backend/app/services/report_generator.py | 120 ++++++++++ .../app/services/switch_traffic_monitor.py | 2 +- src/backend/app/services/test.py | 9 + src/backend/requirements.txt | 7 +- 10 files changed, 744 insertions(+), 24 deletions(-) create mode 100644 src/backend/app/services/config_validator.py create mode 100644 src/backend/app/services/failover_manager.py create mode 100644 src/backend/app/services/network_visualizer.py create mode 100644 src/backend/app/services/report_generator.py create mode 100644 src/backend/app/services/test.py diff --git a/src/backend/app/api/endpoints.py b/src/backend/app/api/endpoints.py index 8bbda72..39dd07c 100644 --- a/src/backend/app/api/endpoints.py +++ b/src/backend/app/api/endpoints.py @@ -20,6 +20,10 @@ from ..services.network_scanner import NetworkScanner from ...app.services.traffic_monitor import traffic_monitor from ...app.models.traffic_models import TrafficRecord, SwitchTrafficRecord from src.backend.app.api.database import SessionLocal +from ..services.network_visualizer import NetworkVisualizer +from ..services.config_validator import ConfigValidator +from ..services.report_generator import ReportGenerator +from fastapi.responses import JSONResponse @@ -461,4 +465,69 @@ async def get_network_adapters(): return {"networks": networks} except Exception as e: - return {"error": f"获取网络适配器信息失败: {str(e)}"} \ No newline at end of file + return {"error": f"获取网络适配器信息失败: {str(e)}"} + + +visualizer = NetworkVisualizer() +report_gen = ReportGenerator() + +@router.get("/topology/visualize", response_class=HTMLResponse) +async def visualize_topology(): + """获取网络拓扑可视化图""" + try: + devices = await list_devices() # 复用现有的设备列表接口 + visualizer.update_topology(devices["devices"]) + image_data = visualizer.generate_topology_image() + + return f""" + + Network Topology + +

Network Topology

+ Network Topology + + + """ + except Exception as e: + raise HTTPException(500, detail=str(e)) + + +@router.post("/config/validate") +async def validate_config(config: dict): + """验证配置有效性""" + is_valid, errors = ConfigValidator.validate_full_config(config) + return { + "valid": is_valid, + "errors": errors, + "has_security_risks": len(ConfigValidator.check_security_risks(config.get("commands", []))) > 0 + } + + +@router.get("/reports/traffic/{ip}") +async def get_traffic_report(ip: str, days: int = 1): + """获取流量分析报告""" + try: + report = report_gen.generate_traffic_report(ip, days) + return JSONResponse(content=report) + except Exception as e: + raise HTTPException(500, detail=str(e)) + + +@router.get("/reports/traffic") +async def get_local_traffic_report(days: int = 1): + """获取本地网络流量报告""" + try: + report = report_gen.generate_traffic_report(days=days) + return JSONResponse(content=report) + except Exception as e: + raise HTTPException(500, detail=str(e)) + + +@router.get("/topology/traffic_heatmap") +async def get_traffic_heatmap(minutes: int = 10): + """获取流量热力图数据""" + try: + heatmap = visualizer.get_traffic_heatmap(minutes) + return {"heatmap": heatmap} + except Exception as e: + raise HTTPException(500, detail=str(e)) \ No newline at end of file diff --git a/src/backend/app/models/__init__.py b/src/backend/app/models/__init__.py index 689d764..141a465 100644 --- a/src/backend/app/models/__init__.py +++ b/src/backend/app/models/__init__.py @@ -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"] diff --git a/src/backend/app/services/config_validator.py b/src/backend/app/services/config_validator.py new file mode 100644 index 0000000..5be5839 --- /dev/null +++ b/src/backend/app/services/config_validator.py @@ -0,0 +1,85 @@ +import re +from typing import Dict, List, Tuple +from ..utils.exceptions import SwitchConfigException + + +class ConfigValidator: + @staticmethod + def validate_vlan_config(config: Dict) -> Tuple[bool, str]: + """验证VLAN配置""" + if 'vlan_id' not in config: + return False, "Missing VLAN ID" + + vlan_id = config['vlan_id'] + if not (1 <= vlan_id <= 4094): + return False, f"Invalid VLAN ID {vlan_id}. Must be 1-4094" + + if 'name' in config and len(config['name']) > 32: + return False, "VLAN name too long (max 32 chars)" + + return True, "Valid VLAN config" + + @staticmethod + def validate_interface_config(config: Dict) -> Tuple[bool, str]: + """验证接口配置""" + required_fields = ['interface', 'ip_address'] + for field in required_fields: + if field not in config: + return False, f"Missing required field: {field}" + + # 验证IP地址格式 + ip_pattern = r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/\d{1,2}$' + if not re.match(ip_pattern, config['ip_address']): + return False, "Invalid IP address format" + + # 验证接口名称格式 + interface_pattern = r'^(GigabitEthernet|FastEthernet|Eth)\d+/\d+/\d+$' + if not re.match(interface_pattern, config['interface']): + return False, "Invalid interface name format" + + return True, "Valid interface config" + + @staticmethod + def check_security_risks(commands: List[str]) -> List[str]: + """检查潜在安全风险""" + risky_commands = [] + dangerous_patterns = [ + r'no\s+aaa', # 禁用认证 + r'enable\s+password', # 明文密码 + r'service\s+password-encryption', # 弱加密 + r'ip\s+http\s+server', # 启用HTTP服务 + r'no\s+ip\s+http\s+secure-server' # 禁用HTTPS + ] + + for cmd in commands: + for pattern in dangerous_patterns: + if re.search(pattern, cmd, re.IGNORECASE): + risky_commands.append(cmd) + break + + return risky_commands + + @staticmethod + def validate_full_config(config: Dict) -> Tuple[bool, List[str]]: + """全面验证配置""" + errors = [] + + if 'type' not in config: + errors.append("Missing configuration type") + return False, errors + + if config['type'] == 'vlan': + valid, msg = ConfigValidator.validate_vlan_config(config) + if not valid: + errors.append(msg) + elif config['type'] == 'interface': + valid, msg = ConfigValidator.validate_interface_config(config) + if not valid: + errors.append(msg) + + if 'commands' in config: + risks = ConfigValidator.check_security_risks(config['commands']) + if risks: + errors.append(f"Potential security risks detected: {', '.join(risks)}") + + return len(errors) == 0, errors \ No newline at end of file diff --git a/src/backend/app/services/failover_manager.py b/src/backend/app/services/failover_manager.py new file mode 100644 index 0000000..84c31d9 --- /dev/null +++ b/src/backend/app/services/failover_manager.py @@ -0,0 +1,129 @@ +import asyncio +from datetime import datetime, timedelta +from pathlib import Path +from typing import Dict, List + +import networkx as nx + +from ..models.traffic_models import SwitchTrafficRecord +from src.backend.app.api.database import SessionLocal +from ..utils.exceptions import SwitchConfigException +from ..services.network_scanner import NetworkScanner + + +class FailoverManager: + def __init__(self, config_backup_dir: str = "config_backups"): + self.scanner = NetworkScanner() + self.backup_dir = Path(config_backup_dir) + self.backup_dir.mkdir(exist_ok=True) + + async def detect_failures(self, threshold: float = 0.9) -> List[Dict]: + """检测可能的网络故障""" + with SessionLocal() as session: + # 获取最近5分钟的所有交换机流量记录 + records = session.query(SwitchTrafficRecord).filter( + SwitchTrafficRecord.timestamp >= datetime.now() - timedelta(minutes=5) + ).all() + + # 分析流量异常 + abnormal_devices = [] + device_stats = {} + + for record in records: + if record.switch_ip not in device_stats: + device_stats[record.switch_ip] = { + 'interfaces': {}, + 'max_in': 0, + 'max_out': 0 + } + + if record.interface not in device_stats[record.switch_ip]['interfaces']: + device_stats[record.switch_ip]['interfaces'][record.interface] = { + 'in': [], + 'out': [] + } + + device_stats[record.switch_ip]['interfaces'][record.interface]['in'].append(record.rate_in) + device_stats[record.switch_ip]['interfaces'][record.interface]['out'].append(record.rate_out) + + # 更新设备最大流量 + if record.rate_in > device_stats[record.switch_ip]['max_in']: + device_stats[record.switch_ip]['max_in'] = record.rate_in + if record.rate_out > device_stats[record.switch_ip]['max_out']: + device_stats[record.switch_ip]['max_out'] = record.rate_out + + # 检测异常 + for ip, stats in device_stats.items(): + for iface, traffic in stats['interfaces'].items(): + avg_in = sum(traffic['in']) / len(traffic['in']) + avg_out = sum(traffic['out']) / len(traffic['out']) + + # 如果平均流量超过最大流量的90%,认为可能有问题 + if avg_in > stats['max_in'] * threshold or \ + avg_out > stats['max_out'] * threshold: + abnormal_devices.append({ + 'ip': ip, + 'interface': iface, + 'avg_in': avg_in, + 'avg_out': avg_out, + 'max_in': stats['max_in'], + 'max_out': stats['max_out'] + }) + + return abnormal_devices + + async def automatic_recovery(self, failed_device_ip: str) -> bool: + """自动故障恢复""" + try: + # 1. 检查设备是否在线 + devices = self.scanner.scan_subnet(failed_device_ip + "/32") + if not devices: + raise SwitchConfigException(f"Device {failed_device_ip} is offline") + + # 2. 查找最近的配置备份 + backup_files = sorted(self.backup_dir.glob(f"{failed_device_ip}_*.cfg")) + if not backup_files: + raise SwitchConfigException(f"No backup found for {failed_device_ip}") + + latest_backup = backup_files[-1] + + # 3. 恢复配置 + with open(latest_backup) as f: + config_commands = f.read().splitlines() + + # 使用SSH执行恢复命令 + # (这里需要实现SSH连接和执行命令的逻辑) + + return True + + except Exception as e: + raise SwitchConfigException(f"Recovery failed: {str(e)}") + + async def redundancy_check(self, critical_devices: List[str]) -> Dict: + """检查关键设备的冗余配置""" + results = {} + topology = self.scanner.get_current_topology() + + for device_ip in critical_devices: + # 检查是否有备用路径 + try: + paths = list(nx.all_shortest_paths( + topology, source=device_ip, target="core_switch")) + + if len(paths) > 1: + results[device_ip] = { + 'status': 'redundant', + 'path_count': len(paths) + } + else: + results[device_ip] = { + 'status': 'single_point_of_failure', + 'recommendation': 'Add redundant links' + } + except: + results[device_ip] = { + 'status': 'disconnected', + 'recommendation': 'Check physical connection' + } + + return results \ No newline at end of file diff --git a/src/backend/app/services/network_optimizer.py b/src/backend/app/services/network_optimizer.py index e58bd5c..39c14f7 100644 --- a/src/backend/app/services/network_optimizer.py +++ b/src/backend/app/services/network_optimizer.py @@ -1,34 +1,220 @@ - import networkx as nx -from scipy.optimize import minimize +import numpy as np +from scipy.optimize import linprog +from typing import Dict, List, Optional, Tuple, Any +from dataclasses import dataclass + + +@dataclass +class FlowDemand: + source: str + destination: str + bandwidth: float + priority: float = 1.0 class NetworkOptimizer: - def __init__(self, devices): + def __init__(self, devices: List[Dict[str, Any]]): """基于图论的网络优化模型""" self.graph = self.build_topology_graph(devices) + self.traffic_matrix: Optional[np.ndarray] = None + self._initialize_capacities() - def build_topology_graph(self, devices): - """构建网络拓扑图""" + def _initialize_capacities(self) -> Dict[Tuple[str, str], float]: + """初始化链路容量字典""" + self.remaining_capacity = { + (u, v): data['bandwidth'] + for u, v, data in self.graph.edges(data=True) + } + return self.remaining_capacity + + def build_topology_graph(self, devices: List[Dict[str, Any]]) -> nx.Graph: + """构建带权重的网络拓扑图""" 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) + G.add_node(device['ip'], + type=device['type'], + capacity=device.get('capacity', 1000)) + + # 添加接口作为子节点 + for interface in device.get('interfaces', []): + interface_id = f"{device['ip']}_{interface['name']}" + G.add_node(interface_id, + type='interface', + capacity=interface.get('bandwidth', 1000)) + G.add_edge(device['ip'], interface_id, + bandwidth=interface.get('bandwidth', 1000), + latency=interface.get('latency', 1)) + + # 添加设备间连接 + self._connect_devices(G, devices) return G - def optimize_path(self, source, target): - """计算最优路径""" - return nx.shortest_path(self.graph, source, target) + @staticmethod + def _connect_devices(graph: nx.Graph, devices: List[Dict[str, Any]]): + """自动连接设备""" + for i in range(len(devices) - 1): + graph.add_edge( + devices[i]['ip'], + devices[i + 1]['ip'], + bandwidth=1000, + latency=5 + ) - def bandwidth_optimization(self): - """带宽优化模型""" + @staticmethod + def _build_flow_conservation(nodes: List[str]) -> Tuple[np.ndarray, np.ndarray]: + """构建流量守恒约束矩阵""" + num_nodes = len(nodes) + A_eq: List[np.ndarray] = [] # 明确指定列表元素类型 + b_eq: List[float] = [] # 明确指定float列表 - def objective(x): - return max(x) + for i in range(num_nodes): + for j in range(num_nodes): + if i != j: + constraint = np.zeros((num_nodes, num_nodes)) + constraint[i][j] = 1 + constraint[j][i] = -1 + A_eq.append(constraint.flatten()) + b_eq.append(0.0) # 明确使用float类型 - constraints = ( - {'type': 'eq', 'fun': lambda x: sum(x) - total_bandwidth} + return np.array(A_eq, dtype=np.float64), np.array(b_eq, dtype=np.float64) + def optimize_bandwidth_allocation( + self, + demands: List[FlowDemand] + ) -> Dict[str, Dict[str, float]]: + """ + 基于线性规划的带宽分配优化 + 返回: {源IP: {目标IP: 分配带宽}} + """ + demand_dict = {(d.source, d.destination): float(d.bandwidth) for d in demands} # 确保float类型 + return self._optimize_with_highs(demand_dict) + + def _optimize_with_highs(self, demands: Dict[Tuple[str, str], float]) -> Dict[str, Dict[str, float]]: + """使用HiGHS求解器实现""" + nodes = list(self.graph.nodes()) + node_index = {node: i for i, node in enumerate(nodes)} + + # 构建流量矩阵 + self.traffic_matrix = np.zeros((len(nodes), len(nodes))) + for (src, dst), bw in demands.items(): + if src in node_index and dst in node_index: + self.traffic_matrix[node_index[src]][node_index[dst]] = bw + + # 构建约束 + c = np.ones(len(nodes) ** 2) # 最小化总流量 + A_ub, b_ub = self._build_capacity_constraints(nodes, node_index) + A_eq, b_eq = self._build_flow_conservation(nodes) + + # 求解线性规划 + res = linprog( + c=c, + A_ub=A_ub, + b_ub=b_ub, + A_eq=A_eq, + b_eq=b_eq, + bounds=(0, None), + method='highs' ) - result = minimize(objective, initial_guess, constraints=constraints) - return result.x \ No newline at end of file + if not res.success: + raise ValueError(f"Optimization failed: {res.message}") + + return self._format_results(res.x, nodes, node_index) + + def _build_capacity_constraints(self, nodes: List[str], node_index: Dict[str, int]) -> Tuple[ + np.ndarray, np.ndarray]: + """构建容量约束矩阵""" + A_ub = [] + b_ub = [] + + for u, v, data in self.graph.edges(data=True): + capacity = float(data.get('bandwidth', 1000)) # 确保float类型 + b_ub.append(capacity) + + constraint = np.zeros((len(nodes), len(nodes))) + for src, dst in [(u, v), (v, u)]: + if src in node_index and dst in node_index: + i, j = node_index[src], node_index[dst] + constraint[i][j] += 1 + A_ub.append(constraint.flatten()) + + return np.array(A_ub), np.array(b_ub) + + @staticmethod + def _format_results(solution: np.ndarray, nodes: List[str], node_index: Dict[str, int]) -> Dict[ + str, Dict[str, float]]: + """格式化优化结果""" + flows = solution.reshape((len(nodes), len(nodes))) + return { + nodes[i]: { + nodes[j]: float(flows[i][j]) # 明确转换为float + for j in range(len(nodes)) + if flows[i][j] > 0.001 + } + for i in range(len(nodes)) + } + + def optimize_qos(self, flows: List[FlowDemand]) -> Dict[str, Dict[str, Any]]: + """ + 带QoS的流量优化 + 返回: { + "源-目标": { + "path": 路径列表, + "allocated": 分配带宽, + "priority": 优先级 + } + } + """ + sorted_flows = sorted(flows, key=lambda x: x.priority, reverse=True) + results = {} + + # 使用实例变量而非局部变量 + self._initialize_capacities() # 重置剩余带宽 + + for flow in sorted_flows: + path = self.find_optimal_path(flow.source, flow.destination, flow.bandwidth) + if not path: + continue + + min_bw = min(self.graph[u][v]['bandwidth'] for u, v in zip(path[:-1], path[1:])) + allocated = min(min_bw, flow.bandwidth) + + # 更新剩余带宽 + for u, v in zip(path[:-1], path[1:]): + self.remaining_capacity[(u, v)] -= allocated + if self.remaining_capacity[(u, v)] <= 0: + self.graph[u][v]['bandwidth'] = 0 + + results[f"{flow.source}-{flow.destination}"] = { + "path": path, + "allocated": float(allocated), # 确保float类型 + "priority": float(flow.priority) + } + + return results + + def find_optimal_path(self, source: str, target: str, + bandwidth: float = 1.0) -> Optional[List[str]]: + """ + 改进的最优路径查找 + """ + try: + paths = nx.shortest_simple_paths( + self.graph, source, target, + weight=lambda u, v, d: 1 / max(1, d['bandwidth']) + d['latency'] # 避免除零 + ) + return next( + (path for path in paths + if self._path_has_sufficient_bandwidth(path, bandwidth)), + None + ) + except (nx.NetworkXNoPath, nx.NodeNotFound): + return None + + def _path_has_sufficient_bandwidth(self, path: List[str], bw: float) -> bool: + """检查路径带宽是否满足要求""" + return all( + self.graph[u][v]['bandwidth'] >= bw + for u, v in zip(path[:-1], path[1:]) + ) \ No newline at end of file diff --git a/src/backend/app/services/network_visualizer.py b/src/backend/app/services/network_visualizer.py new file mode 100644 index 0000000..f9c28fd --- /dev/null +++ b/src/backend/app/services/network_visualizer.py @@ -0,0 +1,104 @@ +from datetime import datetime, timedelta + +import networkx as nx +import matplotlib.pyplot as plt +from io import BytesIO +import base64 +from typing import Dict, List +from ..models.traffic_models import SwitchTrafficRecord +from src.backend.app.api.database import SessionLocal + + +class NetworkVisualizer: + def __init__(self): + self.graph = nx.Graph() + + def update_topology(self, devices: List[Dict]): + """更新网络拓扑图""" + self.graph.clear() + + # 添加节点 + for device in devices: + self.graph.add_node( + device['ip'], + type='switch', + label=f"Switch\n{device['ip']}" + ) + + # 添加连接(简化版,实际应根据扫描结果) + if len(devices) > 1: + for i in range(len(devices) - 1): + self.graph.add_edge( + devices[i]['ip'], + devices[i + 1]['ip'], + bandwidth=1000, + label="1Gbps" + ) + + def generate_topology_image(self) -> str: + """生成拓扑图并返回base64编码""" + plt.figure(figsize=(10, 8)) + pos = nx.spring_layout(self.graph) + + # 绘制节点 + node_colors = [] + for node in self.graph.nodes(): + if self.graph.nodes[node]['type'] == 'switch': + node_colors.append('lightblue') + + nx.draw_networkx_nodes( + self.graph, pos, + node_size=2000, + node_color=node_colors + ) + + # 绘制边 + nx.draw_networkx_edges( + self.graph, pos, + width=2, + alpha=0.5 + ) + + # 绘制标签 + node_labels = nx.get_node_attributes(self.graph, 'label') + nx.draw_networkx_labels( + self.graph, pos, + labels=node_labels, + font_size=8 + ) + + edge_labels = nx.get_edge_attributes(self.graph, 'label') + nx.draw_networkx_edge_labels( + self.graph, pos, + edge_labels=edge_labels, + font_size=8 + ) + + plt.title("Network Topology") + plt.axis('off') + + # 转换为base64 + buf = BytesIO() + plt.savefig(buf, format='png', bbox_inches='tight') + plt.close() + buf.seek(0) + return base64.b64encode(buf.read()).decode('utf-8') + + @staticmethod + def get_traffic_heatmap(minutes: int = 10) -> Dict: + """获取流量热力图数据""" + with SessionLocal() as session: + records = session.query(SwitchTrafficRecord).filter( + SwitchTrafficRecord.timestamp >= datetime.now() - timedelta(minutes=minutes) + ).all() + + heatmap_data = {} + for record in records: + if record.switch_ip not in heatmap_data: + heatmap_data[record.switch_ip] = {} + heatmap_data[record.switch_ip][record.interface] = { + 'in': record.rate_in, + 'out': record.rate_out + } + + return heatmap_data \ No newline at end of file diff --git a/src/backend/app/services/report_generator.py b/src/backend/app/services/report_generator.py new file mode 100644 index 0000000..d6742f4 --- /dev/null +++ b/src/backend/app/services/report_generator.py @@ -0,0 +1,120 @@ +import pandas as pd +import matplotlib.pyplot as plt +from io import BytesIO +import base64 +from datetime import datetime, timedelta +from typing import Dict, List +from ..models.traffic_models import TrafficRecord, SwitchTrafficRecord +from src.backend.app.api.database import SessionLocal + + +class ReportGenerator: + @staticmethod + def generate_traffic_report(ip: str = None, days: int = 7) -> Dict: + """生成流量分析报告""" + with SessionLocal() as session: + time_threshold = datetime.now() - timedelta(days=days) + + if ip: + # 交换机流量报告 + query = session.query(SwitchTrafficRecord).filter( + SwitchTrafficRecord.switch_ip == ip, + SwitchTrafficRecord.timestamp >= time_threshold + ) + df = pd.read_sql(query.statement, session.bind) + + if df.empty: + return {"error": "No data found"} + + # 按接口分组分析 + interface_stats = df.groupby('interface').agg({ + 'rate_in': ['mean', 'max', 'min'], + 'rate_out': ['mean', 'max', 'min'] + }).reset_index() + + # 生成趋势图 + trend_fig = ReportGenerator._plot_traffic_trend(df, f"Switch {ip} Traffic Trend") + + return { + "switch_ip": ip, + "period": f"Last {days} days", + "interface_stats": interface_stats.to_dict('records'), + "trend_chart": trend_fig + } + else: + # 本地网络流量报告 + query = session.query(TrafficRecord).filter( + TrafficRecord.timestamp >= time_threshold + ) + df = pd.read_sql(query.statement, session.bind) + + if df.empty: + return {"error": "No data found"} + + # 按接口分组分析 + interface_stats = df.groupby('interface').agg({ + 'bytes_sent': ['sum', 'mean', 'max'], + 'bytes_recv': ['sum', 'mean', 'max'] + }).reset_index() + + # 生成趋势图 + trend_fig = ReportGenerator._plot_traffic_trend(df, "Local Network Traffic Trend") + + return { + "report_type": "local_network", + "period": f"Last {days} days", + "interface_stats": interface_stats.to_dict('records'), + "trend_chart": trend_fig + } + + @staticmethod + def _plot_traffic_trend(df: pd.DataFrame, title: str) -> str: + """生成流量趋势图""" + plt.figure(figsize=(12, 6)) + + if 'rate_in' in df.columns: # 交换机数据 + df_grouped = df.groupby(pd.Grouper(key='timestamp', freq='1H')).agg({ + 'rate_in': 'mean', + 'rate_out': 'mean' + }) + plt.plot(df_grouped.index, df_grouped['rate_in'], label='Inbound Traffic') + plt.plot(df_grouped.index, df_grouped['rate_out'], label='Outbound Traffic') + plt.ylabel("Traffic Rate (bytes/sec)") + else: # 本地网络数据 + df_grouped = df.groupby(pd.Grouper(key='timestamp', freq='1H')).agg({ + 'bytes_sent': 'sum', + 'bytes_recv': 'sum' + }) + plt.plot(df_grouped.index, df_grouped['bytes_sent'], label='Bytes Sent') + plt.plot(df_grouped.index, df_grouped['bytes_recv'], label='Bytes Received') + plt.ylabel("Bytes") + + plt.title(title) + plt.xlabel("Time") + plt.legend() + plt.grid(True) + + # 转换为base64 + buf = BytesIO() + plt.savefig(buf, format='png', bbox_inches='tight') + plt.close() + buf.seek(0) + return base64.b64encode(buf.read()).decode('utf-8') + + @staticmethod + def generate_config_history_report(days: int = 30) -> Dict: + """生成配置变更历史报告""" + # 需要实现配置历史记录功能 + pass + + @staticmethod + def generate_security_report() -> Dict: + """生成安全评估报告""" + # 需要实现安全扫描功能 + pass + + @staticmethod + def generate_performance_report() -> Dict: + """生成性能评估报告""" + # 需要实现性能基准测试功能 + pass \ No newline at end of file diff --git a/src/backend/app/services/switch_traffic_monitor.py b/src/backend/app/services/switch_traffic_monitor.py index 80a45fd..7c20f3d 100644 --- a/src/backend/app/services/switch_traffic_monitor.py +++ b/src/backend/app/services/switch_traffic_monitor.py @@ -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, diff --git a/src/backend/app/services/test.py b/src/backend/app/services/test.py new file mode 100644 index 0000000..29db72e --- /dev/null +++ b/src/backend/app/services/test.py @@ -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) \ No newline at end of file diff --git a/src/backend/requirements.txt b/src/backend/requirements.txt index 37910c5..3fbede2 100644 --- a/src/backend/requirements.txt +++ b/src/backend/requirements.txt @@ -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