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
+208
View File
@@ -0,0 +1,208 @@
"""Tests for agent update settings, poll, request and ingest lifecycle."""
import uuid
from datetime import datetime, timedelta, timezone
from unittest.mock import patch
from app.models import Host
from app.services.agent_poll import build_agent_poll_payload
from app.services.agent_update import (
AGENT_UPDATE_SUCCESS,
AgentUpdateNotAllowedError,
process_agent_update_ingest,
request_agent_update,
)
from app.services.agent_update_settings import upsert_agent_update_settings
from app.services.ingest import ingest_event
from tests.test_ingest import VALID_EVENT
def test_agent_update_settings_sac_mode(db_session):
cfg = upsert_agent_update_settings(
db_session,
mode="sac",
recommended_ssh_version="2.1.0-SAC",
fallback_after_minutes=10,
)
assert cfg.mode == "sac"
assert cfg.recommended_ssh_version == "2.1.0-SAC"
assert cfg.fallback_after_minutes == 10
def test_request_agent_update_sets_pending(db_session):
upsert_agent_update_settings(db_session, mode="sac", recommended_ssh_version="2.1.0-SAC")
host = Host(
hostname="linux-1",
os_family="linux",
product="ssh-monitor",
product_version="2.0.0-SAC",
agent_instance_id="agent-uuid-1",
)
db_session.add(host)
db_session.commit()
request_agent_update(db_session, host)
assert host.pending_agent_update is True
assert host.pending_update_target_version == "2.1.0-SAC"
assert host.agent_update_state == "requested"
def test_request_agent_update_blocked_in_gpo_mode(db_session):
upsert_agent_update_settings(db_session, mode="gpo")
host = Host(
hostname="linux-2",
os_family="linux",
product="ssh-monitor",
agent_instance_id="agent-uuid-2",
)
db_session.add(host)
db_session.commit()
try:
request_agent_update(db_session, host)
raise AssertionError("expected AgentUpdateNotAllowedError")
except AgentUpdateNotAllowedError:
pass
def test_poll_payload_includes_update_and_config(db_session):
upsert_agent_update_settings(db_session, mode="sac")
host = Host(
hostname="win-1",
os_family="windows",
product="rdp-login-monitor",
agent_instance_id="agent-uuid-3",
pending_agent_update=True,
pending_update_target_version="2.1.0-SAC",
agent_config={"ServerDisplayName": "Test PC"},
agent_config_revision=3,
)
db_session.add(host)
db_session.commit()
payload = build_agent_poll_payload(db_session, host)
assert payload["config_revision"] == 3
assert payload["config"]["settings"]["ServerDisplayName"] == "Test PC"
assert payload["update"]["requested"] is True
assert payload["update"]["target_version"] == "2.1.0-SAC"
assert payload["update"]["source"] == "sac"
def test_ingest_agent_update_success_clears_pending(db_session):
host = Host(
hostname="linux-3",
os_family="linux",
product="ssh-monitor",
agent_instance_id="agent-uuid-4",
pending_agent_update=True,
pending_update_target_version="2.1.0-SAC",
)
db_session.add(host)
db_session.commit()
process_agent_update_ingest(
db_session,
host,
AGENT_UPDATE_SUCCESS,
{"product_version": "2.1.0-SAC"},
)
assert host.pending_agent_update is False
assert host.product_version == "2.1.0-SAC"
assert host.agent_update_state == "success"
def test_api_request_agent_update(jwt_headers, client, db_session):
upsert_agent_update_settings(db_session, mode="sac", recommended_ssh_version="2.1.0-SAC")
host = Host(
hostname="api-linux",
os_family="linux",
product="ssh-monitor",
agent_instance_id="agent-api-1",
)
db_session.add(host)
db_session.commit()
response = client.post(f"/api/v1/hosts/{host.id}/actions/request-agent-update", headers=jwt_headers)
assert response.status_code == 200
body = response.json()
assert body["pending_agent_update"] is True
assert body["target_version"] == "2.1.0-SAC"
def test_api_agent_poll_extended(auth_headers, client, db_session):
upsert_agent_update_settings(db_session, mode="sac")
host = Host(
hostname="poll-host",
os_family="linux",
product="ssh-monitor",
agent_instance_id="poll-agent-id",
pending_agent_update=True,
pending_update_target_version="2.1.0-SAC",
)
db_session.add(host)
db_session.commit()
response = client.get(
"/api/v1/agent/commands",
params={"agent_instance_id": "poll-agent-id"},
headers=auth_headers,
)
assert response.status_code == 200
body = response.json()
assert body["update"]["requested"] is True
assert body["commands"] == []
def test_api_patch_agent_config(jwt_headers, client, db_session):
host = Host(
hostname="cfg-host",
os_family="windows",
product="rdp-login-monitor",
)
db_session.add(host)
db_session.commit()
response = client.patch(
f"/api/v1/hosts/{host.id}/agent-config",
json={"settings": {"ServerDisplayName": "UNMS Kalina", "GetInventory": True}},
headers=jwt_headers,
)
assert response.status_code == 200
body = response.json()
assert body["config_revision"] == 1
assert body["settings"]["ServerDisplayName"] == "UNMS Kalina"
def test_api_agent_update_fallback_ssh(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
get_settings.cache_clear()
host = Host(
hostname="fb-linux",
os_family="linux",
product="ssh-monitor",
ipv4="10.10.36.9",
)
db_session.add(host)
db_session.commit()
with patch("app.services.agent_update.run_ssh_monitor_update") as mock_update:
from app.services.ssh_connect import SshCommandResult
mock_update.return_value = SshCommandResult(
ok=True,
message="updated",
target="fb-linux",
agent_version="2.1.0-SAC",
)
response = client.post(
f"/api/v1/hosts/{host.id}/actions/agent-update-fallback",
headers=jwt_headers,
)
assert response.status_code == 200
assert response.json()["ok"] is True
assert response.json()["product_version"] == "2.1.0-SAC"