feat: Linux SSH admin and remote ssh-monitor update (0.11.0)

This commit is contained in:
PTah
2026-06-20 00:25:35 +10:00
parent 279ea925a8
commit cc8f50de89
17 changed files with 805 additions and 5 deletions
+98
View File
@@ -10,6 +10,7 @@ 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.linux_admin_settings import get_effective_linux_admin_config
from app.services.win_admin_settings import get_effective_win_admin_config
from app.services.winrm_connect import (
HostNotWindowsError,
@@ -19,6 +20,15 @@ from app.services.winrm_connect import (
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,
@@ -172,6 +182,60 @@ class HostWinRmTestResponse(BaseModel):
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
def _ssh_action_response(result: SshCommandResult, *, attempts: list[str] | 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,
)
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,
@@ -235,6 +299,40 @@ def test_host_winrm(
)
@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_config(db)
if not cfg.configured:
raise HTTPException(status_code=400, detail="Linux SSH admin is not configured")
return _run_ssh_action_on_host(host, cfg, action=test_ssh_connection)
@router.post("/{host_id}/actions/agent-update", response_model=HostSshActionResponse)
def update_host_agent_via_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_config(db)
if not cfg.configured:
raise HTTPException(status_code=400, detail="Linux SSH admin is not configured")
return _run_ssh_action_on_host(host, cfg, action=run_ssh_monitor_update)
@router.delete("/{host_id}", response_model=HostDeleteResponse)
def delete_host(
host_id: int,
+47
View File
@@ -37,6 +37,10 @@ from app.services.ui_settings import (
get_effective_ui_settings,
upsert_ui_settings,
)
from app.services.linux_admin_settings import (
get_effective_linux_admin_config,
upsert_linux_admin_settings,
)
from app.services.win_admin_settings import (
get_effective_win_admin_config,
upsert_win_admin_settings,
@@ -493,3 +497,46 @@ def update_win_admin_settings(
password_hint=_mask_secret(cfg.password),
source=cfg.source,
)
class LinuxAdminSettingsResponse(BaseModel):
configured: bool
user: str | None = None
password_hint: str | None = None
source: str = Field(description="env или db")
class LinuxAdminSettingsUpdate(BaseModel):
user: str | None = None
password: str | None = None
@router.get("/linux-admin", response_model=LinuxAdminSettingsResponse)
def get_linux_admin_settings(
db: Session = Depends(get_db),
_user=Depends(require_admin),
) -> LinuxAdminSettingsResponse:
cfg = get_effective_linux_admin_config(db)
return LinuxAdminSettingsResponse(
configured=cfg.configured,
user=cfg.user or None,
password_hint=_mask_secret(cfg.password),
source=cfg.source,
)
@router.put("/linux-admin", response_model=LinuxAdminSettingsResponse)
def update_linux_admin_settings(
body: LinuxAdminSettingsUpdate,
db: Session = Depends(get_db),
_user=Depends(require_admin),
) -> LinuxAdminSettingsResponse:
if body.user is not None and not body.user.strip():
raise HTTPException(status_code=422, detail="user must not be empty")
cfg = upsert_linux_admin_settings(db, user=body.user, password=body.password)
return LinuxAdminSettingsResponse(
configured=cfg.configured,
user=cfg.user or None,
password_hint=_mask_secret(cfg.password),
source=cfg.source,
)