#!/usr/bin/env bash set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # shellcheck disable=SC1091 source "${SCRIPT_DIR}/lib_ops_center.sh" ops_center_load_config "${SCRIPT_DIR}" usage() { cat <<'EOF' usage: bash domain-api/deploy/multi-region/drive_ops_action.sh [action_payload_json|@payload.json] [requested_by] bash domain-api/deploy/multi-region/drive_ops_action.sh [action_payload_json|@payload.json] [requested_by] examples: bash domain-api/deploy/multi-region/drive_ops_action.sh \ http://127.0.0.1:8100 \ preview \ create_release_rollout_worker \ '{"release_id":12}' bash domain-api/deploy/multi-region/drive_ops_action.sh \ http://127.0.0.1:8100 \ create_release_rollout_worker \ '{"release_id":12}' NODE_CODES="mainland-worker-01,mainland-worker-02" \ bash domain-api/deploy/multi-region/drive_ops_action.sh \ http://127.0.0.1:8100 \ run_standard_inspection \ '{}' notes: - preview/execute 双模式共用同一份 action payload 和 node_codes - 旧用法仍默认按 execute 处理 - NODE_CODES can be passed through env as comma/newline separated node_code list - action_payload_json can also use @/path/to/file.json - preview metadata can be passed through env: SOURCE_LABEL / ACTION_TITLE / ACTION_SUMMARY / ACTION_REASON / ACTION_META_TEXT / PRIMARY_LABEL EOF } BASE_URL="${1:-$(ops_center_resolve_mainland_api_base_url)}" SECOND_ARG="${2:-}" THIRD_ARG="${3:-}" FOURTH_ARG="${4:-}" FIFTH_ARG="${5:-}" if [[ "${SECOND_ARG}" == "preview" || "${SECOND_ARG}" == "execute" ]]; then OPERATION="${SECOND_ARG}" ACTION_CODE="${THIRD_ARG:-}" RAW_ACTION_PAYLOAD="${FOURTH_ARG:-}" REQUESTED_BY="${FIFTH_ARG:-$(ops_center_resolve_requested_by)}" else OPERATION="execute" ACTION_CODE="${SECOND_ARG:-}" RAW_ACTION_PAYLOAD="${THIRD_ARG:-}" REQUESTED_BY="${FOURTH_ARG:-$(ops_center_resolve_requested_by)}" fi if [[ -z "${RAW_ACTION_PAYLOAD}" ]]; then RAW_ACTION_PAYLOAD='{}' fi NODE_CODES_INPUT="${NODE_CODES:-}" SOURCE_LABEL="${SOURCE_LABEL:-cli/drive_ops_action}" ACTION_TITLE="${ACTION_TITLE:-}" ACTION_SUMMARY="${ACTION_SUMMARY:-}" ACTION_REASON="${ACTION_REASON:-}" ACTION_META_TEXT="${ACTION_META_TEXT:-}" PRIMARY_LABEL="${PRIMARY_LABEL:-}" print_json_or_raw() { local raw_text="${1:-}" python3 - "${raw_text}" <<'PY' import json import sys raw_text = str(sys.argv[1] or "") try: payload = json.loads(raw_text) except Exception: print(raw_text) else: print(json.dumps(payload, ensure_ascii=False, indent=2)) PY } request_with_status() { local method="$1" local url="$2" local body="${3:-}" if [[ "${method}" == "POST" ]]; then curl -sS -X POST "${url}" \ -H 'Content-Type: application/json' \ -d "${body}" \ -w $'\nHTTP_STATUS=%{http_code}' else curl -sS "${url}" -w $'\nHTTP_STATUS=%{http_code}' fi } if [[ -z "${ACTION_CODE}" ]]; then usage exit 1 fi if [[ "${OPERATION}" != "preview" && "${OPERATION}" != "execute" ]]; then echo "operation must be preview or execute" >&2 usage exit 1 fi ACTION_PAYLOAD_JSON="${RAW_ACTION_PAYLOAD}" if [[ "${ACTION_PAYLOAD_JSON}" == @* ]]; then ACTION_PAYLOAD_PATH="${ACTION_PAYLOAD_JSON#@}" if [[ ! -f "${ACTION_PAYLOAD_PATH}" ]]; then echo "payload file not found: ${ACTION_PAYLOAD_PATH}" >&2 exit 1 fi ACTION_PAYLOAD_JSON="$(cat "${ACTION_PAYLOAD_PATH}")" fi if [[ -z "${NODE_CODES_INPUT}" ]]; then NODE_CODES_INPUT="$(python3 - <<'PY' "${BASE_URL}" "${ACTION_CODE}" "${ACTION_PAYLOAD_JSON}" import json import sys import urllib.request def normalize_codes(*groups): result = [] for group in groups: for item in list(group or []): node_code = str(item or "").strip() if node_code and node_code not in result: result.append(node_code) return result base_url = str(sys.argv[1] or "").rstrip("/") action_code = str(sys.argv[2] or "").strip() payload_raw = str(sys.argv[3] or "").strip() or "{}" try: payload = json.loads(payload_raw) except Exception: payload = {} if not isinstance(payload, dict): payload = {} payload_node_codes = normalize_codes(payload.get("node_codes") or []) if payload_node_codes: print(",".join(payload_node_codes)) raise SystemExit(0) supported_actions = { "open_worker_logs_participating", "run_inspection_participating", "open_worker_logs_standby", "run_inspection_standby", "open_worker_logs", "run_standard_inspection", } if action_code not in supported_actions or not base_url: print("") raise SystemExit(0) try: with urllib.request.urlopen(f"{base_url}/api/v1/ops/overview", timeout=20) as response: overview_payload = json.loads(response.read().decode("utf-8")) overview = dict((overview_payload or {}).get("data") or {}) except Exception: print("") raise SystemExit(0) execution_scene = dict(overview.get("execution_scene") or {}) participation_summary = dict(execution_scene.get("participation_summary") or {}) log_sync = dict(execution_scene.get("log_sync") or {}) dispatch_active = normalize_codes(participation_summary.get("dispatch_active_node_codes") or []) recent_only = normalize_codes(participation_summary.get("recent_only_node_codes") or []) standby = normalize_codes( participation_summary.get("standby_node_codes") or [], participation_summary.get("non_participating_node_codes") or [], ) missing_logs = normalize_codes(log_sync.get("missing_participating_nodes") or []) resolved_node_codes = [] if action_code in {"open_worker_logs_participating", "run_inspection_participating"}: resolved_node_codes = normalize_codes(missing_logs, dispatch_active, recent_only) elif action_code in {"open_worker_logs_standby", "run_inspection_standby"}: resolved_node_codes = normalize_codes(standby) elif action_code == "open_worker_logs": resolved_node_codes = normalize_codes(missing_logs, dispatch_active, recent_only, standby) elif action_code == "run_standard_inspection": resolved_node_codes = normalize_codes(dispatch_active, recent_only, standby) print(",".join(resolved_node_codes)) PY )" fi REQUEST_JSON="$(python3 - <<'PY' "${ACTION_CODE}" "${ACTION_PAYLOAD_JSON}" "${REQUESTED_BY}" "${NODE_CODES_INPUT}" import json import sys action_code = str(sys.argv[1] or "").strip() payload_raw = str(sys.argv[2] or "").strip() or "{}" requested_by = str(sys.argv[3] or "").strip() or "cli/drive_ops_action" node_codes_raw = str(sys.argv[4] or "") try: action_payload = json.loads(payload_raw) except Exception as exc: # pragma: no cover - cli validation raise SystemExit(f"invalid action payload json: {exc}") if not isinstance(action_payload, dict): raise SystemExit("action payload must be a json object") node_codes = [] seen = set() for item in node_codes_raw.replace("\n", ",").split(","): value = str(item or "").strip() if not value or value in seen: continue seen.add(value) node_codes.append(value) request_payload = { "action_code": action_code, "node_codes": node_codes, "action_payload": action_payload, "requested_by": requested_by, } print(json.dumps(request_payload, ensure_ascii=False)) PY )" PREVIEW_REQUEST_JSON="$(python3 - <<'PY' "${ACTION_CODE}" "${ACTION_PAYLOAD_JSON}" "${REQUESTED_BY}" "${NODE_CODES_INPUT}" "${SOURCE_LABEL}" "${ACTION_TITLE}" "${ACTION_SUMMARY}" "${ACTION_REASON}" "${ACTION_META_TEXT}" "${PRIMARY_LABEL}" import json import sys action_code = str(sys.argv[1] or "").strip() payload_raw = str(sys.argv[2] or "").strip() or "{}" requested_by = str(sys.argv[3] or "").strip() or "cli/drive_ops_action" node_codes_raw = str(sys.argv[4] or "") source_label = str(sys.argv[5] or "").strip() or "cli/drive_ops_action" action_title = str(sys.argv[6] or "").strip() action_summary = str(sys.argv[7] or "").strip() action_reason = str(sys.argv[8] or "").strip() action_meta_text = str(sys.argv[9] or "").strip() primary_label = str(sys.argv[10] or "").strip() try: action_payload = json.loads(payload_raw) except Exception as exc: raise SystemExit(f"invalid action payload json: {exc}") if not isinstance(action_payload, dict): raise SystemExit("action payload must be a json object") node_codes = [] seen = set() for item in node_codes_raw.replace("\n", ",").split(","): value = str(item or "").strip() if not value or value in seen: continue seen.add(value) node_codes.append(value) request_payload = { "source_label": source_label, "title": action_title or primary_label or action_code, "summary": action_summary, "reason": action_reason, "meta_text": action_meta_text, "executor_kind": "driver_action", "primary_label": primary_label or action_code, "primary_action_code": action_code, "primary_node_codes": node_codes, "primary_action_payload": action_payload, "requested_by": requested_by, } print(json.dumps(request_payload, ensure_ascii=False)) PY )" if [[ "${OPERATION}" == "preview" ]]; then echo "[1/3] driver action preview request" print_json_or_raw "${PREVIEW_REQUEST_JSON}" echo echo RAW_HTTP_RESPONSE="$(request_with_status "POST" "${BASE_URL}/api/v1/ops/driver-actions/preview" "${PREVIEW_REQUEST_JSON}")" HTTP_STATUS="${RAW_HTTP_RESPONSE##*$'\n'HTTP_STATUS=}" RESPONSE_JSON="${RAW_HTTP_RESPONSE%$'\n'HTTP_STATUS=*}" echo "[2/3] raw response" printf '%s\n' "http_status=${HTTP_STATUS}" print_json_or_raw "${RESPONSE_JSON}" echo echo echo "[3/3] condensed summary" python3 - "${RESPONSE_JSON}" "${HTTP_STATUS}" <<'PY' import json import sys payload = json.loads(sys.argv[1]) http_status = int(sys.argv[2] or 0) data = payload.get("data") or {} primary = data.get("primary") or {} secondary = data.get("secondary") or {} raw_code = payload.get("code", 1) if raw_code in (0, "0", False): normalized_code = 0 else: normalized_code = int(raw_code or 1) summary = { "http_status": http_status, "code": normalized_code, "message": str(payload.get("message") or ""), "contract_key": str(data.get("contract_key") or ""), "contract_version": str(data.get("contract_version") or ""), "preview_endpoint": str(data.get("preview_endpoint") or ""), "source_label": str(data.get("source_label") or ""), "title": str(data.get("title") or ""), "executor_kind": str(data.get("executor_kind") or ""), "execution_chain": str(data.get("execution_chain") or ""), "primary": { "label": str(primary.get("label") or ""), "action_code": str(primary.get("action_code") or ""), "target_method": str(primary.get("target_method") or ""), "target_api": str(primary.get("target_api") or ""), "recommendation_label": str(primary.get("recommendation_label") or ""), "automation_level_label": str(primary.get("automation_level_label") or ""), "risk_level_label": str(primary.get("risk_level_label") or ""), "node_codes": list(primary.get("node_codes") or []), "request_payload": primary.get("request_payload") or {}, }, } if secondary: summary["secondary"] = { "label": str(secondary.get("label") or ""), "action_code": str(secondary.get("action_code") or ""), "target_method": str(secondary.get("target_method") or ""), "target_api": str(secondary.get("target_api") or ""), "recommendation_label": str(secondary.get("recommendation_label") or ""), "automation_level_label": str(secondary.get("automation_level_label") or ""), "risk_level_label": str(secondary.get("risk_level_label") or ""), "node_codes": list(secondary.get("node_codes") or []), "request_payload": secondary.get("request_payload") or {}, } print(json.dumps(summary, ensure_ascii=False, indent=2)) PY else echo "[1/3] driver action request" print_json_or_raw "${REQUEST_JSON}" echo echo RAW_HTTP_RESPONSE="$(request_with_status "POST" "${BASE_URL}/api/v1/ops/driver-actions/execute" "${REQUEST_JSON}")" HTTP_STATUS="${RAW_HTTP_RESPONSE##*$'\n'HTTP_STATUS=}" RESPONSE_JSON="${RAW_HTTP_RESPONSE%$'\n'HTTP_STATUS=*}" echo "[2/3] raw response" printf '%s\n' "http_status=${HTTP_STATUS}" print_json_or_raw "${RESPONSE_JSON}" echo echo echo "[3/3] condensed summary" python3 - "${RESPONSE_JSON}" "${HTTP_STATUS}" <<'PY' import json import sys payload = json.loads(sys.argv[1]) http_status = int(sys.argv[2] or 0) data = payload.get("data") or {} smart_release_result = data.get("smart_release_result") or {} ui_intent = data.get("ui_intent") or {} runtime_settings = data.get("runtime_settings") or {} raw_code = payload.get("code", 1) if raw_code in (0, "0", False): normalized_code = 0 else: normalized_code = int(raw_code or 1) summary = { "http_status": http_status, "code": normalized_code, "message": str(payload.get("message") or ""), "handled": bool(data.get("handled", False)), "action_code": str(data.get("action_code") or ""), "mode": str(data.get("mode") or ""), } if smart_release_result: release = smart_release_result.get("release") or {} rollout = smart_release_result.get("rollout") or {} smart_plan = smart_release_result.get("smart_rollout") or {} preview = smart_release_result.get("preview") or {} summary["smart_release_result"] = { "release_id": int(release.get("id") or 0), "release_version": str(release.get("release_version") or ""), "release_created": bool(smart_release_result.get("release_created", False)), "release_deduplicated": bool(smart_release_result.get("release_deduplicated", False)), "rollout_created": bool(smart_release_result.get("rollout_created", False)), "requires_confirmation": bool(smart_release_result.get("requires_confirmation", False)), "blocked": bool(smart_release_result.get("blocked", False)), "rollout_id": int(rollout.get("id") or 0), "rollout_code": str(rollout.get("rollout_code") or ""), "smart_mode": str(smart_plan.get("mode") or ""), "smart_available": bool(smart_plan.get("available", False)), "target_node_codes": list((smart_plan.get("target_selector") or {}).get("node_codes") or []), "preview_warnings": list(preview.get("warnings") or []), "preview_approval_required": bool(preview.get("approval_required", False)), } if ui_intent: summary["ui_intent"] = { "kind": str(ui_intent.get("kind") or ""), "release_id": int(ui_intent.get("release_id") or 0), "template_key": str(ui_intent.get("template_key") or ""), "playbook_key": str(ui_intent.get("playbook_key") or ""), "target_node_codes": list(ui_intent.get("target_node_codes") or []), } if runtime_settings: summary["runtime_settings"] = { "worker_log_sync_enabled": bool(runtime_settings.get("worker_log_sync_enabled", False)), "worker_log_sync_mode": str(runtime_settings.get("worker_log_sync_mode") or ""), } print(json.dumps(summary, ensure_ascii=False, indent=2)) PY fi if [[ "${HTTP_STATUS}" -lt 200 || "${HTTP_STATUS}" -ge 300 ]]; then exit 1 fi