feat: add ops center and node onboarding flow
This commit is contained in:
633
domain-api/app/api/routes/ops.py
Normal file
633
domain-api/app/api/routes/ops.py
Normal file
@@ -0,0 +1,633 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.schemas.common import ApiResponse
|
||||
from app.services.ops_agent_service import (
|
||||
build_managed_node_handover_bootstrap_plan,
|
||||
execute_managed_node_onboarding_bootstrap,
|
||||
execute_managed_node_onboarding_acceptance,
|
||||
execute_managed_node_onboarding_recovery,
|
||||
get_managed_node_delivery_queue,
|
||||
get_managed_node_handover,
|
||||
get_managed_node_onboarding,
|
||||
list_managed_node_delivery_queue_records,
|
||||
list_managed_nodes_with_agent_state,
|
||||
list_ops_job_events,
|
||||
preview_managed_node_onboarding_bootstrap,
|
||||
preview_managed_node_onboarding_acceptance,
|
||||
preview_managed_node_onboarding_recovery,
|
||||
request_managed_node_delivery_queue_action,
|
||||
summarize_ops_job_events,
|
||||
)
|
||||
from app.services.ops_job_service import (
|
||||
approve_ops_job,
|
||||
cancel_ops_job,
|
||||
create_ops_job,
|
||||
create_ops_job_batch,
|
||||
dispatch_ops_job,
|
||||
get_ops_job,
|
||||
list_ops_jobs,
|
||||
sync_managed_nodes_from_cluster,
|
||||
upsert_managed_node,
|
||||
)
|
||||
from app.services.ops_playbook_service import (
|
||||
cancel_ops_playbook_run,
|
||||
execute_ops_playbook,
|
||||
get_ops_playbook_run,
|
||||
get_ops_playbooks,
|
||||
get_recent_ops_playbook_runs,
|
||||
list_ops_playbook_run_events,
|
||||
preview_ops_playbook,
|
||||
rerun_ops_playbook_run,
|
||||
)
|
||||
from app.services.ops_policy_service import preview_ops_job_policy
|
||||
from app.services.ops_service import (
|
||||
execute_codex_action,
|
||||
execute_resolved_driver_action,
|
||||
get_ops_activity_stream,
|
||||
get_ops_codex_brief,
|
||||
get_ops_contract_detail,
|
||||
get_ops_contract_registry,
|
||||
get_ops_doctor_decision,
|
||||
get_ops_driver_feed,
|
||||
get_ops_go_live_summary,
|
||||
get_ops_go_live_signoff,
|
||||
get_ops_go_live_bundle,
|
||||
get_ops_go_live_review,
|
||||
get_ops_link_snapshot,
|
||||
get_ops_node_scene_log,
|
||||
get_ops_stack_diagnosis,
|
||||
execute_driver_action,
|
||||
preview_driver_action,
|
||||
resolve_codex_action,
|
||||
resolve_driver_action_request,
|
||||
resolve_ops_runbook_sequence,
|
||||
execute_ops_runbook_sequence,
|
||||
get_ops_blueprint,
|
||||
get_ops_capabilities,
|
||||
get_ops_inspection_overview,
|
||||
get_ops_overview,
|
||||
get_ops_runbook,
|
||||
)
|
||||
from app.services.ops_template_service import get_ops_action_templates
|
||||
|
||||
router = APIRouter(tags=["ops"])
|
||||
|
||||
|
||||
@router.get("/ops/overview", response_model=ApiResponse)
|
||||
def ops_overview() -> ApiResponse:
|
||||
return ApiResponse(data=get_ops_overview())
|
||||
|
||||
|
||||
@router.get("/ops/link-snapshot", response_model=ApiResponse)
|
||||
def ops_link_snapshot() -> ApiResponse:
|
||||
return ApiResponse(data=get_ops_link_snapshot())
|
||||
|
||||
|
||||
@router.get("/ops/stack-diagnosis", response_model=ApiResponse)
|
||||
def ops_stack_diagnosis(base_url: str = "") -> ApiResponse:
|
||||
return ApiResponse(data=get_ops_stack_diagnosis(base_url=base_url))
|
||||
|
||||
|
||||
@router.get("/ops/go-live-summary", response_model=ApiResponse)
|
||||
def ops_go_live_summary(base_url: str = "") -> ApiResponse:
|
||||
return ApiResponse(data=get_ops_go_live_summary(base_url=base_url))
|
||||
|
||||
|
||||
@router.get("/ops/doctor-decision", response_model=ApiResponse)
|
||||
def ops_doctor_decision(base_url: str = "", report_dir: str = "") -> ApiResponse:
|
||||
return ApiResponse(data=get_ops_doctor_decision(base_url=base_url, report_dir=report_dir))
|
||||
|
||||
|
||||
@router.get("/ops/go-live-signoff", response_model=ApiResponse)
|
||||
def ops_go_live_signoff(base_url: str = "", report_dir: str = "") -> ApiResponse:
|
||||
return ApiResponse(data=get_ops_go_live_signoff(base_url=base_url, report_dir=report_dir))
|
||||
|
||||
|
||||
@router.get("/ops/go-live-bundle", response_model=ApiResponse)
|
||||
def ops_go_live_bundle(base_url: str = "", report_dir: str = "") -> ApiResponse:
|
||||
return ApiResponse(data=get_ops_go_live_bundle(base_url=base_url, report_dir=report_dir))
|
||||
|
||||
|
||||
@router.get("/ops/go-live-review", response_model=ApiResponse)
|
||||
def ops_go_live_review(base_url: str = "", report_dir: str = "") -> ApiResponse:
|
||||
return ApiResponse(data=get_ops_go_live_review(base_url=base_url, report_dir=report_dir))
|
||||
|
||||
|
||||
@router.get("/ops/inspection-overview", response_model=ApiResponse)
|
||||
def ops_inspection_overview(
|
||||
limit: int = 80,
|
||||
status: str = "",
|
||||
problem_kind: str = "",
|
||||
query: str = "",
|
||||
only_problem: bool = False,
|
||||
only_participating: bool = False,
|
||||
) -> ApiResponse:
|
||||
return ApiResponse(
|
||||
data=get_ops_inspection_overview(
|
||||
fetch_limit=limit,
|
||||
status=status,
|
||||
problem_kind=problem_kind,
|
||||
query=query,
|
||||
only_problem=only_problem,
|
||||
only_participating=only_participating,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@router.get("/ops/activity-stream", response_model=ApiResponse)
|
||||
def ops_activity_stream(
|
||||
limit: int = 18,
|
||||
kind: str = "",
|
||||
status: str = "",
|
||||
execution_mode: str = "",
|
||||
query: str = "",
|
||||
) -> ApiResponse:
|
||||
return ApiResponse(
|
||||
data=get_ops_activity_stream(
|
||||
limit=limit,
|
||||
kind=kind,
|
||||
status=status,
|
||||
execution_mode=execution_mode,
|
||||
query=query,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@router.get("/ops/driver-feed", response_model=ApiResponse)
|
||||
def ops_driver_feed() -> ApiResponse:
|
||||
return ApiResponse(data=get_ops_driver_feed())
|
||||
|
||||
|
||||
@router.get("/ops/codex-brief", response_model=ApiResponse)
|
||||
def ops_codex_brief() -> ApiResponse:
|
||||
return ApiResponse(data=get_ops_codex_brief())
|
||||
|
||||
|
||||
@router.get("/ops/capabilities", response_model=ApiResponse)
|
||||
def ops_capabilities() -> ApiResponse:
|
||||
return ApiResponse(data=get_ops_capabilities())
|
||||
|
||||
|
||||
@router.get("/ops/contracts", response_model=ApiResponse)
|
||||
def ops_contracts() -> ApiResponse:
|
||||
return ApiResponse(data=get_ops_contract_registry())
|
||||
|
||||
|
||||
@router.get("/ops/contracts/{contract_key}", response_model=ApiResponse)
|
||||
def ops_contract_detail(contract_key: str) -> ApiResponse:
|
||||
data = get_ops_contract_detail(contract_key)
|
||||
if not data:
|
||||
return ApiResponse(code=1, message="contract 不存在", data={})
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/ops/action-templates", response_model=ApiResponse)
|
||||
def ops_action_templates() -> ApiResponse:
|
||||
return ApiResponse(data=get_ops_action_templates())
|
||||
|
||||
|
||||
@router.get("/ops/playbooks", response_model=ApiResponse)
|
||||
def ops_playbooks() -> ApiResponse:
|
||||
return ApiResponse(data=get_ops_playbooks())
|
||||
|
||||
|
||||
@router.get("/ops/playbook-runs", response_model=ApiResponse)
|
||||
def ops_playbook_runs(
|
||||
limit: int = 12,
|
||||
status: str = "",
|
||||
group_key: str = "",
|
||||
query: str = "",
|
||||
) -> ApiResponse:
|
||||
return ApiResponse(
|
||||
data=get_recent_ops_playbook_runs(
|
||||
limit=limit,
|
||||
status=status,
|
||||
group_key=group_key,
|
||||
query=query,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@router.get("/ops/playbook-runs/{run_code}", response_model=ApiResponse)
|
||||
def ops_playbook_run_detail(run_code: str) -> ApiResponse:
|
||||
data = get_ops_playbook_run(run_code)
|
||||
if not data:
|
||||
return ApiResponse(code=1, message="playbook run 不存在", data={})
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/ops/playbook-runs/{run_code}/events", response_model=ApiResponse)
|
||||
def ops_playbook_run_events(
|
||||
run_code: str,
|
||||
limit: int = 80,
|
||||
step_key: str = "",
|
||||
node_code: str = "",
|
||||
) -> ApiResponse:
|
||||
data = list_ops_playbook_run_events(
|
||||
run_code,
|
||||
limit=limit,
|
||||
step_key=step_key,
|
||||
node_code=node_code,
|
||||
)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.post("/ops/playbook-runs/{run_code}/rerun", response_model=ApiResponse)
|
||||
def ops_playbook_run_rerun(run_code: str, payload: dict | None = None) -> ApiResponse:
|
||||
payload = payload or {}
|
||||
ok, message, data = rerun_ops_playbook_run(
|
||||
run_code,
|
||||
requested_by=str(payload.get("requested_by") or "api").strip() or "api",
|
||||
)
|
||||
return ApiResponse(code=0 if ok else 1, message=message, data=data)
|
||||
|
||||
|
||||
@router.post("/ops/playbook-runs/{run_code}/cancel", response_model=ApiResponse)
|
||||
def ops_playbook_run_cancel(run_code: str, payload: dict | None = None) -> ApiResponse:
|
||||
payload = payload or {}
|
||||
ok, message, data = cancel_ops_playbook_run(
|
||||
run_code,
|
||||
cancelled_by=str(payload.get("cancelled_by") or "api").strip() or "api",
|
||||
reason=str(payload.get("reason") or "").strip(),
|
||||
)
|
||||
return ApiResponse(code=0 if ok else 1, message=message, data=data)
|
||||
|
||||
|
||||
@router.post("/ops/playbooks/preview", response_model=ApiResponse)
|
||||
def ops_playbook_preview(payload: dict) -> ApiResponse:
|
||||
ok, message, data = preview_ops_playbook(payload or {})
|
||||
return ApiResponse(code=0 if ok else 1, message=message, data=data)
|
||||
|
||||
|
||||
@router.post("/ops/playbooks/execute", response_model=ApiResponse)
|
||||
def ops_playbook_execute(payload: dict) -> ApiResponse:
|
||||
ok, message, data = execute_ops_playbook(payload or {})
|
||||
return ApiResponse(code=0 if ok else 1, message=message, data=data)
|
||||
|
||||
|
||||
@router.get("/ops/blueprint", response_model=ApiResponse)
|
||||
def ops_blueprint() -> ApiResponse:
|
||||
return ApiResponse(data=get_ops_blueprint())
|
||||
|
||||
|
||||
@router.get("/ops/runbook", response_model=ApiResponse)
|
||||
def ops_runbook() -> ApiResponse:
|
||||
return ApiResponse(data=get_ops_runbook())
|
||||
|
||||
|
||||
@router.post("/ops/runbook/sequences/{sequence_key}/resolve", response_model=ApiResponse)
|
||||
def ops_runbook_sequence_resolve(sequence_key: str, payload: dict | None = None) -> ApiResponse:
|
||||
ok, message, data = resolve_ops_runbook_sequence(sequence_key, payload or {})
|
||||
return ApiResponse(code=0 if ok else 1, message=message, data=data)
|
||||
|
||||
|
||||
@router.post("/ops/runbook/sequences/{sequence_key}/execute", response_model=ApiResponse)
|
||||
def ops_runbook_sequence_execute(sequence_key: str, payload: dict | None = None) -> ApiResponse:
|
||||
ok, message, data = execute_ops_runbook_sequence(sequence_key, payload or {})
|
||||
return ApiResponse(code=0 if ok else 1, message=message, data=data)
|
||||
|
||||
|
||||
@router.get("/ops/nodes", response_model=ApiResponse)
|
||||
def ops_nodes() -> ApiResponse:
|
||||
return ApiResponse(data=list_managed_nodes_with_agent_state())
|
||||
|
||||
|
||||
@router.get("/ops/nodes/{node_code}/handover", response_model=ApiResponse)
|
||||
def ops_node_handover(
|
||||
node_code: str,
|
||||
control_plane_base_url: str = "",
|
||||
root_dir: str = "",
|
||||
) -> ApiResponse:
|
||||
data = get_managed_node_handover(
|
||||
node_code,
|
||||
control_plane_base_url=control_plane_base_url,
|
||||
root_dir=root_dir,
|
||||
)
|
||||
if not data:
|
||||
return ApiResponse(code=1, message="节点不存在", data={})
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/ops/nodes/{node_code}/onboarding", response_model=ApiResponse)
|
||||
def ops_node_onboarding(
|
||||
node_code: str,
|
||||
control_plane_base_url: str = "",
|
||||
root_dir: str = "",
|
||||
) -> ApiResponse:
|
||||
data = get_managed_node_onboarding(
|
||||
node_code,
|
||||
control_plane_base_url=control_plane_base_url,
|
||||
root_dir=root_dir,
|
||||
)
|
||||
if not data:
|
||||
return ApiResponse(code=1, message="节点不存在", data={})
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.post("/ops/nodes/{node_code}/onboarding/acceptance/preview", response_model=ApiResponse)
|
||||
def ops_node_onboarding_acceptance_preview(node_code: str, payload: dict | None = None) -> ApiResponse:
|
||||
normalized_payload = payload or {}
|
||||
ok, message, data = preview_managed_node_onboarding_acceptance(
|
||||
node_code=node_code,
|
||||
requested_by=str(normalized_payload.get("requested_by") or "api").strip() or "api",
|
||||
control_plane_base_url=str(normalized_payload.get("control_plane_base_url") or "").strip(),
|
||||
root_dir=str(normalized_payload.get("root_dir") or "").strip(),
|
||||
)
|
||||
return ApiResponse(code=0 if ok else 1, message=message, data=data)
|
||||
|
||||
|
||||
@router.post("/ops/nodes/{node_code}/onboarding/bootstrap/preview", response_model=ApiResponse)
|
||||
def ops_node_onboarding_bootstrap_preview(node_code: str, payload: dict | None = None) -> ApiResponse:
|
||||
normalized_payload = payload or {}
|
||||
ok, message, data = preview_managed_node_onboarding_bootstrap(
|
||||
node_code=node_code,
|
||||
requested_by=str(normalized_payload.get("requested_by") or "api").strip() or "api",
|
||||
control_plane_base_url=str(normalized_payload.get("control_plane_base_url") or "").strip(),
|
||||
root_dir=str(normalized_payload.get("root_dir") or "").strip(),
|
||||
)
|
||||
return ApiResponse(code=0 if ok else 1, message=message, data=data)
|
||||
|
||||
|
||||
@router.post("/ops/nodes/{node_code}/onboarding/bootstrap/execute", response_model=ApiResponse)
|
||||
def ops_node_onboarding_bootstrap_execute(node_code: str, payload: dict | None = None) -> ApiResponse:
|
||||
normalized_payload = payload or {}
|
||||
ok, message, data = execute_managed_node_onboarding_bootstrap(
|
||||
node_code=node_code,
|
||||
requested_by=str(normalized_payload.get("requested_by") or "api").strip() or "api",
|
||||
control_plane_base_url=str(normalized_payload.get("control_plane_base_url") or "").strip(),
|
||||
root_dir=str(normalized_payload.get("root_dir") or "").strip(),
|
||||
)
|
||||
return ApiResponse(code=0 if ok else 1, message=message, data=data)
|
||||
|
||||
|
||||
@router.post("/ops/nodes/{node_code}/onboarding/acceptance/execute", response_model=ApiResponse)
|
||||
def ops_node_onboarding_acceptance_execute(node_code: str, payload: dict | None = None) -> ApiResponse:
|
||||
normalized_payload = payload or {}
|
||||
ok, message, data = execute_managed_node_onboarding_acceptance(
|
||||
node_code=node_code,
|
||||
requested_by=str(normalized_payload.get("requested_by") or "api").strip() or "api",
|
||||
control_plane_base_url=str(normalized_payload.get("control_plane_base_url") or "").strip(),
|
||||
root_dir=str(normalized_payload.get("root_dir") or "").strip(),
|
||||
)
|
||||
return ApiResponse(code=0 if ok else 1, message=message, data=data)
|
||||
|
||||
|
||||
@router.post("/ops/nodes/{node_code}/onboarding/recovery/preview", response_model=ApiResponse)
|
||||
def ops_node_onboarding_recovery_preview(node_code: str, payload: dict | None = None) -> ApiResponse:
|
||||
normalized_payload = payload or {}
|
||||
ok, message, data = preview_managed_node_onboarding_recovery(
|
||||
node_code=node_code,
|
||||
requested_by=str(normalized_payload.get("requested_by") or "api").strip() or "api",
|
||||
control_plane_base_url=str(normalized_payload.get("control_plane_base_url") or "").strip(),
|
||||
root_dir=str(normalized_payload.get("root_dir") or "").strip(),
|
||||
)
|
||||
return ApiResponse(code=0 if ok else 1, message=message, data=data)
|
||||
|
||||
|
||||
@router.post("/ops/nodes/{node_code}/onboarding/recovery/execute", response_model=ApiResponse)
|
||||
def ops_node_onboarding_recovery_execute(node_code: str, payload: dict | None = None) -> ApiResponse:
|
||||
normalized_payload = payload or {}
|
||||
ok, message, data = execute_managed_node_onboarding_recovery(
|
||||
node_code=node_code,
|
||||
requested_by=str(normalized_payload.get("requested_by") or "api").strip() or "api",
|
||||
control_plane_base_url=str(normalized_payload.get("control_plane_base_url") or "").strip(),
|
||||
root_dir=str(normalized_payload.get("root_dir") or "").strip(),
|
||||
)
|
||||
return ApiResponse(code=0 if ok else 1, message=message, data=data)
|
||||
|
||||
|
||||
@router.get("/ops/nodes/{node_code}/scene-log", response_model=ApiResponse)
|
||||
def ops_node_scene_log(
|
||||
node_code: str,
|
||||
limit: int = 80,
|
||||
mode: str = "",
|
||||
) -> ApiResponse:
|
||||
data = get_ops_node_scene_log(node_code, limit=limit, mode=mode)
|
||||
if not data:
|
||||
return ApiResponse(code=1, message="节点不存在", data={})
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.post("/ops/nodes/{node_code}/handover/bootstrap-plan", response_model=ApiResponse)
|
||||
def ops_node_handover_bootstrap_plan(node_code: str, payload: dict | None = None) -> ApiResponse:
|
||||
normalized_payload = payload or {}
|
||||
ok, message, data = build_managed_node_handover_bootstrap_plan(
|
||||
node_code=node_code,
|
||||
issued_by=str(normalized_payload.get("issued_by") or "api").strip() or "api",
|
||||
expires_in_hours=int(normalized_payload.get("expires_in_hours") or 72),
|
||||
control_plane_base_url=str(normalized_payload.get("control_plane_base_url") or "").strip(),
|
||||
root_dir=str(normalized_payload.get("root_dir") or "").strip(),
|
||||
metadata=normalized_payload.get("metadata") or {},
|
||||
)
|
||||
return ApiResponse(code=0 if ok else 1, message=message, data=data)
|
||||
|
||||
|
||||
@router.get("/ops/nodes/{node_code}/delivery-queue", response_model=ApiResponse)
|
||||
def ops_node_delivery_queue(node_code: str) -> ApiResponse:
|
||||
data = get_managed_node_delivery_queue(node_code)
|
||||
if not data:
|
||||
return ApiResponse(code=1, message="节点不存在", data={})
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/ops/nodes/{node_code}/delivery-queue/records", response_model=ApiResponse)
|
||||
def ops_node_delivery_queue_records(
|
||||
node_code: str,
|
||||
state: str = "",
|
||||
limit: int = 50,
|
||||
) -> ApiResponse:
|
||||
data = list_managed_node_delivery_queue_records(
|
||||
node_code,
|
||||
state=state,
|
||||
limit=limit,
|
||||
)
|
||||
if not data:
|
||||
return ApiResponse(code=1, message="节点不存在", data={})
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.post("/ops/nodes/{node_code}/delivery-queue/flush", response_model=ApiResponse)
|
||||
def ops_node_delivery_queue_flush(node_code: str, payload: dict | None = None) -> ApiResponse:
|
||||
ok, message, data = request_managed_node_delivery_queue_action(
|
||||
node_code,
|
||||
"delivery.queue.flush",
|
||||
payload=payload or {},
|
||||
)
|
||||
return ApiResponse(code=0 if ok else 1, message=message, data=data)
|
||||
|
||||
|
||||
@router.post("/ops/nodes/{node_code}/delivery-queue/replay", response_model=ApiResponse)
|
||||
def ops_node_delivery_queue_replay(node_code: str, payload: dict | None = None) -> ApiResponse:
|
||||
ok, message, data = request_managed_node_delivery_queue_action(
|
||||
node_code,
|
||||
"delivery.queue.replay",
|
||||
payload=payload or {},
|
||||
)
|
||||
return ApiResponse(code=0 if ok else 1, message=message, data=data)
|
||||
|
||||
|
||||
@router.post("/ops/nodes/{node_code}/delivery-queue/records/{record_id}/replay", response_model=ApiResponse)
|
||||
def ops_node_delivery_queue_record_replay(
|
||||
node_code: str,
|
||||
record_id: str,
|
||||
payload: dict | None = None,
|
||||
) -> ApiResponse:
|
||||
ok, message, data = request_managed_node_delivery_queue_action(
|
||||
node_code,
|
||||
"delivery.queue.replay",
|
||||
payload=payload or {},
|
||||
record_id=record_id,
|
||||
)
|
||||
return ApiResponse(code=0 if ok else 1, message=message, data=data)
|
||||
|
||||
|
||||
@router.post("/ops/nodes/{node_code}/delivery-queue/records/{record_id}/discard", response_model=ApiResponse)
|
||||
def ops_node_delivery_queue_record_discard(
|
||||
node_code: str,
|
||||
record_id: str,
|
||||
payload: dict | None = None,
|
||||
) -> ApiResponse:
|
||||
ok, message, data = request_managed_node_delivery_queue_action(
|
||||
node_code,
|
||||
"delivery.queue.discard",
|
||||
payload=payload or {},
|
||||
record_id=record_id,
|
||||
)
|
||||
return ApiResponse(code=0 if ok else 1, message=message, data=data)
|
||||
|
||||
|
||||
@router.post("/ops/nodes", response_model=ApiResponse)
|
||||
def ops_nodes_upsert(payload: dict) -> ApiResponse:
|
||||
ok, message, data = upsert_managed_node(payload)
|
||||
return ApiResponse(code=0 if ok else 1, message=message, data=data)
|
||||
|
||||
|
||||
@router.post("/ops/nodes/sync-from-cluster", response_model=ApiResponse)
|
||||
def ops_nodes_sync_from_cluster(dry_run: bool = False) -> ApiResponse:
|
||||
return ApiResponse(data=sync_managed_nodes_from_cluster(dry_run=dry_run))
|
||||
|
||||
|
||||
@router.get("/ops/jobs", response_model=ApiResponse)
|
||||
def ops_jobs(limit: int = 20, compact: bool = True, rollout_id: int = 0) -> ApiResponse:
|
||||
normalized_rollout_id = int(rollout_id or 0)
|
||||
jobs = list_ops_jobs(
|
||||
limit=limit,
|
||||
compact=compact,
|
||||
rollout_id=normalized_rollout_id if normalized_rollout_id > 0 else None,
|
||||
)
|
||||
page_status_counts: dict[str, int] = {}
|
||||
for item in jobs:
|
||||
item_status = str(item.get("status") or "").strip() or "unknown"
|
||||
page_status_counts[item_status] = int(page_status_counts.get(item_status, 0) or 0) + 1
|
||||
return ApiResponse(
|
||||
data={
|
||||
"jobs": jobs,
|
||||
"filters": {
|
||||
"limit": int(limit or 20),
|
||||
"compact": bool(compact),
|
||||
"rollout_id": normalized_rollout_id,
|
||||
},
|
||||
"page_summary": {
|
||||
"jobs_total": len(jobs),
|
||||
"status_counts": page_status_counts,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@router.get("/ops/jobs/{job_id}", response_model=ApiResponse)
|
||||
def ops_job_detail(job_id: int) -> ApiResponse:
|
||||
data = get_ops_job(job_id)
|
||||
if not data:
|
||||
return ApiResponse(code=1, message="任务不存在", data={})
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/ops/jobs/{job_id}/events", response_model=ApiResponse)
|
||||
def ops_job_events(job_id: int, limit: int = 50) -> ApiResponse:
|
||||
events = list_ops_job_events(job_id, limit=limit)
|
||||
return ApiResponse(
|
||||
data={
|
||||
"events": events,
|
||||
"summary": summarize_ops_job_events(events, job_id=job_id, limit=limit),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@router.post("/ops/driver-actions/execute", response_model=ApiResponse)
|
||||
def ops_driver_action_execute(payload: dict) -> ApiResponse:
|
||||
ok, message, data = execute_driver_action(payload or {})
|
||||
return ApiResponse(code=0 if ok else 1, message=message, data=data)
|
||||
|
||||
|
||||
@router.post("/ops/driver-actions/preview", response_model=ApiResponse)
|
||||
def ops_driver_action_preview(payload: dict) -> ApiResponse:
|
||||
ok, message, data = preview_driver_action(payload or {})
|
||||
return ApiResponse(code=0 if ok else 1, message=message, data=data)
|
||||
|
||||
|
||||
@router.post("/ops/driver-actions/resolve", response_model=ApiResponse)
|
||||
def ops_driver_action_resolve(payload: dict | None = None) -> ApiResponse:
|
||||
ok, message, data = resolve_driver_action_request(payload or {})
|
||||
return ApiResponse(code=0 if ok else 1, message=message, data=data)
|
||||
|
||||
|
||||
@router.post("/ops/driver-actions/execute-resolved", response_model=ApiResponse)
|
||||
def ops_driver_action_execute_resolved(payload: dict | None = None) -> ApiResponse:
|
||||
ok, message, data = execute_resolved_driver_action(payload or {})
|
||||
return ApiResponse(code=0 if ok else 1, message=message, data=data)
|
||||
|
||||
|
||||
@router.post("/ops/codex-actions/resolve", response_model=ApiResponse)
|
||||
def ops_codex_action_resolve(payload: dict | None = None) -> ApiResponse:
|
||||
ok, message, data = resolve_codex_action(payload or {})
|
||||
return ApiResponse(code=0 if ok else 1, message=message, data=data)
|
||||
|
||||
|
||||
@router.post("/ops/codex-actions/execute", response_model=ApiResponse)
|
||||
def ops_codex_action_execute(payload: dict | None = None) -> ApiResponse:
|
||||
ok, message, data = execute_codex_action(payload or {})
|
||||
return ApiResponse(code=0 if ok else 1, message=message, data=data)
|
||||
|
||||
|
||||
@router.post("/ops/jobs", response_model=ApiResponse)
|
||||
def ops_jobs_create(payload: dict) -> ApiResponse:
|
||||
ok, message, data = create_ops_job(payload)
|
||||
return ApiResponse(code=0 if ok else 1, message=message, data=data)
|
||||
|
||||
|
||||
@router.post("/ops/jobs/batch", response_model=ApiResponse)
|
||||
def ops_jobs_create_batch(payload: dict) -> ApiResponse:
|
||||
ok, message, data = create_ops_job_batch(payload)
|
||||
return ApiResponse(code=0 if ok else 1, message=message, data=data)
|
||||
|
||||
|
||||
@router.post("/ops/policy/preview", response_model=ApiResponse)
|
||||
def ops_policy_preview(payload: dict) -> ApiResponse:
|
||||
return ApiResponse(data=preview_ops_job_policy(payload))
|
||||
|
||||
|
||||
@router.post("/ops/jobs/{job_id}/approve", response_model=ApiResponse)
|
||||
def ops_job_approve(job_id: int, payload: dict | None = None) -> ApiResponse:
|
||||
payload = payload or {}
|
||||
ok, message, data = approve_ops_job(job_id, approved_by=str(payload.get("approved_by") or "api").strip() or "api")
|
||||
return ApiResponse(code=0 if ok else 1, message=message, data=data)
|
||||
|
||||
|
||||
@router.post("/ops/jobs/{job_id}/cancel", response_model=ApiResponse)
|
||||
def ops_job_cancel(job_id: int, payload: dict | None = None) -> ApiResponse:
|
||||
payload = payload or {}
|
||||
ok, message, data = cancel_ops_job(
|
||||
job_id,
|
||||
cancelled_by=str(payload.get("cancelled_by") or "api").strip() or "api",
|
||||
reason=str(payload.get("reason") or "").strip(),
|
||||
)
|
||||
return ApiResponse(code=0 if ok else 1, message=message, data=data)
|
||||
|
||||
|
||||
@router.post("/ops/jobs/{job_id}/dispatch", response_model=ApiResponse)
|
||||
def ops_job_dispatch(job_id: int) -> ApiResponse:
|
||||
ok, message, data = dispatch_ops_job(job_id)
|
||||
return ApiResponse(code=0 if ok else 1, message=message, data=data)
|
||||
90
domain-api/app/api/routes/ops_agent.py
Normal file
90
domain-api/app/api/routes/ops_agent.py
Normal file
@@ -0,0 +1,90 @@
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Header
|
||||
|
||||
from app.schemas.common import ApiResponse
|
||||
from app.services.ops_agent_service import (
|
||||
agent_append_job_event,
|
||||
agent_complete_job,
|
||||
agent_heartbeat,
|
||||
agent_mark_job_started,
|
||||
agent_pull_jobs,
|
||||
agent_register,
|
||||
build_node_agent_bootstrap_plan,
|
||||
issue_node_agent_token,
|
||||
)
|
||||
|
||||
router = APIRouter(tags=["ops-agent"])
|
||||
|
||||
|
||||
def _resolve_agent_token(x_domaincheck_agent_token: Optional[str]) -> str:
|
||||
return str(x_domaincheck_agent_token or "").strip()
|
||||
|
||||
|
||||
def _build_agent_response(ok: bool, message: str, data: object) -> ApiResponse:
|
||||
detail_code = None
|
||||
if isinstance(data, dict):
|
||||
detail_code = str(data.get("detail_code") or "").strip() or None
|
||||
return ApiResponse(code=0 if ok else 1, message=message, data=data, detail_code=detail_code)
|
||||
|
||||
|
||||
@router.post("/ops/agent/tokens", response_model=ApiResponse)
|
||||
def ops_agent_issue_token(payload: dict) -> ApiResponse:
|
||||
ok, message, data = issue_node_agent_token(
|
||||
node_code=str(payload.get("node_code") or "").strip(),
|
||||
issued_by=str(payload.get("issued_by") or "api").strip() or "api",
|
||||
expires_in_hours=int(payload.get("expires_in_hours") or 72),
|
||||
metadata=payload.get("metadata") or {},
|
||||
)
|
||||
return _build_agent_response(ok, message, data)
|
||||
|
||||
|
||||
@router.post("/ops/agent/bootstrap-plan", response_model=ApiResponse)
|
||||
def ops_agent_bootstrap_plan(payload: dict) -> ApiResponse:
|
||||
ok, message, data = build_node_agent_bootstrap_plan(
|
||||
node_code=str(payload.get("node_code") or "").strip(),
|
||||
node_region=str(payload.get("node_region") or "mainland").strip() or "mainland",
|
||||
node_role=str(payload.get("node_role") or "worker").strip() or "worker",
|
||||
issued_by=str(payload.get("issued_by") or "api").strip() or "api",
|
||||
expires_in_hours=int(payload.get("expires_in_hours") or 72),
|
||||
control_plane_base_url=str(payload.get("control_plane_base_url") or "").strip(),
|
||||
root_dir=str(payload.get("root_dir") or "/opt/domaincheck").strip() or "/opt/domaincheck",
|
||||
metadata=payload.get("metadata") or {},
|
||||
)
|
||||
return _build_agent_response(ok, message, data)
|
||||
|
||||
|
||||
@router.post("/ops/agent/register", response_model=ApiResponse)
|
||||
def ops_agent_register(payload: dict, x_domaincheck_agent_token: Optional[str] = Header(default=None)) -> ApiResponse:
|
||||
ok, message, data = agent_register(payload, token=_resolve_agent_token(x_domaincheck_agent_token))
|
||||
return _build_agent_response(ok, message, data)
|
||||
|
||||
|
||||
@router.post("/ops/agent/heartbeat", response_model=ApiResponse)
|
||||
def ops_agent_heartbeat(payload: dict, x_domaincheck_agent_token: Optional[str] = Header(default=None)) -> ApiResponse:
|
||||
ok, message, data = agent_heartbeat(payload, token=_resolve_agent_token(x_domaincheck_agent_token))
|
||||
return _build_agent_response(ok, message, data)
|
||||
|
||||
|
||||
@router.post("/ops/agent/pull", response_model=ApiResponse)
|
||||
def ops_agent_pull(payload: dict, limit: int = 1, x_domaincheck_agent_token: Optional[str] = Header(default=None)) -> ApiResponse:
|
||||
ok, message, data = agent_pull_jobs(payload, token=_resolve_agent_token(x_domaincheck_agent_token), limit=limit)
|
||||
return _build_agent_response(ok, message, data)
|
||||
|
||||
|
||||
@router.post("/ops/agent/jobs/{job_id}/start", response_model=ApiResponse)
|
||||
def ops_agent_job_start(job_id: int, payload: dict, x_domaincheck_agent_token: Optional[str] = Header(default=None)) -> ApiResponse:
|
||||
ok, message, data = agent_mark_job_started(job_id, payload, token=_resolve_agent_token(x_domaincheck_agent_token))
|
||||
return _build_agent_response(ok, message, data)
|
||||
|
||||
|
||||
@router.post("/ops/agent/jobs/{job_id}/complete", response_model=ApiResponse)
|
||||
def ops_agent_job_complete(job_id: int, payload: dict, x_domaincheck_agent_token: Optional[str] = Header(default=None)) -> ApiResponse:
|
||||
ok, message, data = agent_complete_job(job_id, payload, token=_resolve_agent_token(x_domaincheck_agent_token))
|
||||
return _build_agent_response(ok, message, data)
|
||||
|
||||
|
||||
@router.post("/ops/agent/jobs/{job_id}/events", response_model=ApiResponse)
|
||||
def ops_agent_job_event(job_id: int, payload: dict, x_domaincheck_agent_token: Optional[str] = Header(default=None)) -> ApiResponse:
|
||||
ok, message, data = agent_append_job_event(job_id, payload, token=_resolve_agent_token(x_domaincheck_agent_token))
|
||||
return _build_agent_response(ok, message, data)
|
||||
175
domain-api/app/api/routes/ops_release.py
Normal file
175
domain-api/app/api/routes/ops_release.py
Normal file
@@ -0,0 +1,175 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi.responses import FileResponse
|
||||
|
||||
from app.schemas.common import ApiResponse
|
||||
from app.services.ops_release_service import (
|
||||
advance_release_rollout,
|
||||
activate_release,
|
||||
create_release,
|
||||
create_release_and_smart_rollout_from_latest_package,
|
||||
create_release_from_latest_package,
|
||||
create_release_rollout,
|
||||
create_smart_release_rollout,
|
||||
get_latest_release_package_metadata,
|
||||
get_release_launchpad,
|
||||
get_latest_release,
|
||||
get_release_package_archive_path,
|
||||
get_release_package_sha256_path,
|
||||
get_release,
|
||||
get_release_rollout,
|
||||
list_release_rollout_jobs,
|
||||
list_release_rollouts,
|
||||
list_releases,
|
||||
preview_release_from_latest_package,
|
||||
preview_smart_release_rollout,
|
||||
preview_smart_release_rollout_from_latest_package,
|
||||
)
|
||||
|
||||
router = APIRouter(tags=["ops-release"])
|
||||
|
||||
|
||||
@router.get("/ops/releases", response_model=ApiResponse)
|
||||
def ops_releases(limit: int = 20, channel: str | None = None) -> ApiResponse:
|
||||
return ApiResponse(data={"releases": list_releases(limit=limit, channel=channel)})
|
||||
|
||||
|
||||
@router.get("/ops/releases/latest", response_model=ApiResponse)
|
||||
def ops_releases_latest(channel: str = "stable") -> ApiResponse:
|
||||
return ApiResponse(data=get_latest_release(channel=channel))
|
||||
|
||||
|
||||
@router.get("/ops/releases/package-metadata/latest", response_model=ApiResponse)
|
||||
def ops_releases_package_metadata_latest() -> ApiResponse:
|
||||
return ApiResponse(data=get_latest_release_package_metadata())
|
||||
|
||||
|
||||
@router.get("/ops/releases/launchpad", response_model=ApiResponse)
|
||||
def ops_releases_launchpad(request: Request, channel: str = "stable") -> ApiResponse:
|
||||
derived_base_url = str(request.base_url).rstrip("/")
|
||||
return ApiResponse(data=get_release_launchpad(control_plane_base_url=derived_base_url, channel=channel))
|
||||
|
||||
|
||||
@router.get("/ops/releases/packages/{package_name}/download")
|
||||
def ops_release_package_download(package_name: str):
|
||||
try:
|
||||
archive_path = get_release_package_archive_path(package_name)
|
||||
except ValueError:
|
||||
return ApiResponse(code=1, message="发布包名称不合法", data={})
|
||||
if not archive_path:
|
||||
return ApiResponse(code=1, message="发布包不存在", data={})
|
||||
media_type = "application/octet-stream"
|
||||
archive_name = archive_path.name.lower()
|
||||
if archive_name.endswith(".tar.gz"):
|
||||
media_type = "application/gzip"
|
||||
elif archive_name.endswith(".zip"):
|
||||
media_type = "application/zip"
|
||||
return FileResponse(path=archive_path, filename=archive_path.name, media_type=media_type)
|
||||
|
||||
|
||||
@router.get("/ops/releases/packages/{package_name}/sha256")
|
||||
def ops_release_package_sha256(package_name: str):
|
||||
try:
|
||||
sha256_path = get_release_package_sha256_path(package_name)
|
||||
except ValueError:
|
||||
return ApiResponse(code=1, message="发布包名称不合法", data={})
|
||||
if not sha256_path:
|
||||
return ApiResponse(code=1, message="发布包校验文件不存在", data={})
|
||||
return FileResponse(path=sha256_path, filename=sha256_path.name, media_type="text/plain; charset=utf-8")
|
||||
|
||||
|
||||
@router.get("/ops/releases/{release_id}", response_model=ApiResponse)
|
||||
def ops_releases_detail(release_id: int) -> ApiResponse:
|
||||
data = get_release(release_id)
|
||||
if not data:
|
||||
return ApiResponse(code=1, message="Release 不存在", data={})
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.post("/ops/releases", response_model=ApiResponse)
|
||||
def ops_releases_create(payload: dict) -> ApiResponse:
|
||||
ok, message, data = create_release(payload)
|
||||
return ApiResponse(code=0 if ok else 1, message=message, data=data)
|
||||
|
||||
|
||||
@router.post("/ops/releases/from-package/latest", response_model=ApiResponse)
|
||||
def ops_releases_create_from_latest_package(request: Request, payload: dict | None = None) -> ApiResponse:
|
||||
derived_base_url = str(request.base_url).rstrip("/")
|
||||
ok, message, data = create_release_from_latest_package(payload or {}, control_plane_base_url=derived_base_url)
|
||||
return ApiResponse(code=0 if ok else 1, message=message, data=data)
|
||||
|
||||
|
||||
@router.post("/ops/releases/from-package/latest/preview", response_model=ApiResponse)
|
||||
def ops_releases_preview_from_latest_package(request: Request, payload: dict | None = None) -> ApiResponse:
|
||||
derived_base_url = str(request.base_url).rstrip("/")
|
||||
ok, message, data = preview_release_from_latest_package(payload or {}, control_plane_base_url=derived_base_url)
|
||||
return ApiResponse(code=0 if ok else 1, message=message, data=data)
|
||||
|
||||
|
||||
@router.post("/ops/releases/from-package/latest/smart-rollout", response_model=ApiResponse)
|
||||
def ops_releases_create_from_latest_package_and_smart_rollout(request: Request, payload: dict | None = None) -> ApiResponse:
|
||||
derived_base_url = str(request.base_url).rstrip("/")
|
||||
ok, message, data = create_release_and_smart_rollout_from_latest_package(
|
||||
payload or {},
|
||||
control_plane_base_url=derived_base_url,
|
||||
)
|
||||
return ApiResponse(code=0 if ok else 1, message=message, data=data)
|
||||
|
||||
|
||||
@router.post("/ops/releases/from-package/latest/smart-rollout-preview", response_model=ApiResponse)
|
||||
def ops_releases_preview_from_latest_package_and_smart_rollout(request: Request, payload: dict | None = None) -> ApiResponse:
|
||||
derived_base_url = str(request.base_url).rstrip("/")
|
||||
ok, message, data = preview_smart_release_rollout_from_latest_package(
|
||||
payload or {},
|
||||
control_plane_base_url=derived_base_url,
|
||||
)
|
||||
return ApiResponse(code=0 if ok else 1, message=message, data=data)
|
||||
|
||||
|
||||
@router.post("/ops/releases/{release_id}/activate", response_model=ApiResponse)
|
||||
def ops_releases_activate(release_id: int) -> ApiResponse:
|
||||
ok, message, data = activate_release(release_id)
|
||||
return ApiResponse(code=0 if ok else 1, message=message, data=data)
|
||||
|
||||
|
||||
@router.post("/ops/releases/{release_id}/smart-rollout-preview", response_model=ApiResponse)
|
||||
def ops_releases_smart_rollout_preview(release_id: int, payload: dict | None = None) -> ApiResponse:
|
||||
ok, message, data = preview_smart_release_rollout(release_id, payload or {})
|
||||
return ApiResponse(code=0 if ok else 1, message=message, data=data)
|
||||
|
||||
|
||||
@router.post("/ops/releases/{release_id}/smart-rollout", response_model=ApiResponse)
|
||||
def ops_releases_smart_rollout(release_id: int, payload: dict | None = None) -> ApiResponse:
|
||||
ok, message, data = create_smart_release_rollout(release_id, payload or {})
|
||||
return ApiResponse(code=0 if ok else 1, message=message, data=data)
|
||||
|
||||
|
||||
@router.get("/ops/releases/{release_id}/rollouts", response_model=ApiResponse)
|
||||
def ops_release_rollouts(release_id: int, limit: int = 20) -> ApiResponse:
|
||||
return ApiResponse(data={"rollouts": list_release_rollouts(release_id=release_id, limit=limit)})
|
||||
|
||||
|
||||
@router.get("/ops/rollouts/{rollout_id}", response_model=ApiResponse)
|
||||
def ops_rollout_detail(rollout_id: int) -> ApiResponse:
|
||||
data = get_release_rollout(rollout_id)
|
||||
if not data:
|
||||
return ApiResponse(code=1, message="Rollout 不存在", data={})
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/ops/rollouts/{rollout_id}/jobs", response_model=ApiResponse)
|
||||
def ops_rollout_jobs(rollout_id: int, limit: int = 200) -> ApiResponse:
|
||||
return ApiResponse(data={"jobs": list_release_rollout_jobs(rollout_id, limit=limit)})
|
||||
|
||||
|
||||
@router.post("/ops/rollouts/{rollout_id}/advance", response_model=ApiResponse)
|
||||
def ops_rollout_advance(rollout_id: int, payload: dict | None = None) -> ApiResponse:
|
||||
ok, message, data = advance_release_rollout(rollout_id, payload or {})
|
||||
return ApiResponse(code=0 if ok else 1, message=message, data=data)
|
||||
|
||||
|
||||
@router.post("/ops/releases/{release_id}/rollouts", response_model=ApiResponse)
|
||||
def ops_release_rollouts_create(release_id: int, payload: dict) -> ApiResponse:
|
||||
ok, message, data = create_release_rollout(release_id, payload)
|
||||
return ApiResponse(code=0 if ok else 1, message=message, data=data)
|
||||
@@ -3,6 +3,7 @@ from typing import Optional
|
||||
from fastapi import APIRouter, Header
|
||||
|
||||
from app.schemas.common import ApiResponse
|
||||
from app.services.build_info_service import get_runtime_build_info
|
||||
from app.services.cluster_runtime_service import get_cluster_snapshot
|
||||
from app.services.debug_event_service import get_debug_diagnosis, get_debug_event_overview, get_debug_handoff_report, ingest_debug_event, list_debug_events
|
||||
from app.services.runtime_control_service import runtime_action
|
||||
@@ -37,6 +38,11 @@ def runtime_cluster() -> ApiResponse:
|
||||
return ApiResponse(data=get_cluster_snapshot())
|
||||
|
||||
|
||||
@router.get("/runtime/build-info", response_model=ApiResponse)
|
||||
def runtime_build_info() -> ApiResponse:
|
||||
return ApiResponse(data=get_runtime_build_info())
|
||||
|
||||
|
||||
@router.get("/runtime/sync-summary", response_model=ApiResponse)
|
||||
def runtime_sync_summary() -> ApiResponse:
|
||||
return ApiResponse(data=get_sync_summary())
|
||||
|
||||
Reference in New Issue
Block a user