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
Binary file not shown.
Binary file not shown.
Binary file not shown.
+120
View File
@@ -0,0 +1,120 @@
from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, Enum, Boolean, Float
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.core.database import Base
import enum
class AlertType(str, enum.Enum):
"""告警类型"""
IP_CONFLICT = "ip_conflict" # IP 冲突
UNAUTHORIZED_ACCESS = "unauthorized_access" # 未授权接入
SUBNET_FULL = "subnet_full" # 网段耗尽
SCAN_FAILED = "scan_failed" # 扫描失败
DEVICE_OFFLINE = "device_offline" # 设备离线
NEW_DEVICE_DETECTED = "new_device_detected" # 新设备发现
MAC_CHANGED = "mac_changed" # MAC 地址变化
class AlertSeverity(str, enum.Enum):
"""告警级别"""
INFO = "info" # 信息
WARNING = "warning" # 警告
ERROR = "error" # 错误
CRITICAL = "critical" # 严重
class AlertStatus(str, enum.Enum):
"""告警状态"""
ACTIVE = "active" # 活跃
ACKNOWLEDGED = "acknowledged" # 已确认
RESOLVED = "resolved" # 已解决
IGNORED = "ignored" # 已忽略
class Alert(Base):
"""告警"""
__tablename__ = "alerts"
id = Column(Integer, primary_key=True, index=True)
alert_type = Column(Enum(AlertType), nullable=False, index=True)
severity = Column(Enum(AlertSeverity), default=AlertSeverity.WARNING, index=True)
status = Column(Enum(AlertStatus), default=AlertStatus.ACTIVE, index=True)
title = Column(String(255), nullable=False)
message = Column(Text)
# 关联对象
network_id = Column(Integer, ForeignKey("networks.id"), nullable=True)
ip_address_id = Column(Integer, ForeignKey("ip_addresses.id"), nullable=True)
device_id = Column(Integer, ForeignKey("network_devices.id"), nullable=True)
ip_address_str = Column(String(50), nullable=True) # 冗余存储便于查询
# 关联的 IP/MAC 信息
mac_address = Column(String(50), nullable=True)
conflicting_mac = Column(String(50), nullable=True) # 用于 IP 冲突告警
# 统计信息
usage_percent = Column(Float, nullable=True) # 网段使用率告警时存储
# 处理信息
acknowledged_by = Column(String(100), nullable=True)
acknowledged_at = Column(DateTime(timezone=True), nullable=True)
resolved_by = Column(String(100), nullable=True)
resolved_at = Column(DateTime(timezone=True), nullable=True)
resolution_notes = Column(Text, nullable=True)
created_at = Column(DateTime(timezone=True), server_default=func.now(), index=True)
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
# 关系
network = relationship("Network")
ip_address = relationship("IPAddress")
device = relationship("NetworkDevice")
class AlertRule(Base):
"""告警规则配置"""
__tablename__ = "alert_rules"
id = Column(Integer, primary_key=True, index=True)
name = Column(String(100), unique=True, nullable=False)
description = Column(Text, nullable=True)
alert_type = Column(Enum(AlertType), nullable=False)
severity = Column(Enum(AlertSeverity), default=AlertSeverity.WARNING)
# 规则条件
enabled = Column(Boolean, default=True)
# 阈值配置
subnet_usage_threshold = Column(Float, default=90.0) # 网段使用率阈值 %
scan_failure_threshold = Column(Integer, default=3) # 扫描失败次数阈值
inactivity_threshold_minutes = Column(Integer, default=60) # 设备不活动阈值(分钟)
# MAC 白名单(用于未授权接入检测)
mac_whitelist_enabled = Column(Boolean, default=False)
mac_whitelist = Column(Text, nullable=True) # JSON 格式的 MAC 列表
# 通知配置
notify_email = Column(Boolean, default=False)
notify_webhook = Column(Boolean, default=False)
notification_channels = Column(Text, nullable=True) # JSON 格式的通知渠道
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
class WhitelistedMAC(Base):
"""MAC 地址白名单"""
__tablename__ = "whitelisted_macs"
id = Column(Integer, primary_key=True, index=True)
mac_address = Column(String(50), unique=True, nullable=False, index=True)
description = Column(String(255), nullable=True)
owner = Column(String(100), nullable=True)
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())
+37
View File
@@ -0,0 +1,37 @@
from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, Index
from sqlalchemy.sql import func
from app.core.database import Base
class AuditLog(Base):
"""审计日志:记录用户的关键操作"""
__tablename__ = "audit_logs"
id = Column(Integer, primary_key=True, index=True)
# 操作人(nullable:登录失败、token 失效等场景可能没有 user_id)
user_id = Column(Integer, ForeignKey("users.id", ondelete="SET NULL"), nullable=True, index=True)
username = Column(String(50), index=True, comment="冗余存储用户名,便于 user 被删除后仍能查看")
# 操作内容
action = Column(String(50), nullable=False, index=True, comment="操作类型,如:network.create")
resource_type = Column(String(50), nullable=True, index=True, comment="资源类型:network / ip / snmp / user / scan")
resource_id = Column(String(50), nullable=True, comment="资源 ID(字符串以便兼容多种类型)")
resource_name = Column(String(200), nullable=True, comment="资源名称/标识,便于阅读")
# 请求信息
method = Column(String(10), comment="HTTP 方法")
path = Column(String(500), comment="请求路径")
ip_address = Column(String(50), comment="客户端 IP")
user_agent = Column(String(500), comment="User-Agent")
# 状态
status = Column(String(20), default="success", index=True, comment="success / failed")
detail = Column(Text, comment="变更详情 / 错误信息 / JSON diff")
created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False, index=True)
# 复合索引:按时间+用户查、按时间+资源查更高效
Index("idx_audit_logs_user_created", AuditLog.user_id, AuditLog.created_at.desc())
Index("idx_audit_logs_resource", AuditLog.resource_type, AuditLog.resource_id, AuditLog.created_at.desc())
+97
View File
@@ -0,0 +1,97 @@
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Enum, Text
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.core.database import Base
import enum
class UserRole(str, enum.Enum):
"""用户角色枚举"""
SUPER_ADMIN = "super_admin" # 超级管理员 - 所有权限
ADMIN = "admin" # 管理员 - 除用户管理外的所有权限
OPERATOR = "operator" # 操作员 - 可查看和操作,不能配置
VIEWER = "viewer" # 只读用户 - 只能查看
class UserStatus(str, enum.Enum):
"""用户状态"""
ACTIVE = "active" # 正常
INACTIVE = "inactive" # 禁用
LOCKED = "locked" # 锁定(登录失败过多)
class User(Base):
"""用户表"""
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
username = Column(String(50), unique=True, index=True, nullable=False)
email = Column(String(100), unique=True, index=True)
phone = Column(String(20), index=True)
real_name = Column(String(50), comment="真实姓名")
password_hash = Column(String(255), nullable=False)
role = Column(Enum(UserRole), default=UserRole.VIEWER, nullable=False)
status = Column(Enum(UserStatus), default=UserStatus.ACTIVE, nullable=False)
# 登录信息
last_login_at = Column(DateTime(timezone=True))
last_login_ip = Column(String(50))
login_failed_count = Column(Integer, default=0)
# 通知配置
email_notification = Column(Boolean, default=True)
wechat_notification = Column(Boolean, default=False)
dingtalk_notification = Column(Boolean, default=False)
created_by = Column(Integer, ForeignKey("users.id"), nullable=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
# 关系
creator = relationship("User", remote_side=[id])
class Permission(Base):
"""权限项"""
__tablename__ = "permissions"
id = Column(Integer, primary_key=True, index=True)
code = Column(String(100), unique=True, nullable=False, comment="权限代码,如:network:create")
name = Column(String(100), nullable=False, comment="权限名称")
description = Column(Text)
category = Column(String(50), comment="权限分类:network, ip, scan, snmp, alert, system")
created_at = Column(DateTime(timezone=True), server_default=func.now())
class RolePermission(Base):
"""角色权限关联表"""
__tablename__ = "role_permissions"
id = Column(Integer, primary_key=True, index=True)
role = Column(Enum(UserRole), nullable=False)
permission_id = Column(Integer, ForeignKey("permissions.id"), nullable=False)
created_at = Column(DateTime(timezone=True), server_default=func.now())
class RefreshToken(Base):
"""刷新令牌"""
__tablename__ = "refresh_tokens"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
token = Column(String(500), unique=True, nullable=False)
expires_at = Column(DateTime(timezone=True), nullable=False)
ip_address = Column(String(50))
user_agent = Column(String(500))
revoked = Column(Boolean, default=False)
revoked_at = Column(DateTime(timezone=True))
created_at = Column(DateTime(timezone=True), server_default=func.now())
# 关系
user = relationship("User")
+119
View File
@@ -0,0 +1,119 @@
from sqlalchemy import Column, Integer, String, Text, DateTime, Enum, ForeignKey, JSON, Boolean
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.core.database import Base
import enum
class NetworkGroup(str, enum.Enum):
"""网段分组枚举"""
PRODUCTION = "production"
OFFICE = "office"
TEST = "test"
MANAGEMENT = "management"
OTHER = "other"
class IPStatus(str, enum.Enum):
"""IP状态枚举"""
AVAILABLE = "available"
ONLINE = "online"
OFFLINE = "offline"
RESERVED = "reserved"
class TaskStatus(str, enum.Enum):
"""任务状态枚举"""
PENDING = "pending"
RUNNING = "running"
COMPLETED = "completed"
FAILED = "failed"
class TaskType(str, enum.Enum):
"""任务类型枚举"""
PING = "ping"
ARP = "arp"
SNMP = "snmp"
class Network(Base):
"""网段模型"""
__tablename__ = "networks"
id = Column(Integer, primary_key=True, index=True)
cidr = Column(String(50), unique=True, index=True, nullable=False) # 如: 192.168.1.0/24
name = Column(String(100), index=True)
description = Column(Text, nullable=True)
group_name = Column(String(100), index=True, nullable=True) # 分组名称
gateway = Column(String(50), nullable=True)
vlan_id = Column(Integer, nullable=True)
total_ips = Column(Integer, default=0)
used_ips = Column(Integer, default=0)
reserved_ips = Column(Integer, default=0)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
# 关系
ip_addresses = relationship("IPAddress", back_populates="network", cascade="all, delete-orphan")
scan_tasks = relationship("ScanTask", back_populates="network")
class IPAddress(Base):
"""IP地址模型"""
__tablename__ = "ip_addresses"
id = Column(Integer, primary_key=True, index=True)
network_id = Column(Integer, ForeignKey("networks.id"), nullable=False)
ip_address = Column(String(50), unique=True, index=True, nullable=False)
mac_address = Column(String(50), nullable=True, index=True)
hostname = Column(String(255), nullable=True)
status = Column(Enum(IPStatus), default=IPStatus.AVAILABLE)
# 业务属性
owner = Column(String(100), nullable=True)
business_type = Column(String(50), nullable=True)
notes = Column(Text, nullable=True)
custom_fields = Column(JSON, default=dict)
# 扫描信息
last_seen = Column(DateTime(timezone=True), nullable=True)
first_seen = Column(DateTime(timezone=True), server_default=func.now())
# 物理位置信息
switch_name = Column(String(100), nullable=True)
switch_port = Column(String(50), nullable=True)
vendor = Column(String(100), nullable=True) # MAC OUI 解析的厂商
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
# 关系
network = relationship("Network", back_populates="ip_addresses")
class ScanTask(Base):
"""扫描任务模型"""
__tablename__ = "scan_tasks"
id = Column(Integer, primary_key=True, index=True)
network_id = Column(Integer, ForeignKey("networks.id"), nullable=True)
task_type = Column(Enum(TaskType), nullable=False)
status = Column(Enum(TaskStatus), default=TaskStatus.PENDING)
progress = Column(Integer, default=0) # 0-100
celery_task_id = Column(String(100), nullable=True)
# 统计信息
total_count = Column(Integer, default=0)
success_count = Column(Integer, default=0)
failed_count = Column(Integer, default=0)
started_at = Column(DateTime(timezone=True), nullable=True)
completed_at = Column(DateTime(timezone=True), nullable=True)
error_message = Column(Text, nullable=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
# 关系
network = relationship("Network", back_populates="scan_tasks")
+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")