fix(ipam): 修复IP管理页面网段筛选不生效及下拉框过窄

- Ips.vue: el-select 添加 width: 280px,修复网段下拉框过窄
- Ips.vue: query 参数 networkId → network_id(与 FastAPI snake_case 一致)
- Scan.vue: el-select 用 network.id 代替整个 network 对象作为 value,修复切换网段不生效
- enhanced_scan_service.py: 替换失效的 scapy ARP 扫描为 arp-scan 命令;扫描结果自动创建缺失 IP 记录
This commit is contained in:
Your Name
2026-07-18 09:45:09 +08:00
commit 1e1e5957e8
103 changed files with 13196 additions and 0 deletions
+190
View File
@@ -0,0 +1,190 @@
from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, Enum, Boolean
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.core.database import Base
import enum
class SNMPVersion(str, enum.Enum):
"""SNMP 版本"""
V2C = "v2c"
V3 = "v3"
class SNMPAuthProtocol(str, enum.Enum):
"""SNMP V3 认证协议"""
MD5 = "md5"
SHA = "sha"
SHA256 = "sha256"
class SNMPPrivProtocol(str, enum.Enum):
"""SNMP V3 加密协议"""
DES = "des"
AES = "aes"
AES256 = "aes256"
class DeviceType(str, enum.Enum):
"""设备类型"""
ROUTER = "router"
CORE_SWITCH = "core_switch"
DISTRIBUTION_SWITCH = "distribution_switch"
ACCESS_SWITCH = "access_switch"
FIREWALL = "firewall"
LOAD_BALANCER = "load_balancer"
OTHER = "other"
class SNMPCredential(Base):
"""SNMP 凭据"""
__tablename__ = "snmp_credentials"
id = Column(Integer, primary_key=True, index=True)
name = Column(String(100), unique=True, index=True, nullable=False)
description = Column(Text, nullable=True)
# 版本
version = Column(Enum(SNMPVersion), default=SNMPVersion.V2C, nullable=False)
# V2C 配置
community_string = Column(String(255), nullable=True)
# V3 配置
username = Column(String(100), nullable=True)
auth_password = Column(String(255), nullable=True)
auth_protocol = Column(Enum(SNMPAuthProtocol), nullable=True)
priv_password = Column(String(255), nullable=True)
priv_protocol = Column(Enum(SNMPPrivProtocol), nullable=True)
# 超时和重试
timeout = Column(Integer, default=3)
retries = Column(Integer, default=2)
is_active = Column(Boolean, default=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
# 关系
devices = relationship("NetworkDevice", back_populates="snmp_credential")
class NetworkDevice(Base):
"""网络设备"""
__tablename__ = "network_devices"
id = Column(Integer, primary_key=True, index=True)
name = Column(String(100), unique=True, index=True, nullable=False)
description = Column(Text, nullable=True)
# 连接信息
ip_address = Column(String(50), index=True, nullable=False)
port = Column(Integer, default=161)
# 设备信息
device_type = Column(Enum(DeviceType), default=DeviceType.OTHER)
vendor = Column(String(100), nullable=True)
model = Column(String(100), nullable=True)
firmware_version = Column(String(100), nullable=True)
serial_number = Column(String(100), nullable=True)
location = Column(String(200), nullable=True)
# SNMP 凭据
snmp_credential_id = Column(Integer, ForeignKey("snmp_credentials.id"), nullable=True)
# 状态
is_active = Column(Boolean, default=True)
last_polled_at = Column(DateTime(timezone=True), nullable=True)
last_successful_poll = Column(DateTime(timezone=True), nullable=True)
# 采集配置
arp_poll_interval = Column(Integer, default=300) # ARP表采集间隔(秒)
mac_poll_interval = Column(Integer, default=300) # MAC表采集间隔(秒)
interface_poll_interval = Column(Integer, default=300) # 接口信息采集间隔
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
# 关系
snmp_credential = relationship("SNMPCredential", back_populates="devices")
arp_entries = relationship("ARPEntry", back_populates="device", cascade="all, delete-orphan")
mac_entries = relationship("MACAddressTable", back_populates="device", cascade="all, delete-orphan")
interfaces = relationship("DeviceInterface", back_populates="device", cascade="all, delete-orphan")
class ARPEntry(Base):
"""ARP 表条目 (IP -> MAC 映射)"""
__tablename__ = "arp_entries"
id = Column(Integer, primary_key=True, index=True)
device_id = Column(Integer, ForeignKey("network_devices.id"), nullable=False)
ip_address = Column(String(50), index=True, nullable=False)
mac_address = Column(String(50), index=True, nullable=False)
interface = Column(String(100), nullable=True) # 接口名称/索引
vlan_id = Column(Integer, nullable=True)
# 老化信息
age = Column(Integer, nullable=True) # 秒
is_static = Column(Boolean, default=False)
discovered_at = Column(DateTime(timezone=True), server_default=func.now())
last_seen = Column(DateTime(timezone=True), server_default=func.now())
# 关系
device = relationship("NetworkDevice", back_populates="arp_entries")
class MACAddressTable(Base):
"""MAC 地址表 (MAC -> 端口 映射)"""
__tablename__ = "mac_address_table"
id = Column(Integer, primary_key=True, index=True)
device_id = Column(Integer, ForeignKey("network_devices.id"), nullable=False)
mac_address = Column(String(50), index=True, nullable=False)
vlan_id = Column(Integer, nullable=True)
interface = Column(String(100), nullable=True) # 接口名称
port_number = Column(Integer, nullable=True) # 端口号
# 类型: learned, static, management, etc.
entry_type = Column(String(50), nullable=True)
discovered_at = Column(DateTime(timezone=True), server_default=func.now())
last_seen = Column(DateTime(timezone=True), server_default=func.now())
# 关系
device = relationship("NetworkDevice", back_populates="mac_entries")
class DeviceInterface(Base):
"""设备接口信息"""
__tablename__ = "device_interfaces"
id = Column(Integer, primary_key=True, index=True)
device_id = Column(Integer, ForeignKey("network_devices.id"), nullable=False)
if_index = Column(Integer, nullable=False) # ifIndex
if_name = Column(String(100), nullable=True) # ifName
if_descr = Column(String(255), nullable=True) # ifDescr
if_type = Column(String(50), nullable=True) # ifType
if_mtu = Column(Integer, nullable=True) # ifMtu
if_speed = Column(Integer, nullable=True) # ifSpeed (bps)
if_phys_address = Column(String(50), nullable=True) # ifPhysAddress (MAC)
if_admin_status = Column(String(50), nullable=True) # up/down/testing
if_oper_status = Column(String(50), nullable=True) # up/down/testing
# IP 地址
ip_address = Column(String(50), nullable=True)
ip_netmask = Column(String(50), nullable=True)
# 统计
if_in_octets = Column(Integer, nullable=True)
if_out_octets = Column(Integer, nullable=True)
if_in_errors = Column(Integer, nullable=True)
if_out_errors = Column(Integer, nullable=True)
last_polled_at = Column(DateTime(timezone=True), nullable=True)
# 关系
device = relationship("NetworkDevice", back_populates="interfaces")