9883e6aab3
Allow host-level login/password for home/workgroup PCs while keeping global domain admins in Settings.
933 lines
30 KiB
Python
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,
|
|
)
|
|
|