Files
security-alert-center/backend/app/api/v1/hosts.py
T

933 lines
30 KiB
Python

from typing import Literal
from fastapi import APIRouter, Depends, HTTPException, Query, status
from pydantic import BaseModel, Field
from sqlalchemy import func, select
from sqlalchemy.orm import Session
from datetime import datetime, timezone
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.utils.sql_like import escape_ilike_pattern
from app.services.agent_version import (
latest_agent_versions_by_product,
reference_agent_versions_by_product,
)
from app.services.agent_git_release import get_git_release_versions
from app.services.agent_update import (
AgentUpdateNotAllowedError,
host_version_status,
request_agent_update,
)
from app.services.host_remote_actions import (
RemoteActionAlreadyRunningError,
cancel_host_remote_action,
clear_stale_running_remote_actions,
get_remote_action_status,
run_agent_fallback_action,
run_ssh_monitor_update_action,
start_host_remote_action,
)
from app.services.agent_update_settings import (
PRODUCT_RDP,
PRODUCT_SSH,
get_effective_agent_update_config,
)
from app.services.agent_host_config import get_host_agent_settings, update_host_agent_config
from app.services.host_sessions import (
HostSessionRow,
list_linux_sessions,
list_windows_sessions,
terminate_linux_session,
terminate_windows_session,
)
from app.services.host_delete import delete_host_and_related
from app.services.host_mgmt_credentials import (
get_host_mgmt_access_view,
upsert_host_mgmt_credentials,
)
from app.services.linux_admin_settings import get_effective_linux_admin_for_host
from app.services.win_admin_settings import get_effective_win_admin_for_host
from app.services.winrm_connect import (
HostNotWindowsError,
HostTargetMissingError,
WinAdminNotConfiguredError,
WinRmTestResult,
iter_winrm_targets,
test_winrm_connection,
)
from app.services.ssh_connect import (
HostNotLinuxError as SshHostNotLinuxError,
HostTargetMissingError as SshHostTargetMissingError,
LinuxAdminNotConfiguredError,
SshCommandResult,
iter_ssh_targets,
run_ssh_monitor_update,
test_ssh_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,
)
from app.services.event_type_visibility import get_hidden_event_types, visibility_type_filter
router = APIRouter(prefix="/hosts", tags=["hosts"])
def _count_visible_events(db: Session, host_id: int, hidden: frozenset[str]) -> int:
stmt = select(func.count()).select_from(Event).where(Event.host_id == host_id)
type_filter = visibility_type_filter(hidden)
if type_filter is not None:
stmt = stmt.where(type_filter)
return int(db.scalar(stmt) or 0)
@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"%{escape_ilike_pattern(hostname)}%"
host_match = Host.hostname.ilike(like, escape="\\") | Host.display_name.ilike(like, escape="\\")
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)
latest_map = latest_agent_versions_by_product(db)
update_cfg = get_effective_agent_update_config(db)
git_release = get_git_release_versions(update_cfg, db=db)
reference_map = reference_agent_versions_by_product(
update_cfg,
latest_map,
git_versions=git_release.versions,
)
hidden = get_hidden_event_types(db)
items: list[HostSummary] = []
for host in rows:
event_count = _count_visible_events(db, host.id, hidden)
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
),
version_status=host_version_status(
host,
cfg=update_cfg,
latest_map=latest_map,
git_versions=git_release.versions,
),
pending_agent_update=bool(host.pending_agent_update),
)
)
return HostListResponse(
items=items,
total=total,
page=page,
page_size=page_size,
latest_agent_versions=latest_map,
reference_agent_versions=reference_map,
git_latest_rdp_version=git_release.versions.get(PRODUCT_RDP) or None,
git_latest_ssh_version=git_release.versions.get(PRODUCT_SSH) or None,
)
class HostManualAddBody(BaseModel):
platform: Literal["windows", "linux"]
target: str = Field(..., min_length=1, max_length=253)
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)
hidden = get_hidden_event_types(db)
event_count = _count_visible_events(db, host.id, hidden)
last_hb = hb_map.get(host.id)
latest_map = latest_agent_versions_by_product(db)
update_cfg = get_effective_agent_update_config(db)
git_release = get_git_release_versions(update_cfg, db=db)
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),
version_status=host_version_status(
host,
cfg=update_cfg,
latest_map=latest_map,
git_versions=git_release.versions,
),
pending_agent_update=bool(host.pending_agent_update),
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,
ssh_admin_ok=host.ssh_admin_ok,
ssh_admin_checked_at=host.ssh_admin_checked_at,
pending_update_requested_at=host.pending_update_requested_at,
pending_update_target_version=host.pending_update_target_version,
agent_config_revision=int(host.agent_config_revision or 0),
agent_config=get_host_agent_settings(host) or None,
agent_update_state=host.agent_update_state,
agent_update_last_at=host.agent_update_last_at,
agent_update_last_error=host.agent_update_last_error,
)
def _set_ssh_admin_status(host: Host, ok: bool) -> None:
host.ssh_admin_ok = ok
host.ssh_admin_checked_at = datetime.now(timezone.utc)
@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
class HostSshActionResponse(BaseModel):
ok: bool
message: str
target: str
stdout: str | None = None
stderr: str | None = None
exit_code: int | None = None
product_version: str | None = None
ssh_admin_ok: bool | None = None
class HostAgentUpdateRequestResponse(BaseModel):
status: str
pending_agent_update: bool
target_version: str | None = None
agent_update_state: str | None = None
class HostAgentConfigUpdate(BaseModel):
settings: dict[str, object] = {}
class HostAgentConfigResponse(BaseModel):
config_revision: int
settings: dict[str, object]
class HostMgmtAccessResponse(BaseModel):
host_id: int
has_override: bool
user: str | None = None
password_set: bool = False
password_hint: str | None = None
effective_source: str
effective_configured: bool
effective_user: str | None = None
class HostMgmtAccessUpdate(BaseModel):
user: str | None = None
password: str | None = None
clear: bool = False
class HostRemoteActionStartResponse(BaseModel):
status: str
host_id: int
title: str
message: str
@router.post(
"/manual-add",
response_model=HostRemoteActionStartResponse,
status_code=status.HTTP_202_ACCEPTED,
)
def post_host_manual_add(
body: HostManualAddBody,
db: Session = Depends(get_db),
_user=Depends(require_admin),
) -> HostRemoteActionStartResponse:
from app.services.host_manual_add import ManualHostAddError, prepare_manual_host_add
try:
host = prepare_manual_host_add(db, platform=body.platform, target=body.target)
except ManualHostAddError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
if body.platform == "windows":
title = f"Ручное добавление Windows: {host.hostname}"
else:
title = f"Ручное добавление Linux: {host.hostname}"
try:
start_host_remote_action(
db,
host,
title=title,
runner=run_agent_fallback_action,
)
except RemoteActionAlreadyRunningError as exc:
raise HTTPException(status_code=409, detail=str(exc)) from exc
return HostRemoteActionStartResponse(
status="running",
host_id=host.id,
title=title,
message="Проверка пройдена, установка агента запущена на сервере SAC",
)
class HostRemoteActionJobResponse(BaseModel):
active: bool
host_id: int
hostname: str | None = None
status: str | None = None
title: str = ""
message: str = ""
stdout: str | None = None
stderr: str | None = None
output: str | None = None
ok: bool | None = None
exit_code: int | None = None
product_version: str | None = None
target: str | None = None
agent_update_state: str | None = None
started_at: str | None = None
finished_at: str | None = None
class HostSessionItem(BaseModel):
session_id: str
user: str
tty: str | None = None
state: str | None = None
source_ip: str | None = None
session_name: str | None = None
class HostSessionsResponse(BaseModel):
ok: bool
message: str
target: str | None = None
sessions: list[HostSessionItem]
class HostSessionTerminateBody(BaseModel):
session_id: str
class HostSessionTerminateResponse(BaseModel):
ok: bool
message: str
target: str | None = None
stdout: str | None = None
stderr: str | None = None
def _session_items(rows: list[HostSessionRow]) -> list[HostSessionItem]:
return [
HostSessionItem(
session_id=r.session_id,
user=r.user,
tty=r.tty,
state=r.state,
source_ip=r.source_ip,
session_name=r.session_name,
)
for r in rows
]
def _ssh_action_response(
result: SshCommandResult,
*,
attempts: list[str] | None = None,
ssh_admin_ok: bool | None = None,
) -> HostSshActionResponse:
message = result.message
if attempts:
message = f"{message} (пробовали: {', '.join(attempts)})"
return HostSshActionResponse(
ok=result.ok,
message=message,
target=result.target,
stdout=result.stdout or None,
stderr=result.stderr or None,
exit_code=result.exit_code,
product_version=result.agent_version,
ssh_admin_ok=ssh_admin_ok,
)
def _run_ssh_action_on_host(
host: Host,
cfg,
*,
action,
) -> HostSshActionResponse:
try:
targets = iter_ssh_targets(host)
except SshHostNotLinuxError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
except SshHostTargetMissingError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
last_result: SshCommandResult | None = None
attempts: list[str] = []
for target in targets:
try:
result = action(target=target, user=cfg.user, password=cfg.password)
except LinuxAdminNotConfiguredError 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:
return _ssh_action_response(result, attempts=attempts if len(attempts) > 1 else None)
assert last_result is not None
return _ssh_action_response(last_result, attempts=attempts if len(attempts) > 1 else 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_for_host(db, host)
if not cfg.configured:
raise HTTPException(
status_code=400,
detail="Windows admin is not configured (host override or Settings → Windows)",
)
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.post("/{host_id}/actions/ssh-test", response_model=HostSshActionResponse)
def test_host_ssh(
host_id: int,
db: Session = Depends(get_db),
_user=Depends(require_admin),
) -> HostSshActionResponse:
host = db.get(Host, host_id)
if host is None:
raise HTTPException(status_code=404, detail="Host not found")
cfg = get_effective_linux_admin_for_host(db, host)
if not cfg.configured:
raise HTTPException(
status_code=400,
detail="Linux SSH admin is not configured (host override or Settings → Linux)",
)
response = _run_ssh_action_on_host(host, cfg, action=test_ssh_connection)
_set_ssh_admin_status(host, response.ok)
db.commit()
return response.model_copy(update={"ssh_admin_ok": host.ssh_admin_ok})
@router.post(
"/{host_id}/actions/agent-update",
response_model=HostRemoteActionStartResponse,
status_code=status.HTTP_202_ACCEPTED,
)
def update_host_agent_via_ssh(
host_id: int,
db: Session = Depends(get_db),
_user=Depends(require_admin),
) -> HostRemoteActionStartResponse:
host = db.get(Host, host_id)
if host is None:
raise HTTPException(status_code=404, detail="Host not found")
cfg = get_effective_linux_admin_for_host(db, host)
if not cfg.configured:
raise HTTPException(
status_code=400,
detail="Linux SSH admin is not configured (host override or Settings → Linux)",
)
title = "Обновление ssh-monitor (SSH)"
try:
start_host_remote_action(
db,
host,
title=title,
runner=run_ssh_monitor_update_action,
poll_remote_log=True,
)
except RemoteActionAlreadyRunningError as exc:
raise HTTPException(status_code=409, detail=str(exc)) from exc
return HostRemoteActionStartResponse(
status="running",
host_id=host.id,
title=title,
message="Обновление запущено на сервере SAC и продолжится в фоне",
)
@router.post("/{host_id}/actions/request-agent-update", response_model=HostAgentUpdateRequestResponse)
def post_request_agent_update(
host_id: int,
db: Session = Depends(get_db),
_user=Depends(require_admin),
) -> HostAgentUpdateRequestResponse:
host = db.get(Host, host_id)
if host is None:
raise HTTPException(status_code=404, detail="Host not found")
try:
request_agent_update(db, host)
except AgentUpdateNotAllowedError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
db.commit()
return HostAgentUpdateRequestResponse(
status="requested",
pending_agent_update=host.pending_agent_update,
target_version=host.pending_update_target_version,
agent_update_state=host.agent_update_state,
)
@router.post(
"/{host_id}/actions/agent-update-fallback",
response_model=HostRemoteActionStartResponse,
status_code=status.HTTP_202_ACCEPTED,
)
def post_agent_update_fallback(
host_id: int,
db: Session = Depends(get_db),
_user=Depends(require_admin),
) -> HostRemoteActionStartResponse:
host = db.get(Host, host_id)
if host is None:
raise HTTPException(status_code=404, detail="Host not found")
from app.services.agent_update_settings import PRODUCT_RDP, PRODUCT_SSH
from app.services.ssh_connect import is_linux_host
from app.services.winrm_connect import is_windows_host
product = (host.product or "").strip()
if product == PRODUCT_RDP or is_windows_host(host):
title = "Обновление через WinRM"
elif product == PRODUCT_SSH or is_linux_host(host):
title = "Fallback SSH (ssh-monitor)"
else:
title = "Обновление агента (fallback)"
try:
start_host_remote_action(
db,
host,
title=title,
runner=run_agent_fallback_action,
)
except RemoteActionAlreadyRunningError as exc:
raise HTTPException(status_code=409, detail=str(exc)) from exc
return HostRemoteActionStartResponse(
status="running",
host_id=host.id,
title=title,
message="Обновление запущено на сервере SAC и продолжится в фоне",
)
@router.get("/{host_id}/actions/remote-job", response_model=HostRemoteActionJobResponse)
def get_host_remote_job(
host_id: int,
db: Session = Depends(get_db),
_user=Depends(require_admin),
) -> HostRemoteActionJobResponse:
host = db.get(Host, host_id)
if host is None:
raise HTTPException(status_code=404, detail="Host not found")
db.refresh(host)
return HostRemoteActionJobResponse(**get_remote_action_status(host))
@router.post("/{host_id}/actions/remote-job/cancel", response_model=HostRemoteActionJobResponse)
def cancel_host_remote_job(
host_id: int,
db: Session = Depends(get_db),
_user=Depends(require_admin),
) -> HostRemoteActionJobResponse:
host = db.get(Host, host_id)
if host is None:
raise HTTPException(status_code=404, detail="Host not found")
if not cancel_host_remote_action(db, host):
raise HTTPException(status_code=409, detail="No running remote action for this host")
db.refresh(host)
return HostRemoteActionJobResponse(**get_remote_action_status(host))
@router.post("/{host_id}/actions/sessions/list", response_model=HostSessionsResponse)
def list_host_sessions(
host_id: int,
db: Session = Depends(get_db),
_user=Depends(require_admin),
) -> HostSessionsResponse:
host = db.get(Host, host_id)
if host is None:
raise HTTPException(status_code=404, detail="Host not found")
from app.services.ssh_connect import is_linux_host
from app.services.winrm_connect import is_windows_host
if is_linux_host(host):
cfg = get_effective_linux_admin_for_host(db, host)
if not cfg.configured:
raise HTTPException(
status_code=400,
detail="Linux SSH admin is not configured (host override or Settings → Linux)",
)
try:
sessions, result = list_linux_sessions(host, cfg)
except SshHostNotLinuxError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
except SshHostTargetMissingError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
return HostSessionsResponse(
ok=result.ok,
message=result.message,
target=result.target or None,
sessions=_session_items(sessions),
)
if is_windows_host(host):
cfg = get_effective_win_admin_for_host(db, host)
if not cfg.configured:
raise HTTPException(
status_code=400,
detail="Windows admin is not configured (host override or Settings → Windows)",
)
try:
sessions, result = list_windows_sessions(host, cfg)
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
if result is None:
raise HTTPException(status_code=502, detail="WinRM qwinsta failed")
return HostSessionsResponse(
ok=result.ok,
message=result.message,
target=result.target or None,
sessions=_session_items(sessions),
)
raise HTTPException(status_code=400, detail="Host OS does not support live sessions")
@router.post("/{host_id}/actions/sessions/terminate", response_model=HostSessionTerminateResponse)
def terminate_host_session(
host_id: int,
body: HostSessionTerminateBody,
db: Session = Depends(get_db),
_user=Depends(require_admin),
) -> HostSessionTerminateResponse:
host = db.get(Host, host_id)
if host is None:
raise HTTPException(status_code=404, detail="Host not found")
from app.services.ssh_connect import is_linux_host
from app.services.winrm_connect import is_windows_host
sid = body.session_id.strip()
if not sid:
raise HTTPException(status_code=422, detail="session_id is required")
if is_linux_host(host):
cfg = get_effective_linux_admin_for_host(db, host)
if not cfg.configured:
raise HTTPException(
status_code=400,
detail="Linux SSH admin is not configured (host override or Settings → Linux)",
)
try:
result = terminate_linux_session(host, cfg, sid)
except SshHostNotLinuxError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
except SshHostTargetMissingError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
return HostSessionTerminateResponse(
ok=result.ok,
message=result.message,
target=result.target or None,
stdout=result.stdout or None,
stderr=result.stderr or None,
)
if is_windows_host(host):
cfg = get_effective_win_admin_for_host(db, host)
if not cfg.configured:
raise HTTPException(
status_code=400,
detail="Windows admin is not configured (host override or Settings → Windows)",
)
try:
result = terminate_windows_session(host, cfg, sid)
except HostNotWindowsError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
if result is None:
raise HTTPException(status_code=502, detail="WinRM logoff failed")
return HostSessionTerminateResponse(
ok=result.ok,
message=result.message,
target=result.target or None,
stdout=result.stdout or None,
stderr=result.stderr or None,
)
raise HTTPException(status_code=400, detail="Host OS does not support session terminate")
@router.patch("/{host_id}/agent-config", response_model=HostAgentConfigResponse)
def patch_host_agent_config(
host_id: int,
body: HostAgentConfigUpdate,
db: Session = Depends(get_db),
_user=Depends(require_admin),
) -> HostAgentConfigResponse:
host = db.get(Host, host_id)
if host is None:
raise HTTPException(status_code=404, detail="Host not found")
try:
update_host_agent_config(db, host, body.settings)
except ValueError as exc:
raise HTTPException(status_code=422, detail=str(exc)) from exc
db.commit()
settings = get_host_agent_settings(host)
return HostAgentConfigResponse(
config_revision=int(host.agent_config_revision or 0),
settings=settings,
)
@router.get("/{host_id}/access", response_model=HostMgmtAccessResponse)
def get_host_access(
host_id: int,
db: Session = Depends(get_db),
_user=Depends(require_admin),
) -> HostMgmtAccessResponse:
host = db.get(Host, host_id)
if host is None:
raise HTTPException(status_code=404, detail="Host not found")
view = get_host_mgmt_access_view(db, host)
return HostMgmtAccessResponse(
host_id=view.host_id,
has_override=view.has_override,
user=view.user,
password_set=view.password_set,
password_hint=view.password_hint,
effective_source=view.effective_source,
effective_configured=view.effective_configured,
effective_user=view.effective_user,
)
@router.put("/{host_id}/access", response_model=HostMgmtAccessResponse)
def put_host_access(
host_id: int,
body: HostMgmtAccessUpdate,
db: Session = Depends(get_db),
_user=Depends(require_admin),
) -> HostMgmtAccessResponse:
host = db.get(Host, host_id)
if host is None:
raise HTTPException(status_code=404, detail="Host not found")
view = upsert_host_mgmt_credentials(
db,
host,
user=body.user,
password=body.password,
clear=body.clear,
)
return HostMgmtAccessResponse(
host_id=view.host_id,
has_override=view.has_override,
user=view.user,
password_set=view.password_set,
password_hint=view.password_hint,
effective_source=view.effective_source,
effective_configured=view.effective_configured,
effective_user=view.effective_user,
)
@router.get("/{host_id}/agent-config", response_model=HostAgentConfigResponse)
def get_host_agent_config(
host_id: int,
db: Session = Depends(get_db),
_user=Depends(require_admin),
) -> HostAgentConfigResponse:
host = db.get(Host, host_id)
if host is None:
raise HTTPException(status_code=404, detail="Host not found")
settings = get_host_agent_settings(host)
return HostAgentConfigResponse(
config_revision=int(host.agent_config_revision or 0),
settings=settings,
)
@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,
)