diff --git a/backend/app/services/host_remote_actions.py b/backend/app/services/host_remote_actions.py index 397ecff..7f023fc 100644 --- a/backend/app/services/host_remote_actions.py +++ b/backend/app/services/host_remote_actions.py @@ -16,7 +16,12 @@ from app.services.agent_update import execute_agent_update_fallback from app.services.agent_update_types import AgentUpdateFallbackResult from app.services.linux_admin_settings import get_effective_linux_admin_config from app.services.agent_update_settings import get_effective_agent_update_config -from app.services.ssh_connect import iter_ssh_targets, run_ssh_monitor_update, SshCommandResult, tail_ssh_monitor_update_log +from app.services.ssh_connect import ( + iter_ssh_targets, + run_ssh_monitor_update, + SshCommandResult, + tail_ssh_monitor_update_log, +) logger = logging.getLogger(__name__) @@ -76,6 +81,61 @@ def _result_payload( } +def _completion_title(base_title: str, result: SshCommandResult) -> str: + if result.ok: + return f"{base_title} — готово" + return f"{base_title} — ошибка" + + +def _completion_message(result: SshCommandResult) -> str: + if result.ok: + version = (result.agent_version or "").strip() + if version: + return f"Обновление завершено успешно. Версия агента: {version}" + return "Обновление завершено успешно" + return (result.message or "Обновление не удалось").strip() + + +def _fetch_ssh_update_log_tail(db: Session, host: Host, *, lines: int = 200) -> str: + cfg = get_effective_linux_admin_config(db) + if not cfg.configured: + return "" + try: + targets = iter_ssh_targets(host) + except Exception: + return "" + for target in targets: + try: + tail = tail_ssh_monitor_update_log( + target=target, + user=cfg.user, + password=cfg.password, + lines=lines, + ) + if tail: + return tail + except Exception: + logger.debug("final update log tail failed host_id=%s target=%s", host.id, target, exc_info=True) + return "" + + +def _enrich_ssh_update_payload( + payload: dict[str, Any], + *, + base_title: str, + result: SshCommandResult, + previous_output: str, + log_tail: str, +) -> dict[str, Any]: + payload["title"] = _completion_title(base_title, result) + payload["message"] = _completion_message(result) + if log_tail: + payload["output"] = log_tail + elif previous_output.strip(): + payload["output"] = previous_output + return payload + + def _mark_running(db: Session, host: Host, *, title: str) -> str: started_at = _utcnow().isoformat() host.agent_update_state = "running" @@ -145,6 +205,7 @@ def _poll_ssh_update_log_loop( target=target, user=cfg.user, password=cfg.password, + lines=200, ) except Exception: logger.debug("update log tail failed host_id=%s target=%s", host_id, target, exc_info=True) @@ -205,9 +266,20 @@ def start_host_remote_action( row = session.get(Host, host_id) if row is None: return + previous_output = (row.remote_action or {}).get("output") or "" result = runner(session, row) started = (row.remote_action or {}).get("started_at") or started_at - row.remote_action = _result_payload(result, title=title, started_at=started) + payload = _result_payload(result, title=title, started_at=started) + if poll_remote_log and isinstance(result, SshCommandResult): + log_tail = _fetch_ssh_update_log_tail(session, row) + payload = _enrich_ssh_update_payload( + payload, + base_title=title, + result=result, + previous_output=previous_output, + log_tail=log_tail, + ) + row.remote_action = payload if isinstance(result, SshCommandResult): _apply_ssh_update_result(session, row, result) session.commit() diff --git a/backend/app/version.py b/backend/app/version.py index 2cf26fc..d5e776d 100644 --- a/backend/app/version.py +++ b/backend/app/version.py @@ -1,5 +1,5 @@ """Единый источник версии SAC (API, health, логи, OpenAPI).""" APP_NAME = "Security Alert Center" -APP_VERSION = "0.5.5" +APP_VERSION = "0.5.6" APP_VERSION_LABEL = f"{APP_NAME} v.{APP_VERSION}" diff --git a/backend/tests/test_linux_admin_settings.py b/backend/tests/test_linux_admin_settings.py index 3f7b8ec..cc7e716 100644 --- a/backend/tests/test_linux_admin_settings.py +++ b/backend/tests/test_linux_admin_settings.py @@ -174,3 +174,51 @@ def test_host_agent_update_success(jwt_headers, client, db_session, monkeypatch) assert job["target"] == "ubabuba" if "product_version" in job: assert job["product_version"] is None or isinstance(job["product_version"], str) + + +def test_host_agent_update_job_shows_log_tail_and_completion_title( + jwt_headers, client, db_session, monkeypatch +): + monkeypatch.setenv("SAC_LINUX_ADMIN_USER", "root") + monkeypatch.setenv("SAC_LINUX_ADMIN_PASSWORD", "pw") + from app.config import get_settings + from app.models import Host + from app.services.ssh_connect import SshCommandResult + from tests.test_agent_update import wait_remote_job + + get_settings.cache_clear() + + host = Host(hostname="router", os_family="linux", product="ssh-monitor", ipv4="10.0.0.1") + db_session.add(host) + db_session.commit() + db_session.refresh(host) + + log_tail = ( + "2026-07-08 14:18:15 INFO: === Script update completed successfully ===\n" + "2026-07-08 14:18:15 INFO: завершено успешно (код 0). Итог см. выше\n" + ) + with ( + patch("app.services.host_remote_actions.run_ssh_monitor_update") as mock_update, + patch("app.services.host_remote_actions.tail_ssh_monitor_update_log") as mock_tail, + ): + mock_update.return_value = SshCommandResult( + ok=True, + message="SSH OK (router), exit 0", + target="router", + stdout="", + exit_code=0, + agent_version="2.3.2-SAC", + ) + mock_tail.return_value = log_tail + response = client.post( + f"/api/v1/hosts/{host.id}/actions/agent-update", + headers=jwt_headers, + ) + + assert response.status_code == 202 + job = wait_remote_job(client, host.id, jwt_headers) + assert job["ok"] is True + assert "готово" in (job.get("title") or "") + assert "2.3.2-SAC" in (job.get("message") or "") + assert "Script update completed successfully" in (job.get("output") or "") + mock_tail.assert_called() diff --git a/frontend/src/components/HostActionLogModal.vue b/frontend/src/components/HostActionLogModal.vue index 66a4d24..3d5ad98 100644 --- a/frontend/src/components/HostActionLogModal.vue +++ b/frontend/src/components/HostActionLogModal.vue @@ -2,7 +2,11 @@