Compare commits
10 Commits
ipam_python310
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 11d830b496 | |||
| 934b604b65 | |||
| 13c457acb3 | |||
| 05cd71ca1a | |||
| ee0552f837 | |||
| 094d2cc112 | |||
| 8b1df674ec | |||
| 6076e174d5 | |||
| 8a94f535de | |||
| ebb04f9ad8 |
@@ -437,7 +437,7 @@ def delete_network_device(
|
||||
# ========== SNMP 操作 ==========
|
||||
|
||||
@router.post("/devices/{device_id}/test", summary="测试 SNMP 连接")
|
||||
def test_snmp_connection(device_id: int, db: Session = Depends(get_db)):
|
||||
async def test_snmp_connection(device_id: int, db: Session = Depends(get_db)):
|
||||
"""测试设备的 SNMP 连接(不写审计,避免刷屏)"""
|
||||
from app.models.snmp import NetworkDevice
|
||||
|
||||
@@ -448,7 +448,7 @@ def test_snmp_connection(device_id: int, db: Session = Depends(get_db)):
|
||||
if not device.snmp_credential:
|
||||
raise HTTPException(status_code=400, detail="设备未配置 SNMP 凭据")
|
||||
|
||||
success, info = SNMPService.test_connection(device, device.snmp_credential)
|
||||
success, info = await SNMPService.test_connection(device, device.snmp_credential)
|
||||
|
||||
return {
|
||||
"device_id": device_id,
|
||||
@@ -459,9 +459,9 @@ def test_snmp_connection(device_id: int, db: Session = Depends(get_db)):
|
||||
|
||||
|
||||
@router.post("/devices/{device_id}/poll", summary="立即轮询设备")
|
||||
def poll_device_now(device_id: int, db: Session = Depends(get_db)):
|
||||
async def poll_device_now(device_id: int, db: Session = Depends(get_db)):
|
||||
"""立即轮询设备,获取 ARP 表、MAC 地址表、接口信息(不写审计,常规操作)"""
|
||||
result = SNMPService.poll_device(db, device_id)
|
||||
result = await SNMPService.poll_device(db, device_id)
|
||||
|
||||
if 'error' in result:
|
||||
raise HTTPException(status_code=400, detail=result['error'])
|
||||
@@ -470,7 +470,7 @@ def poll_device_now(device_id: int, db: Session = Depends(get_db)):
|
||||
|
||||
|
||||
@router.post("/devices/{device_id}/arp", summary="获取设备 ARP 表")
|
||||
def get_device_arp_table(device_id: int, db: Session = Depends(get_db)):
|
||||
async def get_device_arp_table(device_id: int, db: Session = Depends(get_db)):
|
||||
"""获取设备的 ARP 表"""
|
||||
from app.models.snmp import NetworkDevice, ARPEntry
|
||||
|
||||
@@ -481,7 +481,7 @@ def get_device_arp_table(device_id: int, db: Session = Depends(get_db)):
|
||||
if not device.snmp_credential:
|
||||
raise HTTPException(status_code=400, detail="设备未配置 SNMP 凭据")
|
||||
|
||||
entries = SNMPService.get_arp_table(device, device.snmp_credential, db)
|
||||
entries = await SNMPService.get_arp_table(device, device.snmp_credential, db)
|
||||
|
||||
return {
|
||||
"device_id": device_id,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,6 +7,7 @@ import socket
|
||||
import ipaddress
|
||||
import re
|
||||
import logging
|
||||
import os
|
||||
|
||||
try:
|
||||
from mac_vendor_lookup import MacLookup, VendorNotFoundError
|
||||
@@ -20,6 +21,40 @@ from app.core.config import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# OUI 数据库文件路径(相对于项目根)
|
||||
_OUI_DB_FILE = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'data', 'oui.txt')
|
||||
|
||||
|
||||
def _load_oui_database() -> Optional[Dict[str, str]]:
|
||||
"""从本地文件加载 OUI 数据库,返回 {prefix: vendor} 字典"""
|
||||
if not os.path.exists(_OUI_DB_FILE):
|
||||
logger.warning(f"OUI 数据库文件不存在: {_OUI_DB_FILE}")
|
||||
return None
|
||||
try:
|
||||
db = {}
|
||||
with open(_OUI_DB_FILE, 'r', encoding='utf-8') as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if ':' in line:
|
||||
prefix, vendor = line.split(':', 1)
|
||||
db[prefix] = vendor
|
||||
logger.info(f"已加载 OUI 数据库: {len(db)} 条记录, 文件: {_OUI_DB_FILE}")
|
||||
return db
|
||||
except Exception as e:
|
||||
logger.error(f"加载 OUI 数据库失败: {e}")
|
||||
return None
|
||||
|
||||
|
||||
# 全局 OUI 数据库(延迟加载)
|
||||
_OUI_DB = None
|
||||
|
||||
|
||||
def _get_oui_db() -> Optional[Dict[str, str]]:
|
||||
global _OUI_DB
|
||||
if _OUI_DB is None:
|
||||
_OUI_DB = _load_oui_database()
|
||||
return _OUI_DB
|
||||
|
||||
|
||||
class EnhancedScanService:
|
||||
"""增强扫描服务 - 包含ARP、DNS解析、MAC厂商识别、NetBIOS主机名发现"""
|
||||
@@ -45,13 +80,23 @@ class EnhancedScanService:
|
||||
if len(mac) < 8:
|
||||
return None
|
||||
|
||||
# 使用 mac_vendor_lookup 库(IEEE OUI 官方数据库)
|
||||
# 提取 OUI(前 6 位十六进制 = AA:BB:CC)
|
||||
oui = mac[:8] # "AA:BB:CC"
|
||||
|
||||
# 1️⃣ 优先使用本地 OUI 数据库(免网络、高性能)
|
||||
db = _get_oui_db()
|
||||
if db:
|
||||
# 数据库 key 是 AABBCC 格式(无冒号)
|
||||
oui_key = oui.replace(':', '')
|
||||
vendor = db.get(oui_key)
|
||||
if vendor:
|
||||
return vendor
|
||||
|
||||
# 2️⃣ 回退:mac_vendor_lookup 在线库
|
||||
if MAC_LOOKUP_AVAILABLE:
|
||||
lookup = EnhancedScanService._get_mac_lookup()
|
||||
if lookup is not None:
|
||||
try:
|
||||
# 提取 OUI(前 3 字节)
|
||||
oui = mac[:8] # "AA:BB:CC"
|
||||
vendor = lookup.lookup(oui)
|
||||
if vendor:
|
||||
return vendor
|
||||
@@ -60,7 +105,7 @@ class EnhancedScanService:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 回退:硬编码常用厂商映射
|
||||
# 3️⃣ 回退:硬编码常用厂商映射
|
||||
return EnhancedScanService._legacy_oui_lookup(mac)
|
||||
|
||||
# 硬编码回退映射(mac_vendor_lookup 不可用时使用)
|
||||
|
||||
@@ -14,9 +14,9 @@ class NetworkService:
|
||||
|
||||
@staticmethod
|
||||
def calculate_total_ips(cidr: str) -> int:
|
||||
"""计算网段的总IP数量"""
|
||||
"""计算网段中实际分配的IP数量(排除网络地址和广播地址)"""
|
||||
network = ipaddress.ip_network(cidr, strict=False)
|
||||
return network.num_addresses
|
||||
return network.num_addresses - 2 if network.num_addresses > 2 else network.num_addresses
|
||||
|
||||
@staticmethod
|
||||
def get_network_addresses(cidr: str) -> List[str]:
|
||||
|
||||
@@ -3,6 +3,7 @@ from sqlalchemy.orm import Session
|
||||
from datetime import datetime
|
||||
import ipaddress
|
||||
import logging
|
||||
import asyncio
|
||||
|
||||
from pysnmp.hlapi.asyncio import (
|
||||
SnmpEngine, CommunityData, UsmUserData,
|
||||
@@ -117,7 +118,7 @@ class SNMPService:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def test_connection(device: NetworkDevice, credential: SNMPCredential) -> Tuple[bool, Dict[str, Any]]:
|
||||
async def test_connection(device: NetworkDevice, credential: SNMPCredential) -> Tuple[bool, Dict[str, Any]]:
|
||||
"""
|
||||
测试 SNMP 连接
|
||||
返回: (成功, 设备信息字典)
|
||||
@@ -131,11 +132,11 @@ class SNMPService:
|
||||
retries=credential.retries
|
||||
)
|
||||
|
||||
errorIndication, errorStatus, errorIndex, varBinds = next(
|
||||
getCmd(SnmpEngine(), auth_data, transport, ContextData(),
|
||||
errorIndication, errorStatus, errorIndex, varBinds = await getCmd(
|
||||
SnmpEngine(), auth_data, transport, ContextData(),
|
||||
ObjectType(ObjectIdentity(SNMPService.OID_SYS_DESCR)),
|
||||
ObjectType(ObjectIdentity(SNMPService.OID_SYS_NAME)),
|
||||
ObjectType(ObjectIdentity(SNMPService.OID_SYS_LOCATION)))
|
||||
ObjectType(ObjectIdentity(SNMPService.OID_SYS_LOCATION))
|
||||
)
|
||||
|
||||
if errorIndication:
|
||||
@@ -162,7 +163,7 @@ class SNMPService:
|
||||
return False, {'error': str(e)}
|
||||
|
||||
@staticmethod
|
||||
def get_arp_table(device: NetworkDevice, credential: SNMPCredential, db: Session) -> List[Dict[str, Any]]:
|
||||
async def get_arp_table(device: NetworkDevice, credential: SNMPCredential, db: Session) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
获取 ARP 表 (IP -> MAC 映射)
|
||||
"""
|
||||
@@ -178,22 +179,21 @@ class SNMPService:
|
||||
)
|
||||
|
||||
# 使用 bulkCmd 获取 ARP 表
|
||||
for errorIndication, errorStatus, errorIndex, varBinds in bulkCmd(
|
||||
errorIndication, errorStatus, errorIndex, varBindTable = await bulkCmd(
|
||||
SnmpEngine(), auth_data, transport, ContextData(),
|
||||
0, 50, # nonRepeaters, maxRepetitions
|
||||
ObjectType(ObjectIdentity(SNMPService.OID_IP_NET_TO_MEDIA_PHYS_ADDRESS)),
|
||||
ObjectType(ObjectIdentity(SNMPService.OID_IP_NET_TO_MEDIA_NET_ADDRESS)),
|
||||
ObjectType(ObjectIdentity(SNMPService.OID_IP_NET_TO_MEDIA_IF_INDEX)),
|
||||
lexicographicMode=False
|
||||
):
|
||||
)
|
||||
if errorIndication:
|
||||
logger.error(f"SNMP 错误: {errorIndication}")
|
||||
break
|
||||
|
||||
if errorStatus:
|
||||
elif errorStatus:
|
||||
logger.error(f"SNMP 错误: {errorStatus}")
|
||||
break
|
||||
|
||||
else:
|
||||
# varBindTable 是列表的列表,每行是一条 SNMP 响应记录
|
||||
for varBinds in varBindTable:
|
||||
# 解析结果
|
||||
arp_data = {}
|
||||
for varBind in varBinds:
|
||||
@@ -258,16 +258,18 @@ class SNMPService:
|
||||
|
||||
db.commit()
|
||||
|
||||
# 更新 IP 资产台账中的 MAC 地址
|
||||
# 更新 IP 资产台账中的 MAC 地址和厂商信息
|
||||
for entry in arp_entries:
|
||||
from app.models.network import IPAddress as IPAddressModel
|
||||
ip_addr = db.query(IPAddressModel).filter(
|
||||
IPAddressModel.ip_address == entry['ip_address']
|
||||
).first()
|
||||
if ip_addr and not ip_addr.mac_address:
|
||||
if ip_addr:
|
||||
if not ip_addr.mac_address:
|
||||
ip_addr.mac_address = entry['mac_address']
|
||||
# 识别厂商
|
||||
ip_addr.vendor = EnhancedScanService.get_mac_vendor(entry['mac_address'])
|
||||
# 始终尝试补全厂商(有 MAC 但没厂商时也需要)
|
||||
if not ip_addr.vendor and ip_addr.mac_address:
|
||||
ip_addr.vendor = EnhancedScanService.get_mac_vendor(ip_addr.mac_address)
|
||||
|
||||
db.commit()
|
||||
device.last_successful_poll = now
|
||||
@@ -281,7 +283,7 @@ class SNMPService:
|
||||
return arp_entries
|
||||
|
||||
@staticmethod
|
||||
def get_mac_address_table(device: NetworkDevice, credential: SNMPCredential, db: Session, vlan_id: Optional[int] = None) -> List[Dict[str, Any]]:
|
||||
async def get_mac_address_table(device: NetworkDevice, credential: SNMPCredential, db: Session, vlan_id: Optional[int] = None) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
获取 MAC 地址表 (MAC -> 端口 映射)
|
||||
"""
|
||||
@@ -297,19 +299,19 @@ class SNMPService:
|
||||
)
|
||||
|
||||
# 获取 MAC 地址表
|
||||
for errorIndication, errorStatus, errorIndex, varBinds in bulkCmd(
|
||||
errorIndication, errorStatus, errorIndex, varBindTable = await bulkCmd(
|
||||
SnmpEngine(), auth_data, transport, ContextData(),
|
||||
0, 100,
|
||||
ObjectType(ObjectIdentity(SNMPService.OID_DOT1D_TP_FDB_PORT)),
|
||||
ObjectType(ObjectIdentity(SNMPService.OID_DOT1D_TP_FDB_STATUS)),
|
||||
lexicographicMode=False
|
||||
):
|
||||
)
|
||||
if errorIndication:
|
||||
break
|
||||
|
||||
if errorStatus:
|
||||
break
|
||||
|
||||
logger.error(f"获取 MAC 地址表失败: {errorIndication}")
|
||||
elif errorStatus:
|
||||
logger.error(f"获取 MAC 地址表失败: {errorStatus}")
|
||||
else:
|
||||
for varBinds in varBindTable:
|
||||
for varBind in varBinds:
|
||||
oid = str(varBind[0])
|
||||
value = varBind[1]
|
||||
@@ -361,7 +363,7 @@ class SNMPService:
|
||||
return mac_entries
|
||||
|
||||
@staticmethod
|
||||
def get_interfaces(device: NetworkDevice, credential: SNMPCredential, db: Session) -> List[Dict[str, Any]]:
|
||||
async def get_interfaces(device: NetworkDevice, credential: SNMPCredential, db: Session) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
获取设备接口信息
|
||||
"""
|
||||
@@ -379,7 +381,7 @@ class SNMPService:
|
||||
# 获取接口信息
|
||||
interface_data = {}
|
||||
|
||||
for errorIndication, errorStatus, errorIndex, varBinds in bulkCmd(
|
||||
errorIndication, errorStatus, errorIndex, varBindTable = await bulkCmd(
|
||||
SnmpEngine(), auth_data, transport, ContextData(),
|
||||
0, 100,
|
||||
ObjectType(ObjectIdentity(SNMPService.OID_IF_DESCR)),
|
||||
@@ -390,10 +392,9 @@ class SNMPService:
|
||||
ObjectType(ObjectIdentity(SNMPService.OID_IF_ADMIN_STATUS)),
|
||||
ObjectType(ObjectIdentity(SNMPService.OID_IF_OPER_STATUS)),
|
||||
lexicographicMode=False
|
||||
):
|
||||
if errorIndication or errorStatus:
|
||||
break
|
||||
|
||||
)
|
||||
if not errorIndication and not errorStatus:
|
||||
for varBinds in varBindTable:
|
||||
for varBind in varBinds:
|
||||
oid = str(varBind[0])
|
||||
value = varBind[1]
|
||||
@@ -485,7 +486,7 @@ class SNMPService:
|
||||
return device
|
||||
|
||||
@staticmethod
|
||||
def poll_device(db: Session, device_id: int) -> Dict[str, Any]:
|
||||
async def poll_device(db: Session, device_id: int) -> Dict[str, Any]:
|
||||
"""轮询设备,获取所有信息"""
|
||||
device = db.query(NetworkDevice).filter(NetworkDevice.id == device_id).first()
|
||||
if not device:
|
||||
@@ -505,17 +506,17 @@ class SNMPService:
|
||||
}
|
||||
|
||||
# 获取 ARP 表
|
||||
arp_entries = SNMPService.get_arp_table(device, credential, db)
|
||||
arp_entries = await SNMPService.get_arp_table(device, credential, db)
|
||||
result['arp_entries'] = arp_entries
|
||||
result['arp_count'] = len(arp_entries)
|
||||
|
||||
# 获取 MAC 地址表
|
||||
mac_entries = SNMPService.get_mac_address_table(device, credential, db)
|
||||
mac_entries = await SNMPService.get_mac_address_table(device, credential, db)
|
||||
result['mac_entries'] = mac_entries
|
||||
result['mac_count'] = len(mac_entries)
|
||||
|
||||
# 获取接口信息
|
||||
interfaces = SNMPService.get_interfaces(device, credential, db)
|
||||
interfaces = await SNMPService.get_interfaces(device, credential, db)
|
||||
result['interfaces'] = interfaces
|
||||
result['interface_count'] = len(interfaces)
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
"""
|
||||
修复已有网段的 total_ips 字段:将 num_addresses 改为 hosts 数量(排除网络地址和广播地址)
|
||||
|
||||
用法: cd /root/ipam/backend && source venv/bin/activate && python scripts/fix_network_total_ips.py
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
|
||||
# 确保能找到 app 模块
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from app.core.database import SessionLocal
|
||||
from app.models.network import Network
|
||||
from app.services.network_service import NetworkService
|
||||
|
||||
|
||||
def fix_total_ips():
|
||||
db = SessionLocal()
|
||||
try:
|
||||
networks = db.query(Network).all()
|
||||
fixed = 0
|
||||
for net in networks:
|
||||
correct = NetworkService.calculate_total_ips(net.cidr)
|
||||
actual_hosts = len(list(NetworkService.get_network_addresses(net.cidr)))
|
||||
if net.total_ips != correct:
|
||||
print(f" [{net.cidr:20s}] {net.total_ips:>5} -> {correct:>5} (hosts={actual_hosts})")
|
||||
net.total_ips = correct
|
||||
fixed += 1
|
||||
db.commit()
|
||||
print(f"\n✅ 已修复 {fixed} 个网段")
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
print(f"❌ 错误: {e}")
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
fix_total_ips()
|
||||
@@ -4,9 +4,10 @@
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
"dev": "node node_modules/vite/bin/vite.js",
|
||||
"dev:clean": "rm -rf node_modules/.vite && node node_modules/vite/bin/vite.js --force",
|
||||
"build": "node node_modules/vite/bin/vite.js build",
|
||||
"preview": "node node_modules/vite/bin/vite.js preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.2",
|
||||
|
||||
@@ -138,21 +138,61 @@ start_frontend() {
|
||||
|
||||
cd $FRONTEND_DIR
|
||||
|
||||
# 检查 node_modules
|
||||
if [ ! -d "node_modules" ]; then
|
||||
# 检查 node_modules 及关键依赖是否完整
|
||||
if [ ! -d "node_modules" ] || [ ! -f "node_modules/element-plus/es/index.mjs" ]; then
|
||||
echo -e "${YELLOW}安装前端依赖...${NC}"
|
||||
npm install
|
||||
fi
|
||||
|
||||
# 检查 OUI 数据库文件
|
||||
if [ ! -f "$BACKEND_DIR/app/data/oui.txt" ]; then
|
||||
echo -e "${YELLOW}OUI 数据库不存在,尝试从 IEEE 下载...${NC}"
|
||||
cd $BACKEND_DIR
|
||||
source venv/bin/activate
|
||||
python3 -c "
|
||||
from mac_vendor_lookup import MacLookup
|
||||
import os
|
||||
os.makedirs('app/data', exist_ok=True)
|
||||
lookup = MacLookup()
|
||||
lookup.update_vendors()
|
||||
# 复制到项目目录
|
||||
import shutil
|
||||
src = os.path.expanduser('~/.cache/mac-vendors.txt')
|
||||
if os.path.exists(src):
|
||||
shutil.copy(src, 'app/data/oui.txt')
|
||||
print('OUI 数据库下载完成')
|
||||
" 2>/dev/null || echo -e "${YELLOW}OUI 数据库下载失败(网络问题),使用内置硬编码回退${NC}"
|
||||
fi
|
||||
|
||||
# 检查端口并清理
|
||||
kill_port $FRONTEND_PORT
|
||||
|
||||
# 清理 Vite 缓存,避免 element-plus exports 解析失败
|
||||
if [ -d "node_modules/.vite" ]; then
|
||||
echo -e "${YELLOW}清理 Vite 缓存...${NC}"
|
||||
rm -rf node_modules/.vite
|
||||
fi
|
||||
|
||||
# 后台启动前端
|
||||
nohup npm run dev -- --host 0.0.0.0 --port $FRONTEND_PORT > /tmp/ipam-frontend.log 2>&1 &
|
||||
FRONTEND_PID=$!
|
||||
|
||||
sleep 8
|
||||
|
||||
if curl -s http://localhost:$FRONTEND_PORT > /dev/null; then
|
||||
echo -e "${GREEN}✅ 前端服务启动成功 (端口 $FRONTEND_PORT)${NC}"
|
||||
echo " PID: $FRONTEND_PID"
|
||||
echo " 日志: /tmp/ipam-frontend.log"
|
||||
return 0
|
||||
else
|
||||
# 如果启动失败,尝试清缓存重试一次
|
||||
echo -e "${YELLOW}首次启动失败,尝试清理 Vite 缓存重试...${NC}"
|
||||
kill -9 $FRONTEND_PID 2>/dev/null
|
||||
sleep 1
|
||||
rm -rf node_modules/.vite
|
||||
nohup npm run dev -- --host 0.0.0.0 --port $FRONTEND_PORT > /tmp/ipam-frontend.log 2>&1 &
|
||||
FRONTEND_PID=$!
|
||||
sleep 12
|
||||
if curl -s http://localhost:$FRONTEND_PORT > /dev/null; then
|
||||
echo -e "${GREEN}✅ 前端服务启动成功 (端口 $FRONTEND_PORT)${NC}"
|
||||
echo " PID: $FRONTEND_PID"
|
||||
@@ -163,6 +203,7 @@ start_frontend() {
|
||||
echo " 查看日志: tail -50 /tmp/ipam-frontend.log"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
echo ""
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user