This commit is contained in:
Your Name
2026-07-20 13:40:05 +08:00
parent 9e34f6f0f9
commit bca0da7c7d
12 changed files with 1609 additions and 525 deletions
+219 -129
View File
@@ -1,155 +1,245 @@
from typing import Optional, Dict, Any, List, Tuple
from typing import Optional, List
from sqlalchemy.orm import Session
from sqlalchemy import desc
from datetime import datetime
from datetime import datetime, timedelta
import json
from app.models.audit import AuditLog
from app.models.audit import AuditLog, AuditAction, AuditResource
from app.models.auth import User
class AuditService:
"""审计日志服务"""
@staticmethod
def record(
db: Session,
*,
action: str,
user: Optional[User] = None,
resource_type: Optional[str] = None,
resource_id: Optional[str] = None,
resource_name: Optional[str] = None,
method: Optional[str] = None,
path: Optional[str] = None,
ip_address: Optional[str] = None,
user_agent: Optional[str] = None,
status: str = "success",
detail: Optional[Any] = None,
) -> AuditLog:
def log(db: Session,
user: User,
action: AuditAction,
resource: AuditResource,
resource_id: Optional[str] = None,
description: Optional[str] = None,
old_value: Optional[dict] = None,
new_value: Optional[dict] = None,
changed_fields: Optional[List[str]] = None,
user_ip: Optional[str] = None,
user_agent: Optional[str] = None,
request_method: Optional[str] = None,
request_path: Optional[str] = None,
success: bool = True,
error_message: Optional[str] = None) -> AuditLog:
"""
写入一条审计日志。任意字段缺失都安全降级(不抛异常),
避免审计日志写入失败影响主业务流程。
记录审计日志
Args:
db: 数据库会话
user: 操作用户
action: 操作类型
resource: 资源类型
resource_id: 资源ID
description: 操作描述
old_value: 旧值
new_value: 新值
changed_fields: 变更的字段列表
user_ip: 用户IP
user_agent: 用户代理
request_method: 请求方法
request_path: 请求路径
success: 操作是否成功
error_message: 错误信息
Returns:
AuditLog 实例
"""
try:
log = AuditLog(
user_id=user.id if user else None,
username=user.username if user else None,
action=action,
resource_type=resource_type,
resource_id=str(resource_id) if resource_id is not None else None,
resource_name=resource_name,
method=method,
path=path,
ip_address=ip_address,
user_agent=(user_agent or "")[:500],
status=status,
detail=_serialize_detail(detail),
)
db.add(log)
db.commit()
return log
except Exception as e:
# 写审计日志失败不能影响主业务,仅回滚
db.rollback()
# 用 print 而非 logger,避免循环依赖(logger 可能未初始化)
print(f"[audit] failed to write log action={action}: {e}")
return None
audit_log = AuditLog(
user_id=user.id if user else None,
username=user.username if user else None,
real_name=user.real_name if user else None,
action=action,
resource=resource,
resource_id=str(resource_id) if resource_id else None,
description=description,
old_value=json.dumps(old_value, ensure_ascii=False, default=str) if old_value else None,
new_value=json.dumps(new_value, ensure_ascii=False, default=str) if new_value else None,
changed_fields=','.join(changed_fields) if changed_fields else None,
user_ip=user_ip,
user_agent=user_agent,
request_method=request_method,
request_path=request_path,
success=1 if success else 0,
error_message=error_message
)
db.add(audit_log)
db.commit()
db.refresh(audit_log)
return audit_log
@staticmethod
def get_logs(
db: Session,
*,
user_id: Optional[int] = None,
username: Optional[str] = None,
action: Optional[str] = None,
resource_type: Optional[str] = None,
status: Optional[str] = None,
start_time: Optional[datetime] = None,
end_time: Optional[datetime] = None,
skip: int = 0,
limit: int = 50,
) -> Tuple[int, List[AuditLog]]:
def get_logs(db: Session,
user_id: Optional[int] = None,
action: Optional[AuditAction] = None,
resource: Optional[AuditResource] = None,
success: Optional[bool] = None,
start_time: Optional[datetime] = None,
end_time: Optional[datetime] = None,
skip: int = 0,
limit: int = 100) -> tuple[int, List[AuditLog]]:
"""
查询审计日志
Args:
db: 数据库会话
user_id: 按用户ID过滤
action: 按操作类型过滤
resource: 按资源类型过滤
success: 按操作结果过滤
start_time: 开始时间
end_time: 结束时间
skip: 跳过记录数
limit: 返回记录数
Returns:
(总数, 记录列表)
"""
query = db.query(AuditLog)
if user_id is not None:
if user_id:
query = query.filter(AuditLog.user_id == user_id)
if username:
query = query.filter(AuditLog.username.like(f"%{username}%"))
if action:
query = query.filter(AuditLog.action.like(f"%{action}%"))
if resource_type:
query = query.filter(AuditLog.resource_type == resource_type)
if status:
query = query.filter(AuditLog.status == status)
query = query.filter(AuditLog.action == action)
if resource:
query = query.filter(AuditLog.resource == resource)
if success is not None:
query = query.filter(AuditLog.success == (1 if success else 0))
if start_time:
query = query.filter(AuditLog.created_at >= start_time)
if end_time:
query = query.filter(AuditLog.created_at <= end_time)
total = query.count()
items = query.order_by(desc(AuditLog.created_at)).offset(skip).limit(limit).all()
items = query.order_by(AuditLog.created_at.desc()).offset(skip).limit(limit).all()
return total, items
@staticmethod
def get_stats(db: Session) -> Dict[str, Any]:
"""首页/汇总用:最近 24h 操作数 + 按 action 分组"""
from datetime import timedelta
cutoff = datetime.utcnow() - timedelta(hours=24)
recent = db.query(AuditLog).filter(AuditLog.created_at >= cutoff).count()
by_action = db.query(AuditLog.action, db.query(AuditLog).filter(AuditLog.created_at >= cutoff).subquery()) # 占位避免循环
# 简化:用 group by
from sqlalchemy import func
rows = db.query(AuditLog.action, func.count(AuditLog.id)).filter(
AuditLog.created_at >= cutoff
).group_by(AuditLog.action).all()
def get_user_actions(db: Session, user_id: int, limit: int = 50) -> List[AuditLog]:
"""获取指定用户的操作日志"""
return db.query(AuditLog)\
.filter(AuditLog.user_id == user_id)\
.order_by(AuditLog.created_at.desc())\
.limit(limit)\
.all()
@staticmethod
def get_resource_logs(db: Session, resource: AuditResource, resource_id: str, limit: int = 50) -> List[AuditLog]:
"""获取指定资源的操作历史"""
return db.query(AuditLog)\
.filter(AuditLog.resource == resource, AuditLog.resource_id == str(resource_id))\
.order_by(AuditLog.created_at.desc())\
.limit(limit)\
.all()
@staticmethod
def get_statistics(db: Session, days: int = 30) -> dict:
"""
获取审计统计信息
Args:
db: 数据库会话
days: 统计天数
Returns:
统计信息字典
"""
start_date = datetime.utcnow() - timedelta(days=days)
# 总操作数
total_ops = db.query(AuditLog).filter(AuditLog.created_at >= start_date).count()
# 成功/失败统计
success_ops = db.query(AuditLog).filter(
AuditLog.created_at >= start_date,
AuditLog.success == 1
).count()
failed_ops = total_ops - success_ops
# 按操作类型统计
action_stats = {}
for action in AuditAction:
count = db.query(AuditLog).filter(
AuditLog.created_at >= start_date,
AuditLog.action == action
).count()
if count > 0:
action_stats[action.value] = count
# 按资源类型统计
resource_stats = {}
for resource in AuditResource:
count = db.query(AuditLog).filter(
AuditLog.created_at >= start_date,
AuditLog.resource == resource
).count()
if count > 0:
resource_stats[resource.value] = count
# 活跃用户数
active_users = db.query(AuditLog.user_id).filter(
AuditLog.created_at >= start_date
).distinct().count()
return {
"recent_24h": recent,
"by_action_24h": {action: count for action, count in rows},
"period_days": days,
"total_operations": total_ops,
"successful_operations": success_ops,
"failed_operations": failed_ops,
"success_rate": round(success_ops / total_ops * 100, 2) if total_ops > 0 else 0,
"by_action": action_stats,
"by_resource": resource_stats,
"active_users_count": active_users
}
@staticmethod
def clean_old_logs(db: Session, days: int = 90) -> int:
"""
清理指定天数之前的日志
Args:
db: 数据库会话
days: 保留天数
Returns:
删除的记录数
"""
cutoff_date = datetime.utcnow() - timedelta(days=days)
deleted = db.query(AuditLog).filter(AuditLog.created_at < cutoff_date).delete()
db.commit()
return deleted
def _serialize_detail(detail: Any) -> Optional[str]:
"""把 dict/list 等结构化数据序列化成字符串,便于存储和展示"""
if detail is None:
return None
if isinstance(detail, str):
return detail[:4000]
try:
return json.dumps(detail, ensure_ascii=False, default=str)[:4000]
except (TypeError, ValueError):
return str(detail)[:4000]
# 常用 action 字符串集中管理(避免散落各处的魔法字符串)
class AuditAction:
# 网络
NETWORK_CREATE = "network.create"
NETWORK_UPDATE = "network.update"
NETWORK_DELETE = "network.delete"
# IP
IP_UPDATE = "ip.update"
IP_RESERVE = "ip.reserve"
IP_UNRESERVE = "ip.unreserve"
# 扫描
SCAN_TRIGGER = "scan.trigger"
# SNMP
SNMP_CRED_CREATE = "snmp.credential.create"
SNMP_CRED_UPDATE = "snmp.credential.update"
SNMP_CRED_DELETE = "snmp.credential.delete"
SNMP_DEVICE_CREATE = "snmp.device.create"
SNMP_DEVICE_UPDATE = "snmp.device.update"
SNMP_DEVICE_DELETE = "snmp.device.delete"
# 告警
ALERT_ACK = "alert.acknowledge"
ALERT_RESOLVE = "alert.resolve"
# 用户管理
USER_LOGIN = "user.login"
USER_LOGIN_FAILED = "user.login.failed"
USER_LOGOUT = "user.logout"
USER_CREATE = "user.create"
USER_UPDATE_STATUS = "user.update_status"
USER_UNLOCK = "user.unlock"
USER_RESET_PASSWORD = "user.reset_password"
USER_DELETE = "user.delete"
USER_CHANGE_PASSWORD = "user.change_password"
class AuditLogger:
"""审计日志装饰器/上下文管理器"""
def __init__(self, db: Session, user: User, resource: AuditResource, resource_id: Optional[str] = None):
self.db = db
self.user = user
self.resource = resource
self.resource_id = resource_id
self.old_value = None
def set_old_value(self, value: dict):
"""设置旧值(用于更新操作)"""
self.old_value = value
def log(self, action: AuditAction, description: str, new_value: Optional[dict] = None, changed_fields: Optional[List[str]] = None):
"""记录日志"""
AuditService.log(
db=self.db,
user=self.user,
action=action,
resource=self.resource,
resource_id=self.resource_id,
description=description,
old_value=self.old_value,
new_value=new_value,
changed_fields=changed_fields
)
+239
View File
@@ -0,0 +1,239 @@
from pydantic_settings import BaseSettings
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
import smtplib
from typing import Optional, List
from datetime import datetime
import logging
logger = logging.getLogger(__name__)
class EmailSettings(BaseSettings):
"""邮件配置"""
SMTP_HOST: str = "localhost"
SMTP_PORT: int = 25
SMTP_USER: Optional[str] = None
SMTP_PASSWORD: Optional[str] = None
SMTP_USE_TLS: bool = False
SMTP_USE_SSL: bool = False
SMTP_FROM: str = "ipam@localhost"
class Config:
env_prefix = "EMAIL_"
class EmailService:
"""邮件服务"""
def __init__(self, settings: EmailSettings):
self.settings = settings
def send_email(self, to: str, subject: str, html_content: str) -> bool:
"""
发送HTML邮件
Args:
to: 收件人邮箱
subject: 邮件主题
html_content: HTML内容
Returns:
是否发送成功
"""
try:
msg = MIMEMultipart('alternative')
msg['From'] = self.settings.SMTP_FROM
msg['To'] = to
msg['Subject'] = subject
html_part = MIMEText(html_content, 'html', 'utf-8')
msg.attach(html_part)
# 连接SMTP服务器
if self.settings.SMTP_USE_SSL:
server = smtplib.SMTP_SSL(
host=self.settings.SMTP_HOST,
port=self.settings.SMTP_PORT,
timeout=10
)
else:
server = smtplib.SMTP(
host=self.settings.SMTP_HOST,
port=self.settings.SMTP_PORT,
timeout=10
)
# 如果需要TLS加密
if self.settings.SMTP_USE_TLS and not self.settings.SMTP_USE_SSL:
server.starttls()
# 如果需要认证
if self.settings.SMTP_USER and self.settings.SMTP_PASSWORD:
server.login(self.settings.SMTP_USER, self.settings.SMTP_PASSWORD)
# 发送邮件
server.send_message(msg)
server.quit()
logger.info(f"邮件发送成功: {to} - {subject}")
return True
except Exception as e:
logger.error(f"邮件发送失败: {str(e)}", exc_info=True)
return False
def send_alert_notification(self, to: str, alert_data: dict) -> bool:
"""发送告警通知邮件"""
severity_colors = {
'critical': '#dc2626',
'error': '#ef4444',
'warning': '#f59e0b',
'info': '#3b82f6'
}
severity_texts = {
'critical': '严重',
'error': '错误',
'warning': '警告',
'info': '信息'
}
alert_type_texts = {
'ip_conflict': 'IP 地址冲突',
'unauthorized_access': '未授权设备接入',
'subnet_full': '网段容量不足',
'device_offline': '设备离线',
'new_device_detected': '新设备发现',
'mac_changed': 'MAC 地址变更'
}
severity = alert_data.get('severity', 'warning')
color = severity_colors.get(severity, '#6b7280')
severity_text = severity_texts.get(severity, severity)
alert_type_text = alert_type_texts.get(alert_data.get('alert_type'), alert_data.get('alert_type'))
html_content = f"""
<html>
<head>
<meta charset="UTF-8">
<style>
body {{ font-family: Arial, sans-serif; line-height: 1.6; color: #333; }}
.header {{ background-color: {color}; color: white; padding: 20px; border-radius: 5px; }}
.content {{ padding: 20px; background-color: #f9fafb; border-radius: 5px; margin-top: 10px; }}
.alert-item {{ margin-bottom: 15px; }}
.label {{ font-weight: bold; color: #6b7280; display: inline-block; width: 100px; }}
.footer {{ margin-top: 20px; padding-top: 20px; border-top: 1px solid #e5e7eb; font-size: 12px; color: #9ca3af; }}
</style>
</head>
<body>
<div class="header">
<h2>⚠️ IPAM 告警通知</h2>
<p>告警级别: <strong>{severity_text}</strong></p>
</div>
<div class="content">
<div class="alert-item">
<span class="label">告警类型:</span> {alert_type_text}
</div>
<div class="alert-item">
<span class="label">告警标题:</span> {alert_data.get('title', '-')}
</div>
<div class="alert-item">
<span class="label">详细信息:</span> {alert_data.get('message', '-')}
</div>
<div class="alert-item">
<span class="label">相关IP:</span> {alert_data.get('ip_address_str', '-')}
</div>
<div class="alert-item">
<span class="label">相关MAC:</span> {alert_data.get('mac_address', '-')}
</div>
<div class="alert-item">
<span class="label">告警时间:</span> {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
</div>
</div>
<div class="footer">
<p>此邮件由 IPAM 地址管理系统自动发送,请勿直接回复。</p>
<p>如需处理该告警,请登录 IPAM 管理控制台: <a href="#">点击访问</a></p>
</div>
</body>
</html>
"""
subject = f"{severity_text}】IPAM 告警 - {alert_data.get('title', '新告警')}"
return self.send_email(to, subject, html_content)
def send_scan_report(self, to: str, report_data: dict) -> bool:
"""发送扫描报告邮件"""
html_content = f"""
<html>
<head>
<meta charset="UTF-8">
<style>
body {{ font-family: Arial, sans-serif; line-height: 1.6; color: #333; }}
.header {{ background-color: #3b82f6; color: white; padding: 20px; border-radius: 5px; }}
.content {{ padding: 20px; background-color: #f9fafb; border-radius: 5px; margin-top: 10px; }}
.stat-item {{ display: inline-block; width: 22%; text-align: center; padding: 15px; background: white; border-radius: 5px; margin: 1%; }}
.stat-number {{ font-size: 28px; font-weight: bold; color: #3b82f6; }}
.stat-label {{ color: #6b7280; font-size: 14px; }}
.success {{ color: #10b981; }}
.warning {{ color: #f59e0b; }}
.footer {{ margin-top: 20px; padding-top: 20px; border-top: 1px solid #e5e7eb; font-size: 12px; color: #9ca3af; }}
</style>
</head>
<body>
<div class="header">
<h2>📊 IPAM 网段扫描报告</h2>
</div>
<div class="content">
<h3>扫描结果概览</h3>
<div class="stat-item">
<div class="stat-number">{report_data.get('total_ips', 0)}</div>
<div class="stat-label">扫描IP总数</div>
</div>
<div class="stat-item">
<div class="stat-number success">{report_data.get('online_ips', 0)}</div>
<div class="stat-label">在线设备</div>
</div>
<div class="stat-item">
<div class="stat-number">{report_data.get('with_mac', 0)}</div>
<div class="stat-label">发现MAC地址</div>
</div>
<div class="stat-item">
<div class="stat-number warning">{report_data.get('new_devices', 0)}</div>
<div class="stat-label">新发现设备</div>
</div>
<div style="clear: both; margin-top: 20px;">
<p><strong>扫描网段:</strong> {report_data.get('network_cidr', '-')}</p>
<p><strong>扫描时间:</strong> {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>
</div>
</div>
<div class="footer">
<p>此邮件由 IPAM 地址管理系统自动发送,请勿直接回复。</p>
</div>
</body>
</html>
"""
subject = f"IPAM 扫描报告 - {report_data.get('network_cidr', '')}"
return self.send_email(to, subject, html_content)
# 全局邮件服务实例
_email_service = None
def get_email_service() -> EmailService:
"""获取邮件服务实例"""
global _email_service
if _email_service is None:
settings = EmailSettings()
_email_service = EmailService(settings)
return _email_service
def init_email_service(settings: EmailSettings):
"""初始化邮件服务"""
global _email_service
_email_service = EmailService(settings)
+277
View File
@@ -0,0 +1,277 @@
from sqlalchemy.orm import Session
from typing import Optional, List, Dict, Any
from io import BytesIO, StringIO
import csv
from datetime import datetime
from app.models.network import IPAddress, Network, IPStatus
from app.models.snmp import NetworkDevice, ARPEntry
from app.models.alert import Alert
class ReportExportService:
"""报表导出服务"""
# ========== CSV 导出 ==========
@staticmethod
def export_ip_addresses_csv(db: Session,
network_id: Optional[int] = None,
status: Optional[IPStatus] = None,
include_online_only: bool = False) -> tuple[str, str]:
"""
导出IP地址表为CSV
Args:
db: 数据库会话
network_id: 网段ID(可选,不指定则导出所有)
status: IP状态过滤
include_online_only: 仅导出在线IP
Returns:
(CSV内容字符串, 文件名)
"""
query = db.query(IPAddress)
if network_id:
query = query.filter(IPAddress.network_id == network_id)
if status:
query = query.filter(IPAddress.status == status)
if include_online_only:
query = query.filter(IPAddress.status == IPStatus.ONLINE)
ips = query.order_by(IPAddress.network_id, IPAddress.id).all()
output = StringIO()
writer = csv.writer(output)
# 写入表头
writer.writerow([
'ID', '网段ID', 'IP地址', 'MAC地址', '主机名', '状态',
'使用者', '业务类型', '备注', '厂商',
'最后发现时间', '创建时间'
])
# 写入数据
for ip in ips:
writer.writerow([
ip.id,
ip.network_id,
ip.ip_address,
ip.mac_address or '',
ip.hostname or '',
ip.status.value,
ip.owner or '',
ip.business_type or '',
ip.notes or '',
ip.vendor or '',
ip.last_seen.strftime('%Y-%m-%d %H:%M:%S') if ip.last_seen else '',
ip.created_at.strftime('%Y-%m-%d %H:%M:%S') if ip.created_at else ''
])
csv_content = output.getvalue()
filename = f"ip_addresses_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
return csv_content, filename
@staticmethod
def export_networks_csv(db: Session) -> tuple[str, str]:
"""导出网段汇总CSV"""
networks = db.query(Network).order_by(Network.id).all()
output = StringIO()
writer = csv.writer(output)
writer.writerow([
'ID', '网段CIDR', '名称', '描述', '分组',
'网关', 'VLAN', '总IP数', '已用IP数', '保留IP数',
'创建时间'
])
for net in networks:
writer.writerow([
net.id,
net.cidr,
net.name or '',
net.description or '',
net.group_name or '',
net.gateway or '',
net.vlan_id or '',
net.total_ips,
net.used_ips,
net.reserved_ips,
net.created_at.strftime('%Y-%m-%d %H:%M:%S') if net.created_at else ''
])
csv_content = output.getvalue()
filename = f"networks_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
return csv_content, filename
@staticmethod
def export_alerts_csv(db: Session, status: Optional[str] = None) -> tuple[str, str]:
"""导出告警CSV"""
query = db.query(Alert)
if status:
query = query.filter(Alert.status == status)
alerts = query.order_by(Alert.created_at.desc()).all()
output = StringIO()
writer = csv.writer(output)
writer.writerow([
'ID', '告警类型', '严重程度', '状态', '标题',
'详细信息', '相关IP', '相关MAC', '创建时间'
])
for alert in alerts:
writer.writerow([
alert.id,
alert.alert_type.value,
alert.severity.value,
alert.status.value,
alert.title,
alert.message or '',
alert.ip_address_str or '',
alert.mac_address or '',
alert.created_at.strftime('%Y-%m-%d %H:%M:%S') if alert.created_at else ''
])
csv_content = output.getvalue()
filename = f"alerts_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
return csv_content, filename
@staticmethod
def export_snmp_devices_csv(db: Session) -> tuple[str, str]:
"""导出SNMP设备CSV"""
devices = db.query(NetworkDevice).order_by(NetworkDevice.id).all()
output = StringIO()
writer = csv.writer(output)
writer.writerow([
'ID', '设备名称', 'IP地址', '端口', '设备类型',
'厂商', '型号', '位置', '最后轮询时间', '创建时间'
])
for dev in devices:
writer.writerow([
dev.id,
dev.name,
dev.ip_address,
dev.port,
dev.device_type.value,
dev.vendor or '',
dev.model or '',
dev.location or '',
dev.last_polled_at.strftime('%Y-%m-%d %H:%M:%S') if dev.last_polled_at else '',
dev.created_at.strftime('%Y-%m-%d %H:%M:%S') if dev.created_at else ''
])
csv_content = output.getvalue()
filename = f"snmp_devices_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
return csv_content, filename
@staticmethod
def export_arp_table_csv(db: Session, device_id: Optional[int] = None) -> tuple[str, str]:
"""导出ARP表CSV"""
query = db.query(ARPEntry)
if device_id:
query = query.filter(ARPEntry.device_id == device_id)
entries = query.order_by(ARPEntry.device_id, ARPEntry.id).all()
output = StringIO()
writer = csv.writer(output)
writer.writerow([
'ID', '设备ID', 'IP地址', 'MAC地址', '接口', '最后发现时间'
])
for entry in entries:
writer.writerow([
entry.id,
entry.device_id,
entry.ip_address,
entry.mac_address,
entry.interface or '',
entry.last_seen.strftime('%Y-%m-%d %H:%M:%S') if entry.last_seen else ''
])
csv_content = output.getvalue()
filename = f"arp_table_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
return csv_content, filename
# ========== 汇总统计报表 ==========
@staticmethod
def get_ipam_summary(db: Session) -> Dict[str, Any]:
"""获取IPAM系统汇总报表"""
# 网段统计
total_networks = db.query(Network).count()
total_ips = db.query(IPAddress).count()
online_ips = db.query(IPAddress).filter(IPAddress.status == IPStatus.ONLINE).count()
reserved_ips = db.query(IPAddress).filter(IPAddress.status == IPStatus.RESERVED).count()
# 带MAC/主机名的IP统计
with_mac = db.query(IPAddress).filter(IPAddress.mac_address.isnot(None)).count()
with_hostname = db.query(IPAddress).filter(IPAddress.hostname.isnot(None)).count()
# 网段使用率分布
networks = db.query(Network).all()
usage_distribution = {
'under_50': 0, # 50%以下
'50_70': 0, # 50%-70%
'70_90': 0, # 70%-90%
'over_90': 0 # 90%以上
}
for net in networks:
usage_rate = (net.used_ips / net.total_ips * 100) if net.total_ips > 0 else 0
if usage_rate < 50:
usage_distribution['under_50'] += 1
elif usage_rate < 70:
usage_distribution['50_70'] += 1
elif usage_rate < 90:
usage_distribution['70_90'] += 1
else:
usage_distribution['over_90'] += 1
# 告警统计
from app.models.alert import Alert, AlertStatus
active_alerts = db.query(Alert).filter(Alert.status == AlertStatus.ACTIVE).count()
resolved_alerts = db.query(Alert).filter(Alert.status == AlertStatus.RESOLVED).count()
# SNMP设备统计
snmp_devices = db.query(NetworkDevice).count()
active_devices = db.query(NetworkDevice).filter(NetworkDevice.last_polled_at.isnot(None)).count()
return {
"summary": {
"total_networks": total_networks,
"total_ip_addresses": total_ips,
"online_ips": online_ips,
"reserved_ips": reserved_ips,
"available_ips": total_ips - online_ips - reserved_ips,
"online_rate": round(online_ips / total_ips * 100, 2) if total_ips > 0 else 0
},
"discovery": {
"with_mac_address": with_mac,
"with_hostname": with_hostname,
"mac_discovery_rate": round(with_mac / total_ips * 100, 2) if total_ips > 0 else 0
},
"network_usage_distribution": usage_distribution,
"alerts": {
"active": active_alerts,
"resolved": resolved_alerts
},
"snmp": {
"total_devices": snmp_devices,
"active_devices": active_devices
},
"generated_at": datetime.now().strftime('%Y-%m-%d %H:%M:%S')
}