feat: add ops center and node onboarding flow
This commit is contained in:
391
domain-api/tests/test_ops_job_service.py
Normal file
391
domain-api/tests/test_ops_job_service.py
Normal file
@@ -0,0 +1,391 @@
|
||||
import json
|
||||
import unittest
|
||||
from datetime import datetime
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from app.core.config import settings
|
||||
from app.services.ops_job_service import (
|
||||
_execute_control_plane_job,
|
||||
_serialize_job_row,
|
||||
create_ops_job,
|
||||
dispatch_ops_job,
|
||||
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"])
|
||||
|
||||
@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 = cursor.execute.call_args_list[-1]
|
||||
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()
|
||||
Reference in New Issue
Block a user