f9d56621f6
Co-authored-by: Cursor <cursoragent@cursor.com>
177 lines
5.7 KiB
Python
177 lines
5.7 KiB
Python
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
from pydantic import BaseModel
|
|
from sqlalchemy import func, select
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.auth.jwt_auth import get_current_user, require_admin
|
|
from app.config import get_settings
|
|
from app.database import get_db
|
|
from app.models import Event, Host
|
|
from app.schemas.list_models import HostDetail, HostListResponse, HostSummary
|
|
from app.services.agent_version import latest_agent_versions_by_product
|
|
from app.services.host_delete import delete_host_and_related
|
|
from app.services.host_health import (
|
|
HEARTBEAT_TYPE,
|
|
agent_status as compute_agent_status,
|
|
apply_agent_status_host_filter,
|
|
max_daily_report_time_by_host,
|
|
max_event_time_by_host,
|
|
)
|
|
|
|
router = APIRouter(prefix="/hosts", tags=["hosts"])
|
|
|
|
|
|
@router.get("", response_model=HostListResponse)
|
|
def list_hosts(
|
|
page: int = Query(1, ge=1),
|
|
page_size: int = Query(50, ge=1, le=200),
|
|
product: str | None = None,
|
|
hostname: str | None = None,
|
|
agent_status: str | None = Query(None, pattern="^(online|stale|unknown)$"),
|
|
db: Session = Depends(get_db),
|
|
_user: str = Depends(get_current_user),
|
|
) -> HostListResponse:
|
|
stmt = select(Host)
|
|
count_stmt = select(func.count()).select_from(Host)
|
|
|
|
if product:
|
|
stmt = stmt.where(Host.product == product)
|
|
count_stmt = count_stmt.where(Host.product == product)
|
|
if hostname:
|
|
like = f"%{hostname}%"
|
|
host_match = Host.hostname.ilike(like) | Host.display_name.ilike(like)
|
|
stmt = stmt.where(host_match)
|
|
count_stmt = count_stmt.where(host_match)
|
|
|
|
settings = get_settings()
|
|
if agent_status:
|
|
stmt, count_stmt = apply_agent_status_host_filter(
|
|
stmt,
|
|
count_stmt,
|
|
agent_status,
|
|
stale_minutes=settings.sac_heartbeat_stale_minutes,
|
|
)
|
|
|
|
total = db.scalar(count_stmt) or 0
|
|
rows = db.scalars(
|
|
stmt.order_by(Host.last_seen_at.desc())
|
|
.offset((page - 1) * page_size)
|
|
.limit(page_size)
|
|
).all()
|
|
|
|
hb_map = max_event_time_by_host(db, HEARTBEAT_TYPE)
|
|
report_map = max_daily_report_time_by_host(db)
|
|
|
|
items: list[HostSummary] = []
|
|
for host in rows:
|
|
event_count = db.scalar(
|
|
select(func.count()).select_from(Event).where(Event.host_id == host.id)
|
|
)
|
|
last_hb = hb_map.get(host.id)
|
|
items.append(
|
|
HostSummary(
|
|
id=host.id,
|
|
hostname=host.hostname,
|
|
display_name=host.display_name,
|
|
os_family=host.os_family,
|
|
os_version=host.os_version,
|
|
product=host.product,
|
|
product_version=host.product_version,
|
|
ipv4=host.ipv4,
|
|
last_seen_at=host.last_seen_at,
|
|
event_count=int(event_count or 0),
|
|
last_heartbeat_at=last_hb,
|
|
last_daily_report_at=report_map.get(host.id),
|
|
last_inventory_at=host.inventory_updated_at,
|
|
agent_status=compute_agent_status(
|
|
last_hb, stale_minutes=settings.sac_heartbeat_stale_minutes
|
|
),
|
|
)
|
|
)
|
|
|
|
return HostListResponse(
|
|
items=items,
|
|
total=total,
|
|
page=page,
|
|
page_size=page_size,
|
|
latest_agent_versions=latest_agent_versions_by_product(db),
|
|
)
|
|
|
|
|
|
def _host_detail_from_model(
|
|
host: Host,
|
|
*,
|
|
db: Session,
|
|
settings,
|
|
) -> HostDetail:
|
|
hb_map = max_event_time_by_host(db, HEARTBEAT_TYPE)
|
|
report_map = max_daily_report_time_by_host(db)
|
|
event_count = db.scalar(
|
|
select(func.count()).select_from(Event).where(Event.host_id == host.id)
|
|
)
|
|
last_hb = hb_map.get(host.id)
|
|
return HostDetail(
|
|
id=host.id,
|
|
hostname=host.hostname,
|
|
display_name=host.display_name,
|
|
os_family=host.os_family,
|
|
os_version=host.os_version,
|
|
product=host.product,
|
|
product_version=host.product_version,
|
|
ipv4=host.ipv4,
|
|
ipv6=host.ipv6,
|
|
last_seen_at=host.last_seen_at,
|
|
event_count=int(event_count or 0),
|
|
last_heartbeat_at=last_hb,
|
|
last_daily_report_at=report_map.get(host.id),
|
|
last_inventory_at=host.inventory_updated_at,
|
|
agent_status=compute_agent_status(last_hb, stale_minutes=settings.sac_heartbeat_stale_minutes),
|
|
agent_instance_id=host.agent_instance_id,
|
|
tags=host.tags if isinstance(host.tags, list) else [],
|
|
use_sac_mode=host.use_sac_mode,
|
|
inventory=host.inventory if isinstance(host.inventory, dict) else None,
|
|
inventory_updated_at=host.inventory_updated_at,
|
|
created_at=host.created_at,
|
|
)
|
|
|
|
|
|
@router.get("/{host_id}", response_model=HostDetail)
|
|
def get_host(
|
|
host_id: int,
|
|
db: Session = Depends(get_db),
|
|
_user: str = Depends(get_current_user),
|
|
) -> HostDetail:
|
|
host = db.get(Host, host_id)
|
|
if host is None:
|
|
raise HTTPException(status_code=404, detail="Host not found")
|
|
settings = get_settings()
|
|
return _host_detail_from_model(host, db=db, settings=settings)
|
|
|
|
|
|
class HostDeleteResponse(BaseModel):
|
|
status: str
|
|
host_id: int
|
|
hostname: str
|
|
deleted_events: int
|
|
deleted_problems: int
|
|
|
|
|
|
@router.delete("/{host_id}", response_model=HostDeleteResponse)
|
|
def delete_host(
|
|
host_id: int,
|
|
db: Session = Depends(get_db),
|
|
_user=Depends(require_admin),
|
|
) -> HostDeleteResponse:
|
|
result = delete_host_and_related(db, host_id)
|
|
if result is None:
|
|
raise HTTPException(status_code=404, detail="Host not found")
|
|
db.commit()
|
|
return HostDeleteResponse(
|
|
status="deleted",
|
|
host_id=result.host_id,
|
|
hostname=result.hostname,
|
|
deleted_events=result.deleted_events,
|
|
deleted_problems=result.deleted_problems,
|
|
)
|
|
|