feat: background remote agent updates survive SAC navigation (0.20.19)

This commit is contained in:
PTah
2026-06-21 11:01:26 +10:00
parent d7bbcc5337
commit 5635be1322
16 changed files with 642 additions and 161 deletions
+99 -35
View File
@@ -1,10 +1,9 @@
from fastapi import APIRouter, Depends, HTTPException, Query
from fastapi import APIRouter, Depends, HTTPException, Query, status
from pydantic import BaseModel
from sqlalchemy import func, select
from sqlalchemy.orm import Session
from datetime import datetime, timezone
from functools import partial
from app.auth.jwt_auth import get_current_user, require_admin
from app.config import get_settings
@@ -18,10 +17,16 @@ from app.services.agent_version import (
from app.services.agent_git_release import get_git_release_versions
from app.services.agent_update import (
AgentUpdateNotAllowedError,
execute_agent_update_fallback,
host_version_status,
request_agent_update,
)
from app.services.host_remote_actions import (
RemoteActionAlreadyRunningError,
get_remote_action_status,
run_agent_fallback_action,
run_ssh_monitor_update_action,
start_host_remote_action,
)
from app.services.agent_update_settings import get_effective_agent_update_config
from app.services.agent_host_config import get_host_agent_settings, update_host_agent_config
from app.services.host_delete import delete_host_and_related
@@ -264,6 +269,32 @@ class HostAgentConfigResponse(BaseModel):
settings: dict[str, object]
class HostRemoteActionStartResponse(BaseModel):
status: str
host_id: int
title: str
message: str
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
def _ssh_action_response(
result: SshCommandResult,
*,
@@ -399,12 +430,16 @@ def test_host_ssh(
return response.model_copy(update={"ssh_admin_ok": host.ssh_admin_ok})
@router.post("/{host_id}/actions/agent-update", response_model=HostSshActionResponse)
@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),
) -> HostSshActionResponse:
) -> HostRemoteActionStartResponse:
host = db.get(Host, host_id)
if host is None:
raise HTTPException(status_code=404, detail="Host not found")
@@ -413,23 +448,21 @@ def update_host_agent_via_ssh(
if not cfg.configured:
raise HTTPException(status_code=400, detail="Linux SSH admin is not configured")
agent_cfg = get_effective_agent_update_config(db)
repo_url = (agent_cfg.ssh_git_repo_url or "").strip() or None
branch = (agent_cfg.git_branch or "main").strip() or "main"
response = _run_ssh_action_on_host(
host,
cfg,
action=partial(run_ssh_monitor_update, repo_url=repo_url, git_branch=branch),
)
_set_ssh_admin_status(host, response.ok)
if response.ok and response.product_version:
host.product_version = response.product_version
db.commit()
return response.model_copy(
update={
"product_version": host.product_version if response.ok else response.product_version,
"ssh_admin_ok": host.ssh_admin_ok,
}
title = "Обновление ssh-monitor (SSH)"
try:
start_host_remote_action(
db,
host,
title=title,
runner=run_ssh_monitor_update_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 и продолжится в фоне",
)
@@ -455,30 +488,61 @@ def post_request_agent_update(
)
@router.post("/{host_id}/actions/agent-update-fallback", response_model=HostSshActionResponse)
@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),
) -> HostSshActionResponse:
) -> HostRemoteActionStartResponse:
host = db.get(Host, host_id)
if host is None:
raise HTTPException(status_code=404, detail="Host not found")
result = execute_agent_update_fallback(db, host)
db.commit()
return HostSshActionResponse(
ok=result.ok,
message=result.message,
target=result.target or host.hostname,
stdout=result.stdout or None,
stderr=result.stderr or None,
exit_code=result.exit_code,
product_version=result.product_version or (host.product_version if result.ok else None),
ssh_admin_ok=host.ssh_admin_ok,
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")
return HostRemoteActionJobResponse(**get_remote_action_status(host))
@router.patch("/{host_id}/agent-config", response_model=HostAgentConfigResponse)
def patch_host_agent_config(
host_id: int,