256 lines
8.3 KiB
Python
256 lines
8.3 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.win_admin_settings import get_effective_win_admin_config
|
|
from app.services.winrm_connect import (
|
|
HostNotWindowsError,
|
|
HostTargetMissingError,
|
|
WinAdminNotConfiguredError,
|
|
WinRmTestResult,
|
|
iter_winrm_targets,
|
|
test_winrm_connection,
|
|
)
|
|
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
|
|
|
|
|
|
class HostWinRmTestResponse(BaseModel):
|
|
ok: bool
|
|
message: str
|
|
target: str
|
|
hostname: str | None = None
|
|
|
|
|
|
@router.post("/{host_id}/actions/winrm-test", response_model=HostWinRmTestResponse)
|
|
def test_host_winrm(
|
|
host_id: int,
|
|
db: Session = Depends(get_db),
|
|
_user=Depends(require_admin),
|
|
) -> HostWinRmTestResponse:
|
|
host = db.get(Host, host_id)
|
|
if host is None:
|
|
raise HTTPException(status_code=404, detail="Host not found")
|
|
|
|
cfg = get_effective_win_admin_config(db)
|
|
if not cfg.configured:
|
|
raise HTTPException(status_code=400, detail="Windows domain admin is not configured")
|
|
|
|
try:
|
|
targets = iter_winrm_targets(host)
|
|
except HostNotWindowsError as exc:
|
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
except HostTargetMissingError as exc:
|
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
|
|
last_result = None
|
|
attempts: list[str] = []
|
|
for target in targets:
|
|
try:
|
|
result = test_winrm_connection(
|
|
target=target,
|
|
user=cfg.user,
|
|
password=cfg.password,
|
|
)
|
|
except WinAdminNotConfiguredError as exc:
|
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
except RuntimeError as exc:
|
|
raise HTTPException(status_code=503, detail=str(exc)) from exc
|
|
last_result = result
|
|
attempts.append(f"{target}={'OK' if result.ok else 'fail'}")
|
|
if result.ok:
|
|
if len(targets) > 1:
|
|
result = WinRmTestResult(
|
|
ok=True,
|
|
message=f"{result.message} (пробовали: {', '.join(attempts)})",
|
|
target=result.target,
|
|
hostname=result.hostname,
|
|
)
|
|
return HostWinRmTestResponse(
|
|
ok=result.ok,
|
|
message=result.message,
|
|
target=result.target,
|
|
hostname=result.hostname,
|
|
)
|
|
|
|
assert last_result is not None
|
|
message = last_result.message
|
|
if len(attempts) > 1:
|
|
message = f"{message} (пробовали: {', '.join(attempts)})"
|
|
return HostWinRmTestResponse(
|
|
ok=False,
|
|
message=message,
|
|
target=last_result.target,
|
|
hostname=last_result.hostname,
|
|
)
|
|
|
|
|
|
@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,
|
|
)
|
|
|