Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 11d830b496 | |||
| 934b604b65 | |||
| 13c457acb3 | |||
| 05cd71ca1a | |||
| ee0552f837 | |||
| 094d2cc112 | |||
| 8b1df674ec | |||
| 6076e174d5 | |||
| 8a94f535de | |||
| ebb04f9ad8 |
@@ -437,7 +437,7 @@ def delete_network_device(
|
|||||||
# ========== SNMP 操作 ==========
|
# ========== SNMP 操作 ==========
|
||||||
|
|
||||||
@router.post("/devices/{device_id}/test", summary="测试 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 连接(不写审计,避免刷屏)"""
|
"""测试设备的 SNMP 连接(不写审计,避免刷屏)"""
|
||||||
from app.models.snmp import NetworkDevice
|
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:
|
if not device.snmp_credential:
|
||||||
raise HTTPException(status_code=400, detail="设备未配置 SNMP 凭据")
|
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 {
|
return {
|
||||||
"device_id": device_id,
|
"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="立即轮询设备")
|
@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 地址表、接口信息(不写审计,常规操作)"""
|
"""立即轮询设备,获取 ARP 表、MAC 地址表、接口信息(不写审计,常规操作)"""
|
||||||
result = SNMPService.poll_device(db, device_id)
|
result = await SNMPService.poll_device(db, device_id)
|
||||||
|
|
||||||
if 'error' in result:
|
if 'error' in result:
|
||||||
raise HTTPException(status_code=400, detail=result['error'])
|
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 表")
|
@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 表"""
|
"""获取设备的 ARP 表"""
|
||||||
from app.models.snmp import NetworkDevice, ARPEntry
|
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:
|
if not device.snmp_credential:
|
||||||
raise HTTPException(status_code=400, detail="设备未配置 SNMP 凭据")
|
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 {
|
return {
|
||||||
"device_id": device_id,
|
"device_id": device_id,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -7,6 +7,7 @@ import socket
|
|||||||
import ipaddress
|
import ipaddress
|
||||||
import re
|
import re
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from mac_vendor_lookup import MacLookup, VendorNotFoundError
|
from mac_vendor_lookup import MacLookup, VendorNotFoundError
|
||||||
@@ -20,6 +21,40 @@ from app.core.config import settings
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
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:
|
class EnhancedScanService:
|
||||||
"""增强扫描服务 - 包含ARP、DNS解析、MAC厂商识别、NetBIOS主机名发现"""
|
"""增强扫描服务 - 包含ARP、DNS解析、MAC厂商识别、NetBIOS主机名发现"""
|
||||||
@@ -45,13 +80,23 @@ class EnhancedScanService:
|
|||||||
if len(mac) < 8:
|
if len(mac) < 8:
|
||||||
return None
|
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:
|
if MAC_LOOKUP_AVAILABLE:
|
||||||
lookup = EnhancedScanService._get_mac_lookup()
|
lookup = EnhancedScanService._get_mac_lookup()
|
||||||
if lookup is not None:
|
if lookup is not None:
|
||||||
try:
|
try:
|
||||||
# 提取 OUI(前 3 字节)
|
|
||||||
oui = mac[:8] # "AA:BB:CC"
|
|
||||||
vendor = lookup.lookup(oui)
|
vendor = lookup.lookup(oui)
|
||||||
if vendor:
|
if vendor:
|
||||||
return vendor
|
return vendor
|
||||||
@@ -60,7 +105,7 @@ class EnhancedScanService:
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# 回退:硬编码常用厂商映射
|
# 3️⃣ 回退:硬编码常用厂商映射
|
||||||
return EnhancedScanService._legacy_oui_lookup(mac)
|
return EnhancedScanService._legacy_oui_lookup(mac)
|
||||||
|
|
||||||
# 硬编码回退映射(mac_vendor_lookup 不可用时使用)
|
# 硬编码回退映射(mac_vendor_lookup 不可用时使用)
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ class NetworkService:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def calculate_total_ips(cidr: str) -> int:
|
def calculate_total_ips(cidr: str) -> int:
|
||||||
"""计算网段的总IP数量"""
|
"""计算网段中实际分配的IP数量(排除网络地址和广播地址)"""
|
||||||
network = ipaddress.ip_network(cidr, strict=False)
|
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
|
@staticmethod
|
||||||
def get_network_addresses(cidr: str) -> List[str]:
|
def get_network_addresses(cidr: str) -> List[str]:
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from sqlalchemy.orm import Session
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import ipaddress
|
import ipaddress
|
||||||
import logging
|
import logging
|
||||||
|
import asyncio
|
||||||
|
|
||||||
from pysnmp.hlapi.asyncio import (
|
from pysnmp.hlapi.asyncio import (
|
||||||
SnmpEngine, CommunityData, UsmUserData,
|
SnmpEngine, CommunityData, UsmUserData,
|
||||||
@@ -117,7 +118,7 @@ class SNMPService:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
@staticmethod
|
@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 连接
|
测试 SNMP 连接
|
||||||
返回: (成功, 设备信息字典)
|
返回: (成功, 设备信息字典)
|
||||||
@@ -131,11 +132,11 @@ class SNMPService:
|
|||||||
retries=credential.retries
|
retries=credential.retries
|
||||||
)
|
)
|
||||||
|
|
||||||
errorIndication, errorStatus, errorIndex, varBinds = next(
|
errorIndication, errorStatus, errorIndex, varBinds = await getCmd(
|
||||||
getCmd(SnmpEngine(), auth_data, transport, ContextData(),
|
SnmpEngine(), auth_data, transport, ContextData(),
|
||||||
ObjectType(ObjectIdentity(SNMPService.OID_SYS_DESCR)),
|
ObjectType(ObjectIdentity(SNMPService.OID_SYS_DESCR)),
|
||||||
ObjectType(ObjectIdentity(SNMPService.OID_SYS_NAME)),
|
ObjectType(ObjectIdentity(SNMPService.OID_SYS_NAME)),
|
||||||
ObjectType(ObjectIdentity(SNMPService.OID_SYS_LOCATION)))
|
ObjectType(ObjectIdentity(SNMPService.OID_SYS_LOCATION))
|
||||||
)
|
)
|
||||||
|
|
||||||
if errorIndication:
|
if errorIndication:
|
||||||
@@ -162,7 +163,7 @@ class SNMPService:
|
|||||||
return False, {'error': str(e)}
|
return False, {'error': str(e)}
|
||||||
|
|
||||||
@staticmethod
|
@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 映射)
|
获取 ARP 表 (IP -> MAC 映射)
|
||||||
"""
|
"""
|
||||||
@@ -178,22 +179,21 @@ class SNMPService:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# 使用 bulkCmd 获取 ARP 表
|
# 使用 bulkCmd 获取 ARP 表
|
||||||
for errorIndication, errorStatus, errorIndex, varBinds in bulkCmd(
|
errorIndication, errorStatus, errorIndex, varBindTable = await bulkCmd(
|
||||||
SnmpEngine(), auth_data, transport, ContextData(),
|
SnmpEngine(), auth_data, transport, ContextData(),
|
||||||
0, 50, # nonRepeaters, maxRepetitions
|
0, 50, # nonRepeaters, maxRepetitions
|
||||||
ObjectType(ObjectIdentity(SNMPService.OID_IP_NET_TO_MEDIA_PHYS_ADDRESS)),
|
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_NET_ADDRESS)),
|
||||||
ObjectType(ObjectIdentity(SNMPService.OID_IP_NET_TO_MEDIA_IF_INDEX)),
|
ObjectType(ObjectIdentity(SNMPService.OID_IP_NET_TO_MEDIA_IF_INDEX)),
|
||||||
lexicographicMode=False
|
lexicographicMode=False
|
||||||
):
|
)
|
||||||
if errorIndication:
|
if errorIndication:
|
||||||
logger.error(f"SNMP 错误: {errorIndication}")
|
logger.error(f"SNMP 错误: {errorIndication}")
|
||||||
break
|
elif errorStatus:
|
||||||
|
|
||||||
if errorStatus:
|
|
||||||
logger.error(f"SNMP 错误: {errorStatus}")
|
logger.error(f"SNMP 错误: {errorStatus}")
|
||||||
break
|
else:
|
||||||
|
# varBindTable 是列表的列表,每行是一条 SNMP 响应记录
|
||||||
|
for varBinds in varBindTable:
|
||||||
# 解析结果
|
# 解析结果
|
||||||
arp_data = {}
|
arp_data = {}
|
||||||
for varBind in varBinds:
|
for varBind in varBinds:
|
||||||
@@ -258,16 +258,18 @@ class SNMPService:
|
|||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
# 更新 IP 资产台账中的 MAC 地址
|
# 更新 IP 资产台账中的 MAC 地址和厂商信息
|
||||||
for entry in arp_entries:
|
for entry in arp_entries:
|
||||||
from app.models.network import IPAddress as IPAddressModel
|
from app.models.network import IPAddress as IPAddressModel
|
||||||
ip_addr = db.query(IPAddressModel).filter(
|
ip_addr = db.query(IPAddressModel).filter(
|
||||||
IPAddressModel.ip_address == entry['ip_address']
|
IPAddressModel.ip_address == entry['ip_address']
|
||||||
).first()
|
).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.mac_address = entry['mac_address']
|
||||||
# 识别厂商
|
# 始终尝试补全厂商(有 MAC 但没厂商时也需要)
|
||||||
ip_addr.vendor = EnhancedScanService.get_mac_vendor(entry['mac_address'])
|
if not ip_addr.vendor and ip_addr.mac_address:
|
||||||
|
ip_addr.vendor = EnhancedScanService.get_mac_vendor(ip_addr.mac_address)
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
device.last_successful_poll = now
|
device.last_successful_poll = now
|
||||||
@@ -281,7 +283,7 @@ class SNMPService:
|
|||||||
return arp_entries
|
return arp_entries
|
||||||
|
|
||||||
@staticmethod
|
@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 -> 端口 映射)
|
获取 MAC 地址表 (MAC -> 端口 映射)
|
||||||
"""
|
"""
|
||||||
@@ -297,19 +299,19 @@ class SNMPService:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# 获取 MAC 地址表
|
# 获取 MAC 地址表
|
||||||
for errorIndication, errorStatus, errorIndex, varBinds in bulkCmd(
|
errorIndication, errorStatus, errorIndex, varBindTable = await bulkCmd(
|
||||||
SnmpEngine(), auth_data, transport, ContextData(),
|
SnmpEngine(), auth_data, transport, ContextData(),
|
||||||
0, 100,
|
0, 100,
|
||||||
ObjectType(ObjectIdentity(SNMPService.OID_DOT1D_TP_FDB_PORT)),
|
ObjectType(ObjectIdentity(SNMPService.OID_DOT1D_TP_FDB_PORT)),
|
||||||
ObjectType(ObjectIdentity(SNMPService.OID_DOT1D_TP_FDB_STATUS)),
|
ObjectType(ObjectIdentity(SNMPService.OID_DOT1D_TP_FDB_STATUS)),
|
||||||
lexicographicMode=False
|
lexicographicMode=False
|
||||||
):
|
)
|
||||||
if errorIndication:
|
if errorIndication:
|
||||||
break
|
logger.error(f"获取 MAC 地址表失败: {errorIndication}")
|
||||||
|
elif errorStatus:
|
||||||
if errorStatus:
|
logger.error(f"获取 MAC 地址表失败: {errorStatus}")
|
||||||
break
|
else:
|
||||||
|
for varBinds in varBindTable:
|
||||||
for varBind in varBinds:
|
for varBind in varBinds:
|
||||||
oid = str(varBind[0])
|
oid = str(varBind[0])
|
||||||
value = varBind[1]
|
value = varBind[1]
|
||||||
@@ -361,7 +363,7 @@ class SNMPService:
|
|||||||
return mac_entries
|
return mac_entries
|
||||||
|
|
||||||
@staticmethod
|
@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 = {}
|
interface_data = {}
|
||||||
|
|
||||||
for errorIndication, errorStatus, errorIndex, varBinds in bulkCmd(
|
errorIndication, errorStatus, errorIndex, varBindTable = await bulkCmd(
|
||||||
SnmpEngine(), auth_data, transport, ContextData(),
|
SnmpEngine(), auth_data, transport, ContextData(),
|
||||||
0, 100,
|
0, 100,
|
||||||
ObjectType(ObjectIdentity(SNMPService.OID_IF_DESCR)),
|
ObjectType(ObjectIdentity(SNMPService.OID_IF_DESCR)),
|
||||||
@@ -390,10 +392,9 @@ class SNMPService:
|
|||||||
ObjectType(ObjectIdentity(SNMPService.OID_IF_ADMIN_STATUS)),
|
ObjectType(ObjectIdentity(SNMPService.OID_IF_ADMIN_STATUS)),
|
||||||
ObjectType(ObjectIdentity(SNMPService.OID_IF_OPER_STATUS)),
|
ObjectType(ObjectIdentity(SNMPService.OID_IF_OPER_STATUS)),
|
||||||
lexicographicMode=False
|
lexicographicMode=False
|
||||||
):
|
)
|
||||||
if errorIndication or errorStatus:
|
if not errorIndication and not errorStatus:
|
||||||
break
|
for varBinds in varBindTable:
|
||||||
|
|
||||||
for varBind in varBinds:
|
for varBind in varBinds:
|
||||||
oid = str(varBind[0])
|
oid = str(varBind[0])
|
||||||
value = varBind[1]
|
value = varBind[1]
|
||||||
@@ -485,7 +486,7 @@ class SNMPService:
|
|||||||
return device
|
return device
|
||||||
|
|
||||||
@staticmethod
|
@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()
|
device = db.query(NetworkDevice).filter(NetworkDevice.id == device_id).first()
|
||||||
if not device:
|
if not device:
|
||||||
@@ -505,17 +506,17 @@ class SNMPService:
|
|||||||
}
|
}
|
||||||
|
|
||||||
# 获取 ARP 表
|
# 获取 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_entries'] = arp_entries
|
||||||
result['arp_count'] = len(arp_entries)
|
result['arp_count'] = len(arp_entries)
|
||||||
|
|
||||||
# 获取 MAC 地址表
|
# 获取 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_entries'] = mac_entries
|
||||||
result['mac_count'] = len(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['interfaces'] = interfaces
|
||||||
result['interface_count'] = len(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",
|
"version": "0.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "node node_modules/vite/bin/vite.js",
|
||||||
"build": "vite build",
|
"dev:clean": "rm -rf node_modules/.vite && node node_modules/vite/bin/vite.js --force",
|
||||||
"preview": "vite preview"
|
"build": "node node_modules/vite/bin/vite.js build",
|
||||||
|
"preview": "node node_modules/vite/bin/vite.js preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@element-plus/icons-vue": "^2.3.2",
|
"@element-plus/icons-vue": "^2.3.2",
|
||||||
|
|||||||
@@ -138,21 +138,61 @@ start_frontend() {
|
|||||||
|
|
||||||
cd $FRONTEND_DIR
|
cd $FRONTEND_DIR
|
||||||
|
|
||||||
# 检查 node_modules
|
# 检查 node_modules 及关键依赖是否完整
|
||||||
if [ ! -d "node_modules" ]; then
|
if [ ! -d "node_modules" ] || [ ! -f "node_modules/element-plus/es/index.mjs" ]; then
|
||||||
echo -e "${YELLOW}安装前端依赖...${NC}"
|
echo -e "${YELLOW}安装前端依赖...${NC}"
|
||||||
npm install
|
npm install
|
||||||
fi
|
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
|
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 &
|
nohup npm run dev -- --host 0.0.0.0 --port $FRONTEND_PORT > /tmp/ipam-frontend.log 2>&1 &
|
||||||
FRONTEND_PID=$!
|
FRONTEND_PID=$!
|
||||||
|
|
||||||
sleep 8
|
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
|
if curl -s http://localhost:$FRONTEND_PORT > /dev/null; then
|
||||||
echo -e "${GREEN}✅ 前端服务启动成功 (端口 $FRONTEND_PORT)${NC}"
|
echo -e "${GREEN}✅ 前端服务启动成功 (端口 $FRONTEND_PORT)${NC}"
|
||||||
echo " PID: $FRONTEND_PID"
|
echo " PID: $FRONTEND_PID"
|
||||||
@@ -163,6 +203,7 @@ start_frontend() {
|
|||||||
echo " 查看日志: tail -50 /tmp/ipam-frontend.log"
|
echo " 查看日志: tail -50 /tmp/ipam-frontend.log"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
fi
|
||||||
echo ""
|
echo ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user