feat: host session list/terminate and event session actions (0.20.28)

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
PTah
2026-06-22 10:13:42 +10:00
parent ca0255e13b
commit d848d94604
12 changed files with 773 additions and 5 deletions
+158
View File
@@ -29,6 +29,13 @@ from app.services.host_remote_actions import (
)
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_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.linux_admin_settings import get_effective_linux_admin_config
from app.services.win_admin_settings import get_effective_win_admin_config
@@ -302,6 +309,48 @@ class HostRemoteActionJobResponse(BaseModel):
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,
*,
@@ -550,6 +599,115 @@ def get_host_remote_job(
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_config(db)
if not cfg.configured:
raise HTTPException(status_code=400, detail="Linux SSH admin is not configured")
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_config(db)
if not cfg.configured:
raise HTTPException(status_code=400, detail="Windows domain admin is not configured")
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_config(db)
if not cfg.configured:
raise HTTPException(status_code=400, detail="Linux SSH admin is not configured")
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_config(db)
if not cfg.configured:
raise HTTPException(status_code=400, detail="Windows domain admin is not configured")
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,