Add asset-based monitor setup
This commit is contained in:
+88
-20
@@ -4,14 +4,42 @@ 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, Incident, Monitor, User
|
||||
from app.schemas.core import CheckResultRead, MonitorCreate, MonitorRead, MonitorUpdate, PingMonitorCreate, TcpMonitorCreate, WebsiteMonitorCreate
|
||||
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())
|
||||
@@ -23,6 +51,8 @@ def create_monitor(
|
||||
_: 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()
|
||||
@@ -36,12 +66,12 @@ def create_website_monitor(
|
||||
_: User = Depends(require_role("admin")),
|
||||
db: Session = Depends(get_db),
|
||||
) -> Monitor:
|
||||
asset_id: int | None = None
|
||||
if payload.create_asset:
|
||||
asset = Asset(name=payload.name, asset_type="website", address=payload.url, status="unknown", extra={})
|
||||
db.add(asset)
|
||||
db.flush()
|
||||
asset_id = asset.id
|
||||
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,
|
||||
@@ -86,12 +116,12 @@ def create_ping_monitor(
|
||||
_: User = Depends(require_role("admin")),
|
||||
db: Session = Depends(get_db),
|
||||
) -> Monitor:
|
||||
asset_id: int | None = None
|
||||
if payload.create_asset:
|
||||
asset = Asset(name=payload.name, asset_type="host", address=payload.host, status="unknown", extra={})
|
||||
db.add(asset)
|
||||
db.flush()
|
||||
asset_id = asset.id
|
||||
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,
|
||||
@@ -129,13 +159,13 @@ def create_tcp_monitor(
|
||||
_: User = Depends(require_role("admin")),
|
||||
db: Session = Depends(get_db),
|
||||
) -> Monitor:
|
||||
asset_id: int | None = None
|
||||
target = f"{payload.host}:{payload.port}"
|
||||
if payload.create_asset:
|
||||
asset = Asset(name=payload.name, asset_type="tcp_service", address=target, status="unknown", extra={})
|
||||
db.add(asset)
|
||||
db.flush()
|
||||
asset_id = asset.id
|
||||
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,
|
||||
@@ -167,6 +197,44 @@ def create_tcp_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)
|
||||
|
||||
@@ -60,6 +60,7 @@ class MonitorRead(MonitorCreate):
|
||||
class WebsiteMonitorCreate(BaseModel):
|
||||
name: str = Field(min_length=1, max_length=160)
|
||||
url: str = Field(min_length=1, max_length=512)
|
||||
asset_id: int | None = None
|
||||
expected_status: int = Field(default=200, ge=100, le=599)
|
||||
expected_text: str | None = None
|
||||
unexpected_text: str | None = None
|
||||
@@ -76,6 +77,7 @@ class WebsiteMonitorCreate(BaseModel):
|
||||
class PingMonitorCreate(BaseModel):
|
||||
name: str = Field(min_length=1, max_length=160)
|
||||
host: str = Field(min_length=1, max_length=255)
|
||||
asset_id: int | None = None
|
||||
timeout_seconds: int = Field(default=5, ge=1, le=60)
|
||||
interval_seconds: int = Field(default=60, ge=10)
|
||||
create_asset: bool = True
|
||||
@@ -88,6 +90,7 @@ class TcpMonitorCreate(BaseModel):
|
||||
name: str = Field(min_length=1, max_length=160)
|
||||
host: str = Field(min_length=1, max_length=255)
|
||||
port: int = Field(ge=1, le=65535)
|
||||
asset_id: int | None = None
|
||||
timeout_seconds: int = Field(default=5, ge=1, le=60)
|
||||
interval_seconds: int = Field(default=60, ge=10)
|
||||
create_asset: bool = True
|
||||
@@ -234,6 +237,14 @@ class SnmpDiscoveryItemRead(BaseModel):
|
||||
unit: str | None = None
|
||||
|
||||
|
||||
class SnmpMonitorsCreate(BaseModel):
|
||||
host: str = Field(min_length=1, max_length=255)
|
||||
asset_id: int
|
||||
credential_profile_id: int
|
||||
selected_items: list[SnmpDiscoveryItemRead] = Field(min_length=1)
|
||||
interval_seconds: int = Field(default=60, ge=10)
|
||||
|
||||
|
||||
class SnmpDiscoveryRead(BaseModel):
|
||||
host: str
|
||||
credential_profile_id: int
|
||||
|
||||
@@ -2,7 +2,8 @@ from fastapi.testclient import TestClient
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.models import AlertRule, Asset, Monitor
|
||||
from app.core.secrets import encrypt_secret
|
||||
from app.models import AlertRule, Asset, Credential, Monitor
|
||||
|
||||
|
||||
def test_create_website_monitor_creates_asset_and_alert_rule(client: TestClient, db_session: Session) -> None:
|
||||
@@ -72,3 +73,141 @@ def test_create_website_monitor_can_skip_default_alert_rule(client: TestClient,
|
||||
assert monitor is not None
|
||||
assert monitor.asset_id is None
|
||||
assert db_session.scalars(select(AlertRule).where(AlertRule.monitor_id == monitor.id)).all() == []
|
||||
|
||||
|
||||
def test_create_website_monitor_can_attach_existing_asset_without_default_alert(client: TestClient, db_session: Session) -> None:
|
||||
asset = Asset(name="Existing App", asset_type="application", address="app.example.com", status="unknown", extra={})
|
||||
db_session.add(asset)
|
||||
db_session.commit()
|
||||
|
||||
response = client.post(
|
||||
"/monitors/website",
|
||||
json={
|
||||
"name": "Existing App HTTPS",
|
||||
"url": "https://app.example.com",
|
||||
"asset_id": asset.id,
|
||||
"create_asset": True,
|
||||
"alert_enabled": False,
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
body = response.json()
|
||||
monitor = db_session.get(Monitor, body["id"])
|
||||
assert monitor is not None
|
||||
assert monitor.asset_id == asset.id
|
||||
assert db_session.scalars(select(AlertRule).where(AlertRule.monitor_id == monitor.id)).all() == []
|
||||
assert db_session.scalars(select(Asset)).all() == [asset]
|
||||
|
||||
|
||||
def test_create_ping_monitor_rejects_missing_asset(client: TestClient) -> None:
|
||||
response = client.post(
|
||||
"/monitors/ping",
|
||||
json={
|
||||
"name": "Missing Asset Ping",
|
||||
"host": "192.0.2.10",
|
||||
"asset_id": 999,
|
||||
"create_asset": False,
|
||||
"alert_enabled": False,
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
def test_create_tcp_monitor_can_attach_existing_asset(client: TestClient, db_session: Session) -> None:
|
||||
asset = Asset(name="Router", asset_type="network_device", address="192.0.2.1", status="unknown", extra={})
|
||||
db_session.add(asset)
|
||||
db_session.commit()
|
||||
|
||||
response = client.post(
|
||||
"/monitors/tcp",
|
||||
json={
|
||||
"name": "Router SSH",
|
||||
"host": "192.0.2.1",
|
||||
"port": 22,
|
||||
"asset_id": asset.id,
|
||||
"create_asset": False,
|
||||
"alert_enabled": False,
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
body = response.json()
|
||||
assert body["asset_id"] == asset.id
|
||||
assert body["target"] == "192.0.2.1:22"
|
||||
|
||||
|
||||
def test_create_snmp_monitors_from_discovery_attaches_asset_and_skips_alerts(client: TestClient, db_session: Session) -> None:
|
||||
asset = Asset(name="Core Switch", asset_type="network_device", address="192.0.2.10", status="unknown", extra={})
|
||||
profile = Credential(
|
||||
name="Core Switch Read Only",
|
||||
credential_type="snmp",
|
||||
encrypted_secret=encrypt_secret("private-community"),
|
||||
extra={"version": "2c"},
|
||||
)
|
||||
db_session.add_all([asset, profile])
|
||||
db_session.commit()
|
||||
|
||||
response = client.post(
|
||||
"/monitors/snmp/from-discovery",
|
||||
json={
|
||||
"host": "192.0.2.10",
|
||||
"asset_id": asset.id,
|
||||
"credential_profile_id": profile.id,
|
||||
"interval_seconds": 120,
|
||||
"selected_items": [
|
||||
{
|
||||
"item_id": "device.uptime",
|
||||
"item_type": "device_uptime",
|
||||
"group": "Device Health",
|
||||
"label": "Device uptime",
|
||||
"unit": "seconds",
|
||||
},
|
||||
{
|
||||
"item_id": "interface.1.status",
|
||||
"item_type": "interface_status",
|
||||
"group": "Interface uplink",
|
||||
"label": "uplink status",
|
||||
"unit": None,
|
||||
},
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
body = response.json()
|
||||
assert len(body) == 2
|
||||
assert {monitor["name"] for monitor in body} == {"Core Switch Device uptime", "Core Switch uplink status"}
|
||||
assert all(monitor["asset_id"] == asset.id for monitor in body)
|
||||
assert all(monitor["monitor_type"] == "snmp" for monitor in body)
|
||||
assert all(monitor["interval_seconds"] == 120 for monitor in body)
|
||||
assert all("1.3.6" not in str(monitor["config"]) for monitor in body)
|
||||
|
||||
monitor_ids = [monitor["id"] for monitor in body]
|
||||
assert db_session.scalars(select(AlertRule).where(AlertRule.monitor_id.in_(monitor_ids))).all() == []
|
||||
|
||||
|
||||
def test_create_snmp_monitors_rejects_missing_profile(client: TestClient, db_session: Session) -> None:
|
||||
asset = Asset(name="Core Switch", asset_type="network_device", address="192.0.2.10", status="unknown", extra={})
|
||||
db_session.add(asset)
|
||||
db_session.commit()
|
||||
|
||||
response = client.post(
|
||||
"/monitors/snmp/from-discovery",
|
||||
json={
|
||||
"host": "192.0.2.10",
|
||||
"asset_id": asset.id,
|
||||
"credential_profile_id": 999,
|
||||
"selected_items": [
|
||||
{
|
||||
"item_id": "device.uptime",
|
||||
"item_type": "device_uptime",
|
||||
"group": "Device Health",
|
||||
"label": "Device uptime",
|
||||
}
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 404
|
||||
|
||||
Reference in New Issue
Block a user