431 lines
15 KiB
Python
431 lines
15 KiB
Python
import json
|
||
import unittest
|
||
from datetime import datetime
|
||
from unittest.mock import MagicMock, patch
|
||
|
||
from app.core.config import settings
|
||
from app.core.db import get_db
|
||
from app.services.ops_job_service import (
|
||
_execute_control_plane_job,
|
||
_serialize_job_row,
|
||
create_ops_job,
|
||
dispatch_ops_job,
|
||
list_managed_nodes,
|
||
upsert_managed_node,
|
||
)
|
||
|
||
|
||
class OpsJobServiceTests(unittest.TestCase):
|
||
def test_serialize_job_row_adds_execution_mode_label(self) -> None:
|
||
row = (
|
||
9,
|
||
"ops-2026041801-abcd12",
|
||
"logs.collect",
|
||
"node",
|
||
"mainland-worker-01",
|
||
"queued",
|
||
"remote-agent",
|
||
"api",
|
||
json.dumps({"service_name": "domaincheck-worker", "lines": 120}, ensure_ascii=False),
|
||
json.dumps({}, ensure_ascii=False),
|
||
json.dumps({}, ensure_ascii=False),
|
||
"",
|
||
datetime(2026, 4, 18, 10, 0, 0),
|
||
None,
|
||
None,
|
||
datetime(2026, 4, 18, 10, 0, 0),
|
||
"low",
|
||
False,
|
||
"approved",
|
||
"",
|
||
None,
|
||
"",
|
||
"",
|
||
None,
|
||
json.dumps({}, ensure_ascii=False),
|
||
json.dumps({}, ensure_ascii=False),
|
||
None,
|
||
)
|
||
|
||
job = _serialize_job_row(row)
|
||
|
||
self.assertEqual("remote-agent", job["execution_mode"])
|
||
self.assertEqual("远端 Agent", job["execution_mode_label"])
|
||
self.assertEqual("排队中", job["status_label"])
|
||
self.assertEqual("已审批", job["approval_status_label"])
|
||
self.assertEqual(["mainland-worker-01"], job["target_node_codes"])
|
||
self.assertEqual(0, job["steps_total"])
|
||
self.assertFalse(job["steps_loaded"])
|
||
self.assertFalse(job["is_compact"])
|
||
self.assertEqual("ops_job", job["focus_ref"]["kind"])
|
||
self.assertEqual(9, job["focus_ref"]["job_id"])
|
||
self.assertIn("目标节点 mainland-worker-01", job["summary_text"])
|
||
|
||
def test_serialize_job_row_counts_loaded_steps(self) -> None:
|
||
row = (
|
||
10,
|
||
"ops-2026041801-efgh34",
|
||
"diagnostics.collect",
|
||
"node",
|
||
"mainland-worker-02",
|
||
"running",
|
||
"ssh",
|
||
"web-ui",
|
||
json.dumps({}, ensure_ascii=False),
|
||
json.dumps({}, ensure_ascii=False),
|
||
json.dumps({}, ensure_ascii=False),
|
||
"",
|
||
datetime(2026, 4, 18, 10, 5, 0),
|
||
datetime(2026, 4, 18, 10, 5, 2),
|
||
None,
|
||
datetime(2026, 4, 18, 10, 5, 3),
|
||
"medium",
|
||
True,
|
||
"approved",
|
||
"admin",
|
||
datetime(2026, 4, 18, 10, 5, 1),
|
||
"",
|
||
"",
|
||
datetime(2026, 4, 18, 10, 5, 2),
|
||
json.dumps({}, ensure_ascii=False),
|
||
json.dumps({}, ensure_ascii=False),
|
||
None,
|
||
)
|
||
|
||
job = _serialize_job_row(
|
||
row,
|
||
steps=[
|
||
{"id": 1, "status": "success"},
|
||
{"id": 2, "status": "running"},
|
||
{"id": 3, "status": "failed"},
|
||
],
|
||
)
|
||
|
||
self.assertEqual("执行中", job["status_label"])
|
||
self.assertEqual("已审批", job["approval_status_label"])
|
||
self.assertEqual(3, job["steps_total"])
|
||
self.assertEqual(1, job["steps_running"])
|
||
self.assertEqual(1, job["steps_success"])
|
||
self.assertEqual(1, job["steps_failed"])
|
||
self.assertEqual(2, job["steps_terminal"])
|
||
self.assertTrue(job["steps_loaded"])
|
||
self.assertIn("mainland-worker-02", job["summary_text"])
|
||
|
||
def test_upsert_managed_node_supports_ssh_entry_and_secret_flags(self) -> None:
|
||
node_code = "test-managed-node-secret"
|
||
ok, message, data = upsert_managed_node(
|
||
{
|
||
"node_code": node_code,
|
||
"region": "mainland",
|
||
"role": "worker",
|
||
"ssh_entry": "121.204.244.188 root <demo-password>",
|
||
"is_enabled": True,
|
||
}
|
||
)
|
||
|
||
self.assertTrue(ok, message)
|
||
node = data["node"]
|
||
self.assertEqual("121.204.244.188", node["ssh_host"])
|
||
self.assertEqual("root", node["ssh_user"])
|
||
self.assertEqual(22, node["ssh_port"])
|
||
self.assertEqual("password", node["auth_mode"])
|
||
self.assertTrue(node["ssh_password_configured"])
|
||
|
||
managed = next(item for item in list_managed_nodes() if item["node_code"] == node_code)
|
||
self.assertTrue(managed["ssh_password_configured"])
|
||
|
||
with get_db() as conn:
|
||
with conn.cursor() as cur:
|
||
cur.execute(
|
||
"SELECT ssh_password FROM ops_managed_node_secrets WHERE node_code = %s",
|
||
(node_code,),
|
||
)
|
||
row = cur.fetchone()
|
||
self.assertIsNotNone(row)
|
||
self.assertEqual("demo-password", str(row[0] or ""))
|
||
|
||
@patch("app.services.ops_job_service.list_managed_nodes")
|
||
@patch("app.services.ops_agent_service.build_node_agent_bootstrap_plan")
|
||
def test_execute_control_plane_bootstrap_uses_managed_node_defaults(
|
||
self,
|
||
mock_build_node_agent_bootstrap_plan,
|
||
mock_list_managed_nodes,
|
||
) -> None:
|
||
mock_list_managed_nodes.return_value = [
|
||
{
|
||
"node_code": "mainland-worker-02",
|
||
"region": "mainland",
|
||
"role": "worker",
|
||
}
|
||
]
|
||
mock_build_node_agent_bootstrap_plan.return_value = (
|
||
True,
|
||
"节点 Agent 接入方案已生成",
|
||
{
|
||
"token_preview": "agent-...1234",
|
||
"bootstrap_plan": {
|
||
"bootstrap_script_name": "bootstrap-node-agent-mainland-worker-02.sh",
|
||
},
|
||
},
|
||
)
|
||
|
||
ok, message, data = _execute_control_plane_job(
|
||
"node.bootstrap",
|
||
target_node_code="mainland-worker-02",
|
||
payload={
|
||
"root_dir": "/opt/domaincheck",
|
||
"expires_in_hours": 12,
|
||
},
|
||
requested_by="unit-test",
|
||
metadata={"source": "test"},
|
||
)
|
||
|
||
self.assertTrue(ok)
|
||
self.assertEqual("节点 Agent 接入方案已生成", message)
|
||
self.assertEqual("control-plane", data["execution_mode"])
|
||
self.assertEqual("mainland", data["node_region"])
|
||
self.assertEqual("worker", data["node_role"])
|
||
mock_build_node_agent_bootstrap_plan.assert_called_once_with(
|
||
node_code="mainland-worker-02",
|
||
node_region="mainland",
|
||
node_role="worker",
|
||
issued_by="unit-test",
|
||
expires_in_hours=12,
|
||
control_plane_base_url="",
|
||
root_dir="/opt/domaincheck",
|
||
metadata={
|
||
"source": "test",
|
||
"issued_from": "ops-job/control-plane",
|
||
"target_node_code": "mainland-worker-02",
|
||
"node_region": "mainland",
|
||
"node_role": "worker",
|
||
},
|
||
)
|
||
|
||
@patch("app.services.ops_job_service.get_db")
|
||
def test_upsert_managed_node_preserves_remote_fields_when_sync_payload_omits_them(self, mock_get_db) -> None:
|
||
existing_metadata = {"operator_note": "keep-me"}
|
||
merged_metadata = {"operator_note": "keep-me", "cluster_status": "online"}
|
||
existing_row = (
|
||
"mainland-worker-01",
|
||
"mainland",
|
||
"worker",
|
||
"Mainland Worker 01",
|
||
"121.204.244.248",
|
||
22,
|
||
"root",
|
||
"key",
|
||
"stable",
|
||
False,
|
||
json.dumps(existing_metadata, ensure_ascii=False),
|
||
datetime(2026, 4, 18, 1, 0, 0),
|
||
None,
|
||
datetime(2026, 4, 18, 1, 5, 0),
|
||
)
|
||
returned_row = (
|
||
"mainland-worker-01",
|
||
"mainland",
|
||
"worker",
|
||
"S244-248",
|
||
"121.204.244.248",
|
||
22,
|
||
"root",
|
||
"key",
|
||
"stable",
|
||
False,
|
||
json.dumps(merged_metadata, ensure_ascii=False),
|
||
datetime(2026, 4, 18, 1, 0, 0),
|
||
None,
|
||
datetime(2026, 4, 18, 1, 6, 0),
|
||
)
|
||
|
||
conn = MagicMock()
|
||
cursor = MagicMock()
|
||
db_ctx = MagicMock()
|
||
cursor_ctx = MagicMock()
|
||
db_ctx.__enter__.return_value = conn
|
||
db_ctx.__exit__.return_value = False
|
||
cursor_ctx.__enter__.return_value = cursor
|
||
cursor_ctx.__exit__.return_value = False
|
||
conn.cursor.return_value = cursor_ctx
|
||
mock_get_db.return_value = db_ctx
|
||
cursor.fetchone.side_effect = [existing_row, returned_row]
|
||
|
||
ok, message, data = upsert_managed_node(
|
||
{
|
||
"node_code": "mainland-worker-01",
|
||
"region": "mainland",
|
||
"role": "worker",
|
||
"title": "S244-248",
|
||
"metadata": {
|
||
"cluster_status": "online",
|
||
},
|
||
}
|
||
)
|
||
|
||
self.assertTrue(ok)
|
||
self.assertEqual("托管节点已保存", message)
|
||
node = data["node"]
|
||
self.assertEqual("121.204.244.248", node["ssh_host"])
|
||
self.assertEqual("root", node["ssh_user"])
|
||
self.assertFalse(node["is_enabled"])
|
||
self.assertEqual("keep-me", node["metadata"]["operator_note"])
|
||
self.assertEqual("online", node["metadata"]["cluster_status"])
|
||
|
||
insert_call = next(
|
||
call
|
||
for call in cursor.execute.call_args_list
|
||
if "INSERT INTO ops_managed_nodes" in str(call.args[0])
|
||
)
|
||
insert_params = insert_call.args[1]
|
||
self.assertEqual("121.204.244.248", insert_params[4])
|
||
self.assertEqual(22, insert_params[5])
|
||
self.assertEqual("root", insert_params[6])
|
||
self.assertFalse(insert_params[9])
|
||
self.assertEqual(merged_metadata, json.loads(insert_params[10]))
|
||
|
||
@patch("app.services.ops_agent_service.append_ops_job_event")
|
||
@patch("app.services.ops_job_service._execute_ssh_job_record")
|
||
@patch("app.services.ops_job_service.get_db")
|
||
def test_dispatch_ops_job_ssh_executes_via_ssh_record(
|
||
self,
|
||
mock_get_db,
|
||
mock_execute_ssh_job_record,
|
||
mock_append_ops_job_event,
|
||
) -> None:
|
||
conn = MagicMock()
|
||
cursor = MagicMock()
|
||
db_ctx = MagicMock()
|
||
cursor_ctx = MagicMock()
|
||
db_ctx.__enter__.return_value = conn
|
||
db_ctx.__exit__.return_value = False
|
||
cursor_ctx.__enter__.return_value = cursor
|
||
cursor_ctx.__exit__.return_value = False
|
||
conn.cursor.return_value = cursor_ctx
|
||
mock_get_db.return_value = db_ctx
|
||
cursor.fetchone.return_value = (
|
||
9,
|
||
"ops-2026041801-abcd12",
|
||
"logs.collect",
|
||
"node",
|
||
"mainland-worker-01",
|
||
"queued",
|
||
"ssh",
|
||
"api",
|
||
json.dumps({"service_name": "domaincheck-worker", "lines": 120}, ensure_ascii=False),
|
||
json.dumps({}, ensure_ascii=False),
|
||
json.dumps({}, ensure_ascii=False),
|
||
"",
|
||
datetime(2026, 4, 18, 10, 0, 0),
|
||
None,
|
||
None,
|
||
datetime(2026, 4, 18, 10, 0, 0),
|
||
"low",
|
||
False,
|
||
"approved",
|
||
"",
|
||
None,
|
||
"",
|
||
"",
|
||
None,
|
||
json.dumps({}, ensure_ascii=False),
|
||
json.dumps({}, ensure_ascii=False),
|
||
None,
|
||
)
|
||
mock_execute_ssh_job_record.return_value = (
|
||
True,
|
||
"worker logs collected",
|
||
{"job": {"id": 9, "status": "success"}},
|
||
)
|
||
|
||
ok, message, data = dispatch_ops_job(9)
|
||
|
||
self.assertTrue(ok)
|
||
self.assertEqual("已按 SSH 执行派发:worker logs collected", message)
|
||
self.assertEqual("success", data["job"]["status"])
|
||
mock_execute_ssh_job_record.assert_called_once_with(9)
|
||
mock_append_ops_job_event.assert_called_once()
|
||
event_kwargs = mock_append_ops_job_event.call_args.kwargs
|
||
self.assertEqual("job_dispatch_requested", event_kwargs["event_type"])
|
||
self.assertEqual("mainland-worker-01", event_kwargs["node_code"])
|
||
self.assertEqual("ssh", event_kwargs["payload"]["execution_mode"])
|
||
|
||
@patch("app.services.ops_agent_service.append_ops_job_event")
|
||
@patch("app.services.ops_job_service.get_ops_job")
|
||
@patch("app.services.ops_job_service._execute_local_job")
|
||
@patch("app.services.ops_policy_service.preview_ops_job_policy")
|
||
@patch("app.services.ops_job_service.get_db")
|
||
def test_create_ops_job_auto_uses_local_runtime_for_local_structured_action(
|
||
self,
|
||
mock_get_db,
|
||
mock_preview_ops_job_policy,
|
||
mock_execute_local_job,
|
||
mock_get_ops_job,
|
||
mock_append_ops_job_event,
|
||
) -> None:
|
||
conn = MagicMock()
|
||
cursor = MagicMock()
|
||
db_ctx = MagicMock()
|
||
cursor_ctx = MagicMock()
|
||
db_ctx.__enter__.return_value = conn
|
||
db_ctx.__exit__.return_value = False
|
||
cursor_ctx.__enter__.return_value = cursor
|
||
cursor_ctx.__exit__.return_value = False
|
||
conn.cursor.return_value = cursor_ctx
|
||
mock_get_db.return_value = db_ctx
|
||
cursor.fetchone.return_value = (42,)
|
||
|
||
mock_preview_ops_job_policy.return_value = {
|
||
"risk_level": "low",
|
||
"approval_required": False,
|
||
"blocked": False,
|
||
"blocking_reasons": [],
|
||
}
|
||
mock_execute_local_job.return_value = (
|
||
True,
|
||
"worker logs collected",
|
||
{"executor": "local-runtime"},
|
||
)
|
||
mock_get_ops_job.return_value = {
|
||
"id": 42,
|
||
"status": "success",
|
||
"execution_mode": "local-runtime",
|
||
}
|
||
|
||
ok, message, data = create_ops_job(
|
||
{
|
||
"action": "logs.collect",
|
||
"target_node_code": settings.node_code,
|
||
"payload": {
|
||
"service_name": "domaincheck-worker",
|
||
"lines": 120,
|
||
},
|
||
"requested_by": "unit-test",
|
||
}
|
||
)
|
||
|
||
self.assertTrue(ok)
|
||
self.assertEqual("worker logs collected", message)
|
||
self.assertTrue(data["executed_immediately"])
|
||
self.assertEqual("local-runtime", data["job"]["execution_mode"])
|
||
mock_execute_local_job.assert_called_once_with(
|
||
"logs.collect",
|
||
{
|
||
"service_name": "domaincheck-worker",
|
||
"lines": 120,
|
||
},
|
||
)
|
||
|
||
insert_call = next(
|
||
call
|
||
for call in cursor.execute.call_args_list
|
||
if "INSERT INTO ops_jobs" in str(call.args[0])
|
||
)
|
||
insert_params = insert_call.args[1]
|
||
self.assertEqual("local-runtime", insert_params[5])
|
||
mock_append_ops_job_event.assert_called_once()
|
||
|
||
|
||
if __name__ == "__main__":
|
||
unittest.main()
|