feat: agent updates from SAC, poll config, SSH/WinRM fallback (0.12.0)

Add agent update settings, pending self-update via poll, desired config per host, and automatic or manual SSH/WinRM fallback when agents do not report agent.update events.
This commit is contained in:
2026-06-20 15:52:10 +10:00
parent 75f4c475df
commit cd2f27792d
24 changed files with 1494 additions and 15 deletions
+120
View File
@@ -11,6 +11,14 @@ from app.database import get_db
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.agent_update import (
AgentUpdateNotAllowedError,
execute_agent_update_fallback,
host_version_status,
request_agent_update,
)
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
from app.services.linux_admin_settings import get_effective_linux_admin_config
from app.services.win_admin_settings import get_effective_win_admin_config
@@ -82,6 +90,8 @@ def list_hosts(
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)
items: list[HostSummary] = []
for host in rows:
@@ -107,6 +117,8 @@ def list_hosts(
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),
pending_agent_update=bool(host.pending_agent_update),
)
)
@@ -131,6 +143,8 @@ def _host_detail_from_model(
select(func.count()).select_from(Event).where(Event.host_id == host.id)
)
last_hb = hb_map.get(host.id)
latest_map = latest_agent_versions_by_product(db)
update_cfg = get_effective_agent_update_config(db)
return HostDetail(
id=host.id,
hostname=host.hostname,
@@ -147,6 +161,8 @@ def _host_detail_from_model(
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),
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,
@@ -155,6 +171,13 @@ def _host_detail_from_model(
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,
)
@@ -202,6 +225,22 @@ class HostSshActionResponse(BaseModel):
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]
def _ssh_action_response(
result: SshCommandResult,
*,
@@ -364,6 +403,87 @@ def update_host_agent_via_ssh(
)
@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=HostSshActionResponse)
def post_agent_update_fallback(
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")
ok, message, version = execute_agent_update_fallback(db, host)
db.commit()
return HostSshActionResponse(
ok=ok,
message=message,
target=host.hostname,
product_version=version or (host.product_version if ok else None),
ssh_admin_ok=host.ssh_admin_ok,
)
@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}/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,