Compare commits

..

No commits in common. "c74d55e62bbe7c94326ff42921276ce065752543" and "da5743e27a8e934d42ea9adbb9a3fd7ff7efac05" have entirely different histories.

6 changed files with 49 additions and 137 deletions

View File

@ -1,4 +1,3 @@
import socket
from datetime import datetime, timedelta from datetime import datetime, timedelta
from fastapi import (APIRouter, HTTPException, Response, WebSocket, WebSocketDisconnect) from fastapi import (APIRouter, HTTPException, Response, WebSocket, WebSocketDisconnect)
from typing import List from typing import List
@ -8,8 +7,6 @@ from fastapi.responses import HTMLResponse
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import io import io
import base64 import base64
import psutil
import ipaddress
from ..services.switch_traffic_monitor import get_switch_monitor from ..services.switch_traffic_monitor import get_switch_monitor
from ..utils import logger from ..utils import logger
@ -274,7 +271,7 @@ async def get_switch_current_traffic(switch_ip: str, interface: str = None):
try: try:
monitor = get_switch_monitor(switch_ip) monitor = get_switch_monitor(switch_ip)
# 如果没有指定接口,获取所有接口的流量
if not interface: if not interface:
traffic_data = {} traffic_data = {}
for iface in monitor.interface_oids: for iface in monitor.interface_oids:
@ -284,6 +281,7 @@ async def get_switch_current_traffic(switch_ip: str, interface: str = None):
"traffic": traffic_data "traffic": traffic_data
} }
# 获取指定接口的流量
return await get_interface_current_traffic(switch_ip, interface) return await get_interface_current_traffic(switch_ip, interface)
except Exception as e: except Exception as e:
logger.error(f"获取交换机流量失败: {str(e)}") logger.error(f"获取交换机流量失败: {str(e)}")
@ -294,7 +292,7 @@ async def get_interface_current_traffic(switch_ip: str, interface: str) -> dict:
"""获取指定交换机接口的当前流量数据""" """获取指定交换机接口的当前流量数据"""
try: try:
with SessionLocal() as session: with SessionLocal() as session:
# 获取最新记录
record = session.query(SwitchTrafficRecord).filter( record = session.query(SwitchTrafficRecord).filter(
SwitchTrafficRecord.switch_ip == switch_ip, SwitchTrafficRecord.switch_ip == switch_ip,
SwitchTrafficRecord.interface == interface SwitchTrafficRecord.interface == interface
@ -329,21 +327,26 @@ async def get_switch_traffic_history(switch_ip: str, interface: str = None, minu
try: try:
monitor = get_switch_monitor(switch_ip) monitor = get_switch_monitor(switch_ip)
# 如果没有指定接口,返回所有接口的历史数据
if not interface: if not interface:
return { return {
"switch_ip": switch_ip, "switch_ip": switch_ip,
"history": monitor.get_traffic_history() "history": monitor.get_traffic_history()
} }
# 获取指定接口的历史数据
with SessionLocal() as session: with SessionLocal() as session:
# 计算时间范围
time_threshold = datetime.now() - timedelta(minutes=minutes) time_threshold = datetime.now() - timedelta(minutes=minutes)
# 查询历史记录
records = session.query(SwitchTrafficRecord).filter( records = session.query(SwitchTrafficRecord).filter(
SwitchTrafficRecord.switch_ip == switch_ip, SwitchTrafficRecord.switch_ip == switch_ip,
SwitchTrafficRecord.interface == interface, SwitchTrafficRecord.interface == interface,
SwitchTrafficRecord.timestamp >= time_threshold SwitchTrafficRecord.timestamp >= time_threshold
).order_by(SwitchTrafficRecord.timestamp.asc()).all() ).order_by(SwitchTrafficRecord.timestamp.asc()).all()
# 提取数据
history_data = { history_data = {
"in": [record.rate_in for record in records], "in": [record.rate_in for record in records],
"out": [record.rate_out for record in records], "out": [record.rate_out for record in records],
@ -368,10 +371,13 @@ async def websocket_switch_traffic(websocket: WebSocket, switch_ip: str, interfa
monitor = get_switch_monitor(switch_ip) monitor = get_switch_monitor(switch_ip)
while True: while True:
# 获取流量数据
if interface: if interface:
# 单个接口
traffic_data = await get_interface_current_traffic(switch_ip, interface) traffic_data = await get_interface_current_traffic(switch_ip, interface)
await websocket.send_json(traffic_data) await websocket.send_json(traffic_data)
else: else:
# 所有接口
traffic_data = {} traffic_data = {}
for iface in monitor.interface_oids: for iface in monitor.interface_oids:
traffic_data[iface] = await get_interface_current_traffic(switch_ip, iface) traffic_data[iface] = await get_interface_current_traffic(switch_ip, iface)
@ -381,6 +387,7 @@ async def websocket_switch_traffic(websocket: WebSocket, switch_ip: str, interfa
"traffic": traffic_data "traffic": traffic_data
}) })
# 每秒更新一次
await asyncio.sleep(1) await asyncio.sleep(1)
except WebSocketDisconnect: except WebSocketDisconnect:
logger.info(f"客户端断开交换机流量连接: {switch_ip}") logger.info(f"客户端断开交换机流量连接: {switch_ip}")
@ -388,17 +395,22 @@ async def websocket_switch_traffic(websocket: WebSocket, switch_ip: str, interfa
logger.error(f"交换机流量WebSocket错误: {str(e)}") logger.error(f"交换机流量WebSocket错误: {str(e)}")
await websocket.close(code=1011, reason=str(e)) await websocket.close(code=1011, reason=str(e))
# 交换机流量可视化
@router.get("/traffic/switch/plot", response_class=HTMLResponse, summary="交换机流量可视化") @router.get("/traffic/switch/plot", response_class=HTMLResponse, summary="交换机流量可视化")
async def plot_switch_traffic(switch_ip: str, interface: str, minutes: int = 10): async def plot_switch_traffic(switch_ip: str, interface: str, minutes: int = 10):
"""生成交换机流量图表""" """生成交换机流量图表"""
try: try:
# 获取历史数据
history = await get_switch_traffic_history(switch_ip, interface, minutes) history = await get_switch_traffic_history(switch_ip, interface, minutes)
history_data = history["history"] history_data = history["history"]
# 提取数据点
time_points = [datetime.fromisoformat(t) for t in history_data["time"]] time_points = [datetime.fromisoformat(t) for t in history_data["time"]]
in_rates = history_data["in"] in_rates = history_data["in"]
out_rates = history_data["out"] out_rates = history_data["out"]
# 创建图表
plt.figure(figsize=(12, 6)) plt.figure(figsize=(12, 6))
plt.plot(time_points, in_rates, label="流入流量 (B/s)") plt.plot(time_points, in_rates, label="流入流量 (B/s)")
plt.plot(time_points, out_rates, label="流出流量 (B/s)") plt.plot(time_points, out_rates, label="流出流量 (B/s)")
@ -409,6 +421,8 @@ async def plot_switch_traffic(switch_ip: str, interface: str, minutes: int = 10)
plt.grid(True) plt.grid(True)
plt.xticks(rotation=45) plt.xticks(rotation=45)
plt.tight_layout() plt.tight_layout()
# 转换为HTML图像
buf = io.BytesIO() buf = io.BytesIO()
plt.savefig(buf, format="png") plt.savefig(buf, format="png")
buf.seek(0) buf.seek(0)
@ -436,29 +450,3 @@ async def plot_switch_traffic(switch_ip: str, interface: str, minutes: int = 10)
except Exception as e: except Exception as e:
logger.error(f"生成流量图表失败: {str(e)}") logger.error(f"生成流量图表失败: {str(e)}")
return HTMLResponse(content=f"<h1>错误</h1><p>{str(e)}</p>", status_code=500) return HTMLResponse(content=f"<h1>错误</h1><p>{str(e)}</p>", status_code=500)
@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
})
return {"networks": networks}
except Exception as e:
return {"error": f"获取网络适配器信息失败: {str(e)}"}

View File

@ -30,9 +30,9 @@ class AIService:
2. 必须包含'commands'字段包含可直接执行的命令列表 2. 必须包含'commands'字段包含可直接执行的命令列表
3. 其他参数根据配置类型动态添加 3. 其他参数根据配置类型动态添加
4. 不要包含解释性文本步骤说明或注释 4. 不要包含解释性文本步骤说明或注释
5.要包含使用ssh连接交换机后的完整命令包括但不完全包括system-view退出保存等完整操作注意保存还需要输入Y和回车 5.要包含使用ssh连接交换机后的完整命令包括但不完全包括system-view退出保存等完整操作
示例命令'创建VLAN 100名称为TEST' 示例命令'创建VLAN 100名称为TEST'
示例返回{"type": "vlan", "vlan_id": 100, "name": "TEST", "commands": ["system-view\n","vlan 100\n", "name TEST\n","quit\n","\x1A\n","save\n","Y\n"]} 示例返回{"type": "vlan", "vlan_id": 100, "name": "TEST", "commands": ["vlan 100", "name TEST"]}
注意这里生成的commands中需包含登录交换机和保存等所有操作命令我们使ssh连接交换机你不需要给出连接ssh的命令你只需要给出使用ssh连接到交换机后所输入的全部命令 注意这里生成的commands中需包含登录交换机和保存等所有操作命令我们使ssh连接交换机你不需要给出连接ssh的命令你只需要给出使用ssh连接到交换机后所输入的全部命令
""" """

View File

@ -12,6 +12,7 @@ pysnmp==7.1.21
aiofiles==23.2.1 aiofiles==23.2.1
loguru==0.7.2 loguru==0.7.2
tenacity==8.2.3 tenacity==8.2.3
asyncio==3.4.3 asyncio==3.4.3

View File

@ -8,7 +8,7 @@ import { Box } from '@chakra-ui/react';
*/ */
const PageContainer = ({ children }) => { const PageContainer = ({ children }) => {
return ( return (
<Box pt={'85px'} px={6}> <Box pt={'80px'} px={6}>
{children} {children}
</Box> </Box>
); );

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react'; import React, { useEffect, useState } from 'react';
import { import {
Box, Box,
VStack, VStack,
@ -7,13 +7,9 @@ import {
Button, Button,
Text, Text,
Spinner, Spinner,
Table,
Badge, Badge,
HStack, HStack,
Select, Table,
Field,
Portal,
createListCollection,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import DocumentTitle from '@/components/system/pages/DocumentTitle'; import DocumentTitle from '@/components/system/pages/DocumentTitle';
import PageContainer from '@/components/system/PageContainer'; import PageContainer from '@/components/system/PageContainer';
@ -32,29 +28,22 @@ import scanEffect from '@/libs/script/scanPage/scanEffect';
*/ */
const ScanPage = () => { const ScanPage = () => {
const [subnet, setSubnet] = useState(''); const [subnet, setSubnet] = useState('');
const [networkAdapters, setNetworkAdapters] = useState(createListCollection({ items: [] })); const [devices, setDevices] = useState([]);
const [scannedDevices, setScannedDevices] = useState([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [localIp, setLocalIp] = useState(''); const [localIp, setLocalIp] = useState('');
const [selectedNetwork, setSelectedNetwork] = useState('');
const [inputMode, setInputMode] = useState(false);
const config = ConfigTool.load(); const config = ConfigTool.load();
const testMode = config.testMode; const testMode = config.testMode;
useEffect(() => { useEffect(() => {
api.getNetworkAdapters().then((response) => { scanEffect
const { networks } = response.data; .fetchLocalInfo({
const networkCollection = createListCollection({ setLocalIp: setLocalIp,
items: networks.map((network) => ({ setSubnet: setSubnet,
label: `${network.adapter} (${network.network})`, subnet: subnet,
value: network.network, })
})), .then();
}); }, [subnet, Notification]);
setNetworkAdapters(networkCollection);
setLocalIp(config.backendUrl);
});
}, []);
const handleScan = async () => { const handleScan = async () => {
setLoading(true); setLoading(true);
@ -68,18 +57,17 @@ const ScanPage = () => {
}); });
} else { } else {
const res = await api.scan(subnet); const res = await api.scan(subnet);
console.log(`res:${JSON.stringify(res)}`); scanDevices = res.devices || [];
scanDevices = res.data.devices || [];
Notification.success({ Notification.success({
title: '扫描子网设备成功!', title: '扫描子网设备成功!',
}); });
} }
console.log(`device:${JSON.stringify(scanDevices)}`);
scanDevices = scanDevices.map((d, idx) => ({ scanDevices = scanDevices.map((d, idx) => ({
...d, ...d,
name: `交换机 ${idx + 1}`, name: `交换机 ${idx + 1}`,
})); }));
setScannedDevices(scanDevices); setDevices(scanDevices);
const updatedStats = { const updatedStats = {
totalDevices: scanDevices.length, totalDevices: scanDevices.length,
onlineDevices: scanDevices.length, onlineDevices: scanDevices.length,
@ -109,20 +97,15 @@ const ScanPage = () => {
}); });
} else { } else {
const res = await api.listDevices(); const res = await api.listDevices();
scanDevices = res.data.devices || []; scanDevices = res.devices || [];
} }
setScannedDevices(scanDevices); setDevices(scanDevices);
} catch (err) { } catch (err) {
Notification.error({ title: '获取上次扫描记录失败' }); Notification.error({ title: '获取上次扫描记录失败' });
} }
setLoading(false); setLoading(false);
}; };
const handleNetworkChange = ({ value }) => {
setSelectedNetwork(value[0] ?? '');
setSubnet(value[0] ?? '');
};
return ( return (
<DocumentTitle title={'网络扫描'}> <DocumentTitle title={'网络扫描'}>
<DashboardBackground /> <DashboardBackground />
@ -149,67 +132,13 @@ const ScanPage = () => {
</HStack> </HStack>
<HStack mb={4} spacing={4}> <HStack mb={4} spacing={4}>
<Field.Root> <Input
<Field.Label fontWeight={'bold'} mb={1} fontSize="sm"> placeholder={'输入子网 (如 192.168.1.0/24)'}
{'选择网段'} value={subnet}
</Field.Label> onChange={(e) => setSubnet(e.target.value)}
<Select.Root width={'300px'}
collection={networkAdapters} bg={'whiteAlpha.200'}
value={selectedNetwork ? [selectedNetwork] : []} />
onValueChange={handleNetworkChange}
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>
<Select.Content>
{networkAdapters.items.map((item, index) => (
<Select.Item key={`${item.value}-${index}`} item={item}>
{' '}
{item.label}
</Select.Item>
))}
</Select.Content>
</Select.Positioner>
</Portal>
</Select.Root>
</Field.Root>
<Button
mb={-7}
onClick={() => setInputMode(!inputMode)}
colorPalette={'blue'}
variant={'outline'}
>
{inputMode ? '关闭自定义' : '自定义网段'}
</Button>
</HStack>
{inputMode ? (
<FadeInWrapper delay={0.1} yOffset={-2}>
<Input
mb={4}
placeholder={'输入子网 (如 192.168.1.0/24)'}
value={subnet}
onChange={(e) => setSubnet(e.target.value)}
width={'300px'}
bg={'whiteAlpha.200'}
/>
</FadeInWrapper>
) : null}
<HStack mb={5} spacing={4}>
<Button <Button
onClick={handleScan} onClick={handleScan}
isLoading={loading} isLoading={loading}
@ -235,7 +164,7 @@ const ScanPage = () => {
</HStack> </HStack>
)} )}
{!loading && scannedDevices.length > 0 && ( {!loading && devices.length > 0 && (
<FadeInWrapper delay={0.2} yOffset={-5}> <FadeInWrapper delay={0.2} yOffset={-5}>
<Table.Root <Table.Root
variant={'outline'} variant={'outline'}
@ -264,7 +193,7 @@ const ScanPage = () => {
</Table.Row> </Table.Row>
</Table.Header> </Table.Header>
<Table.Body> <Table.Body>
{scannedDevices.map((d) => ( {devices.map((d) => (
<Table.Row <Table.Row
key={d.ip} key={d.ip}
_hover={{ _hover={{
@ -282,7 +211,7 @@ const ScanPage = () => {
</FadeInWrapper> </FadeInWrapper>
)} )}
{!loading && scannedDevices.length === 0 && ( {!loading && devices.length === 0 && (
<Text color={'gray.400'}>{'暂无扫描结果,请执行扫描..'}</Text> <Text color={'gray.400'}>{'暂无扫描结果,请执行扫描..'}</Text>
)} )}
</Box> </Box>

View File

@ -52,12 +52,6 @@ export const api = {
applyConfig: (switch_ip, commands) => applyConfig: (switch_ip, commands) =>
axios.post(buildUrl('/api/execute_cli_commands'), { switch_ip: switch_ip, commands: commands }), axios.post(buildUrl('/api/execute_cli_commands'), { switch_ip: switch_ip, commands: commands }),
/**
* 获取网络适配器信息
* @returns {Promise<axios.AxiosResponse<any>>}
*/
getNetworkAdapters: () => axios.get(buildUrl('/api/network_adapters')),
/** /**
* 更新基础URL * 更新基础URL
* @param url * @param url