Files
2026-05-23 21:07:05 -06:00

314 lines
9.6 KiB
Python

from datetime import UTC, datetime
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy import func, select
from sqlalchemy.orm import Session
from app.api.credentials import SNMP_CREDENTIAL_TYPE
from app.auth.dependencies import get_current_user, require_role
from app.db.session import get_db
from app.models import AlertRule, Asset, CheckResult, Credential, Incident, Monitor, User
from app.schemas.core import (
CheckResultRead,
MonitorCreate,
MonitorRead,
MonitorUpdate,
PingMonitorCreate,
SnmpMonitorsCreate,
TcpMonitorCreate,
WebsiteMonitorCreate,
)
router = APIRouter(prefix="/monitors", tags=["monitors"])
def _get_asset_or_404(db: Session, asset_id: int) -> Asset:
asset = db.get(Asset, asset_id)
if asset is None:
raise HTTPException(status_code=404, detail="Asset not found")
return asset
def _resolve_asset_id(db: Session, asset_id: int | None, create_asset: bool, asset: Asset) -> int | None:
if asset_id is not None:
_get_asset_or_404(db, asset_id)
return asset_id
if not create_asset:
return None
db.add(asset)
db.flush()
return asset.id
@router.get("", response_model=list[MonitorRead])
def list_monitors(_: User = Depends(get_current_user), db: Session = Depends(get_db)) -> list[Monitor]:
return list(db.scalars(select(Monitor).order_by(Monitor.name)).all())
@router.post("", response_model=MonitorRead)
def create_monitor(
payload: MonitorCreate,
_: User = Depends(require_role("admin")),
db: Session = Depends(get_db),
) -> Monitor:
if payload.asset_id is not None:
_get_asset_or_404(db, payload.asset_id)
monitor = Monitor(**payload.model_dump())
db.add(monitor)
db.commit()
db.refresh(monitor)
return monitor
@router.post("/website", response_model=MonitorRead)
def create_website_monitor(
payload: WebsiteMonitorCreate,
_: User = Depends(require_role("admin")),
db: Session = Depends(get_db),
) -> Monitor:
asset_id = _resolve_asset_id(
db,
payload.asset_id,
payload.create_asset,
Asset(name=payload.name, asset_type="website", address=payload.url, status="unknown", extra={}),
)
monitor = Monitor(
asset_id=asset_id,
name=payload.name,
monitor_type="http",
target=payload.url,
config={
"expected_status": payload.expected_status,
"expected_text": payload.expected_text,
"unexpected_text": payload.unexpected_text,
"timeout_seconds": payload.timeout_seconds,
"check_tls_expiry": payload.check_tls_expiry,
"tls_warning_days": payload.tls_warning_days,
},
interval_seconds=payload.interval_seconds,
status="unknown",
)
db.add(monitor)
db.flush()
if payload.alert_enabled:
db.add(
AlertRule(
monitor_id=monitor.id,
name=f"{payload.name} website failure",
severity=payload.alert_severity,
condition={"type": "status_not_up"},
failure_threshold=payload.failure_threshold,
cooldown_seconds=300,
is_enabled=True,
)
)
db.commit()
db.refresh(monitor)
return monitor
@router.post("/ping", response_model=MonitorRead)
def create_ping_monitor(
payload: PingMonitorCreate,
_: User = Depends(require_role("admin")),
db: Session = Depends(get_db),
) -> Monitor:
asset_id = _resolve_asset_id(
db,
payload.asset_id,
payload.create_asset,
Asset(name=payload.name, asset_type="host", address=payload.host, status="unknown", extra={}),
)
monitor = Monitor(
asset_id=asset_id,
name=payload.name,
monitor_type="ping",
target=payload.host,
config={"timeout_seconds": payload.timeout_seconds},
interval_seconds=payload.interval_seconds,
status="unknown",
)
db.add(monitor)
db.flush()
if payload.alert_enabled:
db.add(
AlertRule(
monitor_id=monitor.id,
name=f"{payload.name} ping failure",
severity=payload.alert_severity,
condition={"type": "status_not_up"},
failure_threshold=payload.failure_threshold,
cooldown_seconds=300,
is_enabled=True,
)
)
db.commit()
db.refresh(monitor)
return monitor
@router.post("/tcp", response_model=MonitorRead)
def create_tcp_monitor(
payload: TcpMonitorCreate,
_: User = Depends(require_role("admin")),
db: Session = Depends(get_db),
) -> Monitor:
target = f"{payload.host}:{payload.port}"
asset_id = _resolve_asset_id(
db,
payload.asset_id,
payload.create_asset,
Asset(name=payload.name, asset_type="tcp_service", address=target, status="unknown", extra={}),
)
monitor = Monitor(
asset_id=asset_id,
name=payload.name,
monitor_type="tcp",
target=target,
config={"host": payload.host, "port": payload.port, "timeout_seconds": payload.timeout_seconds},
interval_seconds=payload.interval_seconds,
status="unknown",
)
db.add(monitor)
db.flush()
if payload.alert_enabled:
db.add(
AlertRule(
monitor_id=monitor.id,
name=f"{payload.name} TCP connection failure",
severity=payload.alert_severity,
condition={"type": "status_not_up"},
failure_threshold=payload.failure_threshold,
cooldown_seconds=300,
is_enabled=True,
)
)
db.commit()
db.refresh(monitor)
return monitor
@router.post("/snmp/from-discovery", response_model=list[MonitorRead])
def create_snmp_monitors_from_discovery(
payload: SnmpMonitorsCreate,
_: User = Depends(require_role("admin")),
db: Session = Depends(get_db),
) -> list[Monitor]:
asset = _get_asset_or_404(db, payload.asset_id)
profile = db.get(Credential, payload.credential_profile_id)
if profile is None or profile.credential_type != SNMP_CREDENTIAL_TYPE:
raise HTTPException(status_code=404, detail="SNMP credential profile not found")
monitors: list[Monitor] = []
for item in payload.selected_items:
monitor = Monitor(
asset_id=asset.id,
name=f"{asset.name} {item.label}",
monitor_type="snmp",
target=payload.host,
config={
"credential_profile_id": payload.credential_profile_id,
"item_id": item.item_id,
"item_type": item.item_type,
"group": item.group,
"label": item.label,
"unit": item.unit,
},
interval_seconds=payload.interval_seconds,
status="unknown",
)
db.add(monitor)
monitors.append(monitor)
db.commit()
for monitor in monitors:
db.refresh(monitor)
return monitors
@router.get("/{monitor_id}", response_model=MonitorRead)
def get_monitor(monitor_id: int, _: User = Depends(get_current_user), db: Session = Depends(get_db)) -> Monitor:
monitor = db.get(Monitor, monitor_id)
if monitor is None:
raise HTTPException(status_code=404, detail="Monitor not found")
return monitor
@router.patch("/{monitor_id}", response_model=MonitorRead)
def update_monitor(
monitor_id: int,
payload: MonitorUpdate,
_: User = Depends(require_role("admin")),
db: Session = Depends(get_db),
) -> Monitor:
monitor = db.get(Monitor, monitor_id)
if monitor is None:
raise HTTPException(status_code=404, detail="Monitor not found")
for field, value in payload.model_dump(exclude_unset=True).items():
setattr(monitor, field, value)
db.commit()
db.refresh(monitor)
return monitor
@router.delete("/{monitor_id}", status_code=204)
def delete_monitor(
monitor_id: int,
cleanup_orphan_asset: bool = True,
_: User = Depends(require_role("admin")),
db: Session = Depends(get_db),
) -> None:
monitor = db.get(Monitor, monitor_id)
if monitor is None:
raise HTTPException(status_code=404, detail="Monitor not found")
asset_id = monitor.asset_id
now = datetime.now(UTC)
open_incidents = db.scalars(select(Incident).where(Incident.monitor_id == monitor_id, Incident.status == "open")).all()
for incident in open_incidents:
incident.status = "resolved"
incident.resolved_at = now
incident.details = {**(incident.details or {}), "resolution_reason": "monitor_deleted"}
db.delete(monitor)
db.flush()
if cleanup_orphan_asset and asset_id is not None:
remaining = db.scalar(select(func.count(Monitor.id)).where(Monitor.asset_id == asset_id))
asset = db.get(Asset, asset_id)
if remaining == 0 and asset is not None and asset.asset_type in {"website", "host", "tcp_service"}:
db.delete(asset)
db.commit()
@router.get("/{monitor_id}/results", response_model=list[CheckResultRead])
def list_monitor_results(
monitor_id: int,
limit: int = 20,
_: User = Depends(get_current_user),
db: Session = Depends(get_db),
) -> list[CheckResult]:
monitor = db.get(Monitor, monitor_id)
if monitor is None:
raise HTTPException(status_code=404, detail="Monitor not found")
return list(
db.scalars(
select(CheckResult)
.where(CheckResult.monitor_id == monitor_id)
.order_by(CheckResult.observed_at.desc())
.limit(min(limit, 100))
).all()
)