feat: add ops center and node onboarding flow
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -12,7 +12,7 @@ TEMPLATE_DIR="$API_DIR/deploy/multi-region/templates"
|
||||
|
||||
if [ "$ROLE" != "controller" ] && [ "$ROLE" != "worker" ]; then
|
||||
echo "invalid role: $ROLE"
|
||||
echo "usage: bash deploy/multi-region/bootstrap_mainland.sh /opt/domaincheck [controller|worker]"
|
||||
echo "usage: bash domain-api/deploy/multi-region/bootstrap_mainland.sh /opt/domaincheck [controller|worker]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
61
domain-api/deploy/multi-region/bootstrap_node_agent.sh
Executable file
61
domain-api/deploy/multi-region/bootstrap_node_agent.sh
Executable file
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="${1:-/opt/domaincheck}"
|
||||
ENV_FILE="/etc/default/domaincheck-node-agent"
|
||||
SERVICE_FILE="/etc/systemd/system/domaincheck-node-agent.service"
|
||||
SERVICE_NAME="domaincheck-node-agent"
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
resolve_project_root() {
|
||||
local candidate
|
||||
for candidate in \
|
||||
"${ROOT_DIR}/domainCheck" \
|
||||
"${ROOT_DIR}/domain-api" \
|
||||
"${ROOT_DIR}" \
|
||||
"$(cd "${SCRIPT_DIR}/../.." && pwd)"; do
|
||||
if [[ -f "${candidate}/deploy/systemd/domain-node-agent.service" && -f "${candidate}/deploy/multi-region/templates/domaincheck-node-agent.env.example" ]]; then
|
||||
printf '%s' "${candidate}"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
PROJECT_ROOT="$(resolve_project_root || true)"
|
||||
if [[ -z "${PROJECT_ROOT}" ]]; then
|
||||
echo "unable to resolve project root under ${ROOT_DIR} or ${SCRIPT_DIR}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TEMPLATE_FILE="${PROJECT_ROOT}/deploy/multi-region/templates/domaincheck-node-agent.env.example"
|
||||
UNIT_TEMPLATE="${PROJECT_ROOT}/deploy/systemd/domain-node-agent.service"
|
||||
|
||||
echo "[1/5] install systemd unit"
|
||||
install -m 0644 "${UNIT_TEMPLATE}" "${SERVICE_FILE}"
|
||||
|
||||
echo "[2/5] install env template if absent"
|
||||
if [[ ! -f "${ENV_FILE}" ]]; then
|
||||
install -m 0644 "${TEMPLATE_FILE}" "${ENV_FILE}"
|
||||
echo "created ${ENV_FILE}"
|
||||
else
|
||||
echo "keep existing ${ENV_FILE}"
|
||||
fi
|
||||
|
||||
echo "[3/5] reload systemd"
|
||||
systemctl daemon-reload
|
||||
|
||||
echo "[4/5] enable service"
|
||||
systemctl enable "${SERVICE_NAME}" >/dev/null
|
||||
|
||||
echo "[5/5] next steps"
|
||||
echo "project_root=${PROJECT_ROOT}"
|
||||
echo "edit ${ENV_FILE} and set:"
|
||||
echo " OPS_CONTROL_PLANE_BASE_URL"
|
||||
echo " OPS_AGENT_TOKEN"
|
||||
echo " NODE_CODE / NODE_REGION / NODE_ROLE"
|
||||
echo
|
||||
echo "then run:"
|
||||
echo " systemctl restart ${SERVICE_NAME}"
|
||||
echo " systemctl status ${SERVICE_NAME} --no-pager -l"
|
||||
91
domain-api/deploy/multi-region/build_node_agent_bootstrap_plan.sh
Executable file
91
domain-api/deploy/multi-region/build_node_agent_bootstrap_plan.sh
Executable file
@@ -0,0 +1,91 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
API_BASE_URL="${1:-http://127.0.0.1:8100}"
|
||||
NODE_CODE="${2:-}"
|
||||
NODE_REGION="${3:-mainland}"
|
||||
NODE_ROLE="${4:-worker}"
|
||||
CONTROL_PLANE_BASE_URL="${5:-${API_BASE_URL}}"
|
||||
ROOT_DIR="${6:-/opt/domaincheck}"
|
||||
ISSUED_BY="${ISSUED_BY:-cli-bootstrap}"
|
||||
EXPIRES_IN_HOURS="${EXPIRES_IN_HOURS:-72}"
|
||||
|
||||
if [[ -z "${NODE_CODE}" ]]; then
|
||||
echo "usage: $0 <api_base_url> <node_code> [node_region] [node_role] [control_plane_base_url] [root_dir]" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
NORMALIZED_API_BASE_URL="${API_BASE_URL%/}"
|
||||
NORMALIZED_CONTROL_PLANE_BASE_URL="${CONTROL_PLANE_BASE_URL%/}"
|
||||
if [[ "${NORMALIZED_API_BASE_URL}" != */api/v1 ]]; then
|
||||
API_ENDPOINT="${NORMALIZED_API_BASE_URL}/api/v1/ops/agent/bootstrap-plan"
|
||||
else
|
||||
API_ENDPOINT="${NORMALIZED_API_BASE_URL}/ops/agent/bootstrap-plan"
|
||||
fi
|
||||
|
||||
TMP_RESPONSE_FILE="$(mktemp)"
|
||||
cleanup() {
|
||||
rm -f "${TMP_RESPONSE_FILE}"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
python3 - "${NODE_CODE}" "${NODE_REGION}" "${NODE_ROLE}" "${ISSUED_BY}" "${EXPIRES_IN_HOURS}" "${NORMALIZED_CONTROL_PLANE_BASE_URL}" "${ROOT_DIR}" >"${TMP_RESPONSE_FILE}.payload" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
|
||||
payload = {
|
||||
"node_code": sys.argv[1],
|
||||
"node_region": sys.argv[2],
|
||||
"node_role": sys.argv[3],
|
||||
"issued_by": sys.argv[4],
|
||||
"expires_in_hours": int(sys.argv[5]),
|
||||
"control_plane_base_url": sys.argv[6],
|
||||
"root_dir": sys.argv[7],
|
||||
"metadata": {
|
||||
"issued_from": "build_node_agent_bootstrap_plan.sh",
|
||||
},
|
||||
}
|
||||
print(json.dumps(payload, ensure_ascii=False))
|
||||
PY
|
||||
|
||||
curl -fsS -X POST \
|
||||
-H 'Content-Type: application/json' \
|
||||
--data-binary @"${TMP_RESPONSE_FILE}.payload" \
|
||||
"${API_ENDPOINT}" >"${TMP_RESPONSE_FILE}"
|
||||
|
||||
python3 - "${TMP_RESPONSE_FILE}" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
response = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
|
||||
response_code = response.get("code", 0)
|
||||
if int(response_code if response_code is not None else 0) != 0:
|
||||
raise SystemExit(response.get("message") or "bootstrap plan request failed")
|
||||
|
||||
data = response.get("data") or {}
|
||||
plan = data.get("bootstrap_plan") or {}
|
||||
|
||||
print("[token]")
|
||||
print(f"node_code={data.get('node_code', '')}")
|
||||
print(f"token_preview={data.get('token_preview', '')}")
|
||||
print(f"expires_at={data.get('expires_at', '')}")
|
||||
print()
|
||||
print("[env]")
|
||||
print(plan.get("env_content") or "")
|
||||
print()
|
||||
print("[commands]")
|
||||
print(plan.get("command_block") or "")
|
||||
print()
|
||||
print("[bootstrap-script]")
|
||||
print(plan.get("bootstrap_script_content") or "")
|
||||
print()
|
||||
print("[bootstrap-run-block]")
|
||||
print(plan.get("bootstrap_run_script_block") or "")
|
||||
print()
|
||||
print("[health-checks]")
|
||||
for item in plan.get("health_checks") or []:
|
||||
print(item)
|
||||
PY
|
||||
|
||||
rm -f "${TMP_RESPONSE_FILE}.payload"
|
||||
345
domain-api/deploy/multi-region/check_env_drift.sh
Normal file
345
domain-api/deploy/multi-region/check_env_drift.sh
Normal file
@@ -0,0 +1,345 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
BASE_URL="${1:-http://127.0.0.1:8100}"
|
||||
API_SERVICE="${API_SERVICE:-domaincheck-api}"
|
||||
WORKER_SERVICE="${WORKER_SERVICE:-domaincheck-worker}"
|
||||
SYNC_AGENT_SERVICE="${SYNC_AGENT_SERVICE:-domaincheck-sync-agent}"
|
||||
NODE_AGENT_SERVICE="${NODE_AGENT_SERVICE:-domaincheck-node-agent}"
|
||||
API_ENV_FILE="${API_ENV_FILE:-/etc/default/domaincheck-api}"
|
||||
WORKER_ENV_FILE="${WORKER_ENV_FILE:-/etc/default/domaincheck-worker}"
|
||||
NODE_AGENT_ENV_FILE="${NODE_AGENT_ENV_FILE:-/etc/default/domaincheck-node-agent}"
|
||||
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
|
||||
API_DIR="${PROJECT_ROOT}/domain-api"
|
||||
WEB_DIR="${PROJECT_ROOT}/domain-web"
|
||||
PYTHON_BIN="${PYTHON_BIN:-python3}"
|
||||
|
||||
FETCH_BODY=""
|
||||
FETCH_STATUS="000"
|
||||
|
||||
fetch_json() {
|
||||
local url="${1:-}"
|
||||
local raw_text
|
||||
raw_text="$(curl -sS \
|
||||
--connect-timeout "${CURL_CONNECT_TIMEOUT:-3}" \
|
||||
--max-time "${CURL_MAX_TIME:-10}" \
|
||||
"${url}" \
|
||||
-w $'\nHTTP_STATUS=%{http_code}' || true)"
|
||||
FETCH_BODY="${raw_text%$'\n'HTTP_STATUS=*}"
|
||||
FETCH_STATUS="${raw_text##*$'\n'HTTP_STATUS=}"
|
||||
}
|
||||
|
||||
echo "[1/8] local command availability"
|
||||
for cmd in bash curl systemctl "${PYTHON_BIN}" node npm; do
|
||||
if command -v "${cmd}" >/dev/null 2>&1; then
|
||||
printf '%s: ok -> %s\n' "${cmd}" "$(command -v "${cmd}")"
|
||||
else
|
||||
printf '%s: missing\n' "${cmd}"
|
||||
fi
|
||||
done
|
||||
echo
|
||||
|
||||
echo "[2/8] python environment"
|
||||
"${PYTHON_BIN}" - <<'PY' "${API_DIR}" "${WEB_DIR}"
|
||||
import importlib.util
|
||||
import json
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
api_dir = pathlib.Path(sys.argv[1])
|
||||
web_dir = pathlib.Path(sys.argv[2])
|
||||
|
||||
def has_module(name: str) -> bool:
|
||||
return importlib.util.find_spec(name) is not None
|
||||
|
||||
payload = {
|
||||
"python_executable": sys.executable,
|
||||
"python_version": sys.version.split()[0],
|
||||
"pytest_installed": has_module("pytest"),
|
||||
"uvicorn_installed": has_module("uvicorn"),
|
||||
"fastapi_installed": has_module("fastapi"),
|
||||
"api_requirements_exists": (api_dir / "requirements.txt").exists(),
|
||||
"web_package_json_exists": (web_dir / "package.json").exists(),
|
||||
}
|
||||
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
||||
PY
|
||||
echo
|
||||
|
||||
echo "[3/8] env files"
|
||||
for env_file in "${API_ENV_FILE}" "${WORKER_ENV_FILE}" "${NODE_AGENT_ENV_FILE}"; do
|
||||
if [[ -f "${env_file}" ]]; then
|
||||
echo "exists: ${env_file}"
|
||||
else
|
||||
echo "missing: ${env_file}"
|
||||
fi
|
||||
done
|
||||
echo
|
||||
|
||||
echo "[4/8] systemd service states"
|
||||
for service in "${API_SERVICE}" "${WORKER_SERVICE}" "${SYNC_AGENT_SERVICE}" "${NODE_AGENT_SERVICE}"; do
|
||||
if systemctl list-unit-files "${service}.service" --no-pager >/dev/null 2>&1; then
|
||||
printf '%s: enabled=%s active=%s\n' \
|
||||
"${service}" \
|
||||
"$(systemctl is-enabled "${service}" 2>/dev/null || true)" \
|
||||
"$(systemctl is-active "${service}" 2>/dev/null || true)"
|
||||
else
|
||||
printf '%s: not-installed\n' "${service}"
|
||||
fi
|
||||
done
|
||||
echo
|
||||
|
||||
echo "[5/8] runtime preflight"
|
||||
fetch_json "${BASE_URL}/api/v1/runtime/preflight"
|
||||
printf 'status=%s\n%s\n\n' "${FETCH_STATUS}" "${FETCH_BODY}"
|
||||
|
||||
echo "[6/8] runtime readiness"
|
||||
fetch_json "${BASE_URL}/api/v1/runtime/readiness"
|
||||
printf 'status=%s\n%s\n\n' "${FETCH_STATUS}" "${FETCH_BODY}"
|
||||
|
||||
echo "[7/8] runtime build-info"
|
||||
fetch_json "${BASE_URL}/api/v1/runtime/build-info"
|
||||
printf 'status=%s\n%s\n\n' "${FETCH_STATUS}" "${FETCH_BODY}"
|
||||
|
||||
echo "[8/8] condensed env audit summary"
|
||||
"${PYTHON_BIN}" - <<'PY' \
|
||||
"${BASE_URL}" \
|
||||
"${API_SERVICE}" \
|
||||
"${WORKER_SERVICE}" \
|
||||
"${SYNC_AGENT_SERVICE}" \
|
||||
"${NODE_AGENT_SERVICE}" \
|
||||
"${API_ENV_FILE}" \
|
||||
"${WORKER_ENV_FILE}" \
|
||||
"${NODE_AGENT_ENV_FILE}" \
|
||||
"${PROJECT_ROOT}"
|
||||
import importlib.util
|
||||
import json
|
||||
import pathlib
|
||||
import subprocess
|
||||
import sys
|
||||
import urllib.request
|
||||
from typing import Dict, List
|
||||
|
||||
base_url, api_service, worker_service, sync_agent_service, node_agent_service, api_env, worker_env, node_agent_env, project_root = sys.argv[1:10]
|
||||
project_root_path = pathlib.Path(project_root)
|
||||
|
||||
def has_module(name: str) -> bool:
|
||||
return importlib.util.find_spec(name) is not None
|
||||
|
||||
def inspect_python_runtime(path: str) -> Dict[str, object]:
|
||||
runtime_path = pathlib.Path(path)
|
||||
if not runtime_path.exists():
|
||||
return {}
|
||||
probe = r"""
|
||||
import importlib.util
|
||||
import json
|
||||
import sys
|
||||
|
||||
mods = ["pytest", "fastapi", "uvicorn"]
|
||||
print(json.dumps({
|
||||
"python_executable": sys.executable,
|
||||
"python_version": sys.version.split()[0],
|
||||
"modules": {name: importlib.util.find_spec(name) is not None for name in mods},
|
||||
}, ensure_ascii=False))
|
||||
"""
|
||||
try:
|
||||
raw = subprocess.check_output([str(runtime_path), "-c", probe], text=True, stderr=subprocess.DEVNULL)
|
||||
except Exception:
|
||||
return {}
|
||||
try:
|
||||
payload = json.loads(raw)
|
||||
except Exception:
|
||||
return {}
|
||||
payload["path"] = str(runtime_path)
|
||||
return payload
|
||||
|
||||
def systemd_state(service: str) -> dict:
|
||||
try:
|
||||
enabled = subprocess.check_output(
|
||||
["systemctl", "is-enabled", service],
|
||||
text=True,
|
||||
stderr=subprocess.DEVNULL,
|
||||
).strip()
|
||||
except Exception:
|
||||
enabled = ""
|
||||
try:
|
||||
active = subprocess.check_output(
|
||||
["systemctl", "is-active", service],
|
||||
text=True,
|
||||
stderr=subprocess.DEVNULL,
|
||||
).strip()
|
||||
except Exception:
|
||||
active = ""
|
||||
return {"enabled": enabled, "active": active}
|
||||
|
||||
def fetch_json(url: str) -> tuple[str, dict]:
|
||||
try:
|
||||
with urllib.request.urlopen(url, timeout=10) as resp:
|
||||
status = str(resp.status)
|
||||
raw = resp.read().decode("utf-8", errors="replace")
|
||||
except Exception:
|
||||
return "000", {}
|
||||
try:
|
||||
payload = json.loads(raw)
|
||||
except Exception:
|
||||
return status, {}
|
||||
return status, payload.get("data") if isinstance(payload, dict) else {}
|
||||
|
||||
def read_git_value(*args: str) -> str:
|
||||
try:
|
||||
result = subprocess.check_output(
|
||||
["git", "-C", str(project_root_path), *args],
|
||||
text=True,
|
||||
stderr=subprocess.DEVNULL,
|
||||
).strip()
|
||||
except Exception:
|
||||
return ""
|
||||
return result
|
||||
|
||||
def read_text(path: pathlib.Path) -> str:
|
||||
try:
|
||||
return path.read_text(encoding="utf-8", errors="replace")
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
local_commit_sha = read_git_value("rev-parse", "--short=12", "HEAD")
|
||||
local_commit_ref = read_git_value("rev-parse", "--abbrev-ref", "HEAD")
|
||||
ops_agent_service_text = read_text(project_root_path / "domain-api" / "app" / "services" / "ops_agent_service.py")
|
||||
bootstrap_node_agent_text = read_text(project_root_path / "domain-api" / "deploy" / "multi-region" / "bootstrap_node_agent.sh")
|
||||
|
||||
repo_supports_install_command_block = (
|
||||
"install_command_block" in ops_agent_service_text
|
||||
and "_candidate_node_agent_install_paths" in ops_agent_service_text
|
||||
)
|
||||
repo_supports_multi_layout_bootstrap = "resolve_project_root" in bootstrap_node_agent_text
|
||||
|
||||
preflight_status, preflight = fetch_json(f"{base_url}/api/v1/runtime/preflight")
|
||||
readiness_status, readiness = fetch_json(f"{base_url}/api/v1/runtime/readiness")
|
||||
build_status, build = fetch_json(f"{base_url}/api/v1/runtime/build-info")
|
||||
route_surface = build.get("route_surface") if isinstance(build, dict) else {}
|
||||
|
||||
missing = []
|
||||
tooling_items: List[str] = []
|
||||
for env_file in [api_env, worker_env, node_agent_env]:
|
||||
if not pathlib.Path(env_file).exists():
|
||||
missing.append(f"missing_env:{env_file}")
|
||||
|
||||
python_candidates: List[str] = []
|
||||
seen_candidates = set()
|
||||
for candidate in [
|
||||
"/opt/domaincheck/domainCheck/.venv/bin/python",
|
||||
str(pathlib.Path.cwd() / ".venv" / "bin" / "python"),
|
||||
str(pathlib.Path.cwd() / "domain-api" / ".venv" / "bin" / "python"),
|
||||
pathlib.Path(sys.executable).as_posix(),
|
||||
]:
|
||||
if candidate and candidate not in seen_candidates:
|
||||
seen_candidates.add(candidate)
|
||||
python_candidates.append(candidate)
|
||||
|
||||
python_runtimes = [item for item in (inspect_python_runtime(candidate) for candidate in python_candidates) if item]
|
||||
|
||||
def runtime_has_module(module_name: str) -> bool:
|
||||
for runtime in python_runtimes:
|
||||
modules = runtime.get("modules") or {}
|
||||
if bool(modules.get(module_name)):
|
||||
return True
|
||||
return False
|
||||
|
||||
if not runtime_has_module("fastapi"):
|
||||
missing.append("python_module:fastapi")
|
||||
if not runtime_has_module("uvicorn"):
|
||||
missing.append("python_module:uvicorn")
|
||||
if not runtime_has_module("pytest"):
|
||||
tooling_items.append("python_module:pytest")
|
||||
|
||||
runtime_commit_sha = str(build.get("commit_sha") or "").strip() if isinstance(build, dict) else ""
|
||||
runtime_commit_ref = str(build.get("commit_ref") or "").strip() if isinstance(build, dict) else ""
|
||||
runtime_route_missing_keys = [str(item).strip() for item in list((route_surface or {}).get("missing_keys") or []) if str(item).strip()]
|
||||
runtime_expected_paths = {
|
||||
str(key).strip(): str(value).strip()
|
||||
for key, value in dict((route_surface or {}).get("expected_paths") or {}).items()
|
||||
if str(key).strip()
|
||||
}
|
||||
runtime_bootstrap_plan_route_missing = "ops_node_handover_bootstrap_plan" in runtime_route_missing_keys
|
||||
runtime_declares_bootstrap_plan_route = "ops_node_handover_bootstrap_plan" in runtime_expected_paths
|
||||
runtime_repo_commit_drift = bool(local_commit_sha and runtime_commit_sha and local_commit_sha != runtime_commit_sha)
|
||||
runtime_repo_capability_drift = bool(
|
||||
repo_supports_install_command_block
|
||||
and (runtime_bootstrap_plan_route_missing or not runtime_declares_bootstrap_plan_route)
|
||||
)
|
||||
|
||||
recommended_actions: List[str] = []
|
||||
if runtime_repo_capability_drift:
|
||||
recommended_actions.append("systemctl restart domaincheck-api")
|
||||
recommended_actions.append("bash domain-api/deploy/multi-region/drive_ops_center.sh stack-diagnosis http://127.0.0.1:8100 summary")
|
||||
recommended_actions.append("bash domain-api/deploy/multi-region/drive_ops_center.sh node-bootstrap-plan http://127.0.0.1:8100 mainland-worker-01")
|
||||
elif runtime_repo_commit_drift:
|
||||
recommended_actions.append("systemctl restart domaincheck-api")
|
||||
recommended_actions.append("bash domain-api/deploy/multi-region/drive_ops_center.sh doctor http://127.0.0.1:8100")
|
||||
|
||||
payload = {
|
||||
"status": "ready",
|
||||
"headline": "当前环境没有发现新的基础阻断,可继续做上线复核。",
|
||||
"repo": {
|
||||
"project_root": str(project_root_path),
|
||||
"commit_sha": local_commit_sha,
|
||||
"commit_ref": local_commit_ref,
|
||||
"supports_install_command_block": repo_supports_install_command_block,
|
||||
"supports_multi_layout_bootstrap": repo_supports_multi_layout_bootstrap,
|
||||
},
|
||||
"python": {
|
||||
"pytest_installed": runtime_has_module("pytest"),
|
||||
"fastapi_installed": runtime_has_module("fastapi"),
|
||||
"uvicorn_installed": runtime_has_module("uvicorn"),
|
||||
"runtimes": python_runtimes,
|
||||
},
|
||||
"services": {
|
||||
api_service: systemd_state(api_service),
|
||||
worker_service: systemd_state(worker_service),
|
||||
sync_agent_service: systemd_state(sync_agent_service),
|
||||
node_agent_service: systemd_state(node_agent_service),
|
||||
},
|
||||
"runtime": {
|
||||
"preflight_status_code": preflight_status,
|
||||
"preflight_ok": bool((preflight or {}).get("ok", False)),
|
||||
"readiness_status_code": readiness_status,
|
||||
"readiness_status": str((readiness or {}).get("status") or ""),
|
||||
"readiness_summary": str((readiness or {}).get("summary") or ""),
|
||||
"build_status_code": build_status,
|
||||
"build_commit_sha": runtime_commit_sha,
|
||||
"build_commit_ref": runtime_commit_ref,
|
||||
"route_surface_complete": bool((route_surface or {}).get("surface_complete", False)),
|
||||
"route_surface_missing_keys": runtime_route_missing_keys,
|
||||
"route_surface_declares_bootstrap_plan": runtime_declares_bootstrap_plan_route,
|
||||
"repo_commit_drift": runtime_repo_commit_drift,
|
||||
"repo_capability_drift": runtime_repo_capability_drift,
|
||||
"runtime_may_need_restart": runtime_repo_commit_drift or runtime_repo_capability_drift,
|
||||
},
|
||||
"missing_items": missing,
|
||||
"tooling_items": tooling_items,
|
||||
"recommended_actions": recommended_actions,
|
||||
}
|
||||
|
||||
if not bool((preflight or {}).get("ok", False)):
|
||||
payload["status"] = "blocked"
|
||||
payload["headline"] = "runtime/preflight 未通过,先修环境与依赖,再继续上线复核。"
|
||||
elif str((readiness or {}).get("status") or "") == "blocking":
|
||||
payload["status"] = "blocked"
|
||||
payload["headline"] = "runtime/readiness 当前为 blocking,现场仍不适合进入正式发布。"
|
||||
elif runtime_repo_capability_drift:
|
||||
payload["status"] = "attention"
|
||||
payload["headline"] = "本地仓库已经具备新的 Node Agent 接管能力,但运行中的 API 路由面仍旧,当前更像是服务未重启到最新代码。"
|
||||
elif runtime_repo_commit_drift:
|
||||
payload["status"] = "attention"
|
||||
payload["headline"] = "本地仓库 commit 与运行中 API build-info 不一致,建议先重启或重发当前版本,再继续上线复核。"
|
||||
elif not bool((route_surface or {}).get("surface_complete", False)):
|
||||
payload["status"] = "attention"
|
||||
payload["headline"] = "运行中 API 路由面还不完整,建议先确认当前实例是否已经吃到最新版本。"
|
||||
elif missing:
|
||||
payload["status"] = "attention"
|
||||
payload["headline"] = "基础环境存在缺口,虽然未必阻断运行,但建议先补齐后再交付。"
|
||||
elif tooling_items:
|
||||
payload["status"] = "ready"
|
||||
payload["headline"] = "运行环境可继续上线复核,但本机工具链仍有缺口,建议后续补齐。"
|
||||
|
||||
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
||||
PY
|
||||
423
domain-api/deploy/multi-region/check_go_live.sh
Normal file
423
domain-api/deploy/multi-region/check_go_live.sh
Normal file
@@ -0,0 +1,423 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
BASE_URL="${1:-http://127.0.0.1:8100}"
|
||||
OVERSEAS_BASE_URL="${2:-}"
|
||||
VIEW_MODE="${3:-full}"
|
||||
CURL_CONNECT_TIMEOUT="${CURL_CONNECT_TIMEOUT:-3}"
|
||||
CURL_MAX_TIME="${CURL_MAX_TIME:-20}"
|
||||
LOCAL_NODE_AGENT_SERVICE_NAME="${LOCAL_NODE_AGENT_SERVICE_NAME:-domaincheck-node-agent}"
|
||||
LOCAL_NODE_AGENT_ENV_FILE="${LOCAL_NODE_AGENT_ENV_FILE:-/etc/default/domaincheck-node-agent}"
|
||||
|
||||
FETCH_BODY=""
|
||||
FETCH_STATUS="000"
|
||||
|
||||
fetch_json() {
|
||||
local url="${1:-}"
|
||||
local raw_text
|
||||
raw_text="$(curl -sS \
|
||||
--connect-timeout "${CURL_CONNECT_TIMEOUT}" \
|
||||
--max-time "${CURL_MAX_TIME}" \
|
||||
"${url}" \
|
||||
-w $'\nHTTP_STATUS=%{http_code}' || true)"
|
||||
FETCH_BODY="${raw_text%$'\n'HTTP_STATUS=*}"
|
||||
FETCH_STATUS="${raw_text##*$'\n'HTTP_STATUS=}"
|
||||
}
|
||||
|
||||
run_local_node_agent_probe() {
|
||||
SERVICE_NAME="${LOCAL_NODE_AGENT_SERVICE_NAME}" \
|
||||
ENV_FILE="${LOCAL_NODE_AGENT_ENV_FILE}" \
|
||||
bash "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/check_node_agent.sh"
|
||||
}
|
||||
|
||||
fetch_json "${BASE_URL}/api/v1/ops/stack-diagnosis?base_url=${BASE_URL}"
|
||||
STACK_JSON="${FETCH_BODY}"
|
||||
STACK_STATUS="${FETCH_STATUS}"
|
||||
|
||||
fetch_json "${BASE_URL}/api/v1/ops/go-live-summary?base_url=${BASE_URL}"
|
||||
GO_LIVE_API_JSON="${FETCH_BODY}"
|
||||
GO_LIVE_API_STATUS="${FETCH_STATUS}"
|
||||
|
||||
if [[ "${GO_LIVE_API_STATUS}" == 2* ]]; then
|
||||
if [[ "${VIEW_MODE}" != "summary" ]]; then
|
||||
echo "[1/1] go-live summary (api)"
|
||||
fi
|
||||
printf '%s\n' "${GO_LIVE_API_JSON}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
fetch_json "${BASE_URL}/api/v1/ops/contracts"
|
||||
CONTRACTS_JSON="${FETCH_BODY}"
|
||||
CONTRACTS_STATUS="${FETCH_STATUS}"
|
||||
|
||||
fetch_json "${BASE_URL}/api/v1/ops/releases/launchpad"
|
||||
LAUNCHPAD_JSON="${FETCH_BODY}"
|
||||
LAUNCHPAD_STATUS="${FETCH_STATUS}"
|
||||
|
||||
fetch_json "${BASE_URL}/api/v1/runtime/build-info"
|
||||
BUILD_INFO_JSON="${FETCH_BODY}"
|
||||
BUILD_INFO_STATUS="${FETCH_STATUS}"
|
||||
|
||||
fetch_json "${BASE_URL}/api/v1/ops/nodes"
|
||||
NODES_JSON="${FETCH_BODY}"
|
||||
NODES_STATUS="${FETCH_STATUS}"
|
||||
|
||||
fetch_json "${BASE_URL}/api/v1/ops/overview"
|
||||
OVERVIEW_JSON="${FETCH_BODY}"
|
||||
OVERVIEW_STATUS="${FETCH_STATUS}"
|
||||
|
||||
LOCAL_NODE_AGENT_RAW="$(run_local_node_agent_probe 2>&1 || true)"
|
||||
|
||||
if [[ "${VIEW_MODE}" != "summary" ]]; then
|
||||
echo "[1/7] ops stack diagnosis"
|
||||
printf 'status=%s\n%s\n\n' "${STACK_STATUS}" "${STACK_JSON}"
|
||||
|
||||
echo "[2/7] ops contracts"
|
||||
printf 'status=%s\n%s\n\n' "${CONTRACTS_STATUS}" "${CONTRACTS_JSON}"
|
||||
|
||||
echo "[3/7] release launchpad"
|
||||
printf 'status=%s\n%s\n\n' "${LAUNCHPAD_STATUS}" "${LAUNCHPAD_JSON}"
|
||||
|
||||
echo "[4/7] runtime build info"
|
||||
printf 'status=%s\n%s\n\n' "${BUILD_INFO_STATUS}" "${BUILD_INFO_JSON}"
|
||||
|
||||
echo "[5/7] managed nodes"
|
||||
printf 'status=%s\n%s\n\n' "${NODES_STATUS}" "${NODES_JSON}"
|
||||
|
||||
echo "[6/7] ops overview"
|
||||
printf 'status=%s\n%s\n\n' "${OVERVIEW_STATUS}" "${OVERVIEW_JSON}"
|
||||
|
||||
echo "[7/7] local node agent probe"
|
||||
printf '%s\n\n' "${LOCAL_NODE_AGENT_RAW}"
|
||||
fi
|
||||
|
||||
echo "[8/8] go-live summary"
|
||||
python3 - <<'PY' \
|
||||
"${BASE_URL}" \
|
||||
"${OVERSEAS_BASE_URL}" \
|
||||
"${STACK_JSON}" \
|
||||
"${STACK_STATUS}" \
|
||||
"${CONTRACTS_JSON}" \
|
||||
"${CONTRACTS_STATUS}" \
|
||||
"${LAUNCHPAD_JSON}" \
|
||||
"${LAUNCHPAD_STATUS}" \
|
||||
"${BUILD_INFO_JSON}" \
|
||||
"${BUILD_INFO_STATUS}" \
|
||||
"${NODES_JSON}" \
|
||||
"${NODES_STATUS}" \
|
||||
"${OVERVIEW_JSON}" \
|
||||
"${OVERVIEW_STATUS}" \
|
||||
"${LOCAL_NODE_AGENT_RAW}" \
|
||||
"${LOCAL_NODE_AGENT_SERVICE_NAME}"
|
||||
import json
|
||||
import sys
|
||||
|
||||
|
||||
def load_payload(raw_text: str) -> dict:
|
||||
try:
|
||||
payload = json.loads(str(raw_text or ""))
|
||||
except Exception:
|
||||
return {}
|
||||
return payload if isinstance(payload, dict) else {}
|
||||
|
||||
|
||||
def load_data(raw_text: str) -> dict:
|
||||
payload = load_payload(raw_text)
|
||||
data = payload.get("data")
|
||||
return data if isinstance(data, dict) else {}
|
||||
|
||||
|
||||
def ensure_dict(value):
|
||||
return value if isinstance(value, dict) else {}
|
||||
|
||||
|
||||
def ensure_list(value):
|
||||
return value if isinstance(value, list) else []
|
||||
|
||||
|
||||
def ensure_str(value):
|
||||
return value if isinstance(value, str) else ""
|
||||
|
||||
|
||||
def truthy_http(status_code: str) -> bool:
|
||||
return str(status_code or "").startswith("2")
|
||||
|
||||
|
||||
base_url = str(sys.argv[1] or "http://127.0.0.1:8100").strip()
|
||||
overseas_base_url = str(sys.argv[2] or "").strip()
|
||||
stack_payload = load_data(sys.argv[3])
|
||||
stack_status_code = str(sys.argv[4] or "000")
|
||||
contracts_payload = load_data(sys.argv[5])
|
||||
contracts_status_code = str(sys.argv[6] or "000")
|
||||
launchpad_payload = load_data(sys.argv[7])
|
||||
launchpad_status_code = str(sys.argv[8] or "000")
|
||||
build_payload = load_data(sys.argv[9])
|
||||
build_status_code = str(sys.argv[10] or "000")
|
||||
nodes_payload = load_data(sys.argv[11])
|
||||
nodes_status_code = str(sys.argv[12] or "000")
|
||||
overview_payload = load_data(sys.argv[13])
|
||||
overview_status_code = str(sys.argv[14] or "000")
|
||||
local_node_agent_raw = str(sys.argv[15] or "")
|
||||
local_node_agent_service_name = str(sys.argv[16] or "domaincheck-node-agent").strip()
|
||||
|
||||
diagnosis = ensure_dict(stack_payload.get("diagnosis"))
|
||||
release_hub = ensure_dict(stack_payload.get("release_hub"))
|
||||
managed_nodes = ensure_dict(stack_payload.get("managed_nodes"))
|
||||
runtime_build_info = ensure_dict(stack_payload.get("runtime_build_info")) or build_payload
|
||||
launchpad_status = ensure_dict(launchpad_payload.get("launchpad_status"))
|
||||
launchpad_recommended_target_node_code = ensure_str(launchpad_status.get("recommended_target_node_code"))
|
||||
launchpad_recommended_recovery_label = ensure_str(launchpad_status.get("recommended_recovery_label"))
|
||||
launchpad_recommended_recovery_summary = ensure_str(launchpad_status.get("recommended_recovery_summary"))
|
||||
launchpad_onboarding_bootstrap_pending_nodes = int(
|
||||
launchpad_status.get("onboarding_bootstrap_pending_nodes", 0) or 0
|
||||
)
|
||||
launchpad_onboarding_acceptance_ready_nodes = int(
|
||||
launchpad_status.get("onboarding_acceptance_ready_nodes", 0) or 0
|
||||
)
|
||||
contracts = ensure_list(contracts_payload.get("contracts"))
|
||||
nodes_summary = ensure_dict(nodes_payload.get("summary")) or ensure_dict(managed_nodes.get("summary"))
|
||||
route_surface = ensure_dict(runtime_build_info.get("route_surface"))
|
||||
next_step = ensure_dict(diagnosis.get("next_step"))
|
||||
operator_decision = ensure_dict(diagnosis.get("operator_decision"))
|
||||
overview_recommendation = ensure_dict(overview_payload.get("recommendation"))
|
||||
execution_scene = ensure_dict(overview_payload.get("execution_scene"))
|
||||
log_sync = ensure_dict(execution_scene.get("log_sync"))
|
||||
|
||||
local_agent_enabled = ""
|
||||
local_agent_active = ""
|
||||
local_agent_installed = False
|
||||
for line in [item.strip() for item in local_node_agent_raw.splitlines() if item.strip()]:
|
||||
if line == "not-installed":
|
||||
local_agent_installed = False
|
||||
if line == "enabled":
|
||||
local_agent_enabled = "enabled"
|
||||
local_agent_installed = True
|
||||
elif line == "disabled":
|
||||
local_agent_enabled = "disabled"
|
||||
local_agent_installed = True
|
||||
elif line in {"active", "inactive", "failed"}:
|
||||
local_agent_active = line
|
||||
local_agent_installed = True
|
||||
|
||||
blocking_reasons = []
|
||||
warnings = []
|
||||
publish_blocking_reasons = []
|
||||
publish_warnings = []
|
||||
|
||||
stack_status = ensure_str(diagnosis.get("stack_status")) or "unknown"
|
||||
if stack_status == "blocked":
|
||||
blocking_reasons.append("stack_diagnosis=blocked")
|
||||
publish_blocking_reasons.append("stack_diagnosis=blocked")
|
||||
elif stack_status == "attention":
|
||||
warnings.append("stack_diagnosis=attention")
|
||||
publish_warnings.append("stack_diagnosis=attention")
|
||||
elif not truthy_http(stack_status_code):
|
||||
blocking_reasons.append("stack_diagnosis_unavailable")
|
||||
publish_blocking_reasons.append("stack_diagnosis_unavailable")
|
||||
|
||||
surface_complete = bool(route_surface.get("surface_complete", False))
|
||||
if not surface_complete:
|
||||
missing_keys = [ensure_str(item) for item in ensure_list(route_surface.get("missing_keys")) if ensure_str(item)]
|
||||
blocking_reasons.append(
|
||||
"route_surface_incomplete" + (f":{','.join(missing_keys)}" if missing_keys else "")
|
||||
)
|
||||
publish_blocking_reasons.append(
|
||||
"route_surface_incomplete" + (f":{','.join(missing_keys)}" if missing_keys else "")
|
||||
)
|
||||
|
||||
if not truthy_http(contracts_status_code) or len(contracts) <= 0:
|
||||
blocking_reasons.append("contracts_unavailable")
|
||||
publish_blocking_reasons.append("contracts_unavailable")
|
||||
|
||||
launchpad_status_code_value = ensure_str(launchpad_status.get("status")) or ensure_str(release_hub.get("launchpad_status")) or ""
|
||||
if launchpad_status_code_value == "blocked":
|
||||
blocking_reasons.append("release_launchpad=blocked")
|
||||
publish_blocking_reasons.append("release_launchpad=blocked")
|
||||
elif launchpad_status_code_value == "attention":
|
||||
warnings.append("release_launchpad=attention")
|
||||
publish_warnings.append("release_launchpad=attention")
|
||||
elif not truthy_http(launchpad_status_code):
|
||||
blocking_reasons.append("release_launchpad_unavailable")
|
||||
publish_blocking_reasons.append("release_launchpad_unavailable")
|
||||
|
||||
if launchpad_onboarding_bootstrap_pending_nodes > 0:
|
||||
warnings.append(f"launchpad_onboarding_bootstrap_pending={launchpad_onboarding_bootstrap_pending_nodes}")
|
||||
publish_warnings.append(f"launchpad_onboarding_bootstrap_pending={launchpad_onboarding_bootstrap_pending_nodes}")
|
||||
if launchpad_onboarding_acceptance_ready_nodes > 0:
|
||||
warnings.append(f"launchpad_onboarding_acceptance_ready={launchpad_onboarding_acceptance_ready_nodes}")
|
||||
publish_warnings.append(f"launchpad_onboarding_acceptance_ready={launchpad_onboarding_acceptance_ready_nodes}")
|
||||
|
||||
managed_enabled = int(nodes_summary.get("managed_enabled", 0) or 0)
|
||||
remote_access_ready = int(nodes_summary.get("remote_access_ready", 0) or 0)
|
||||
queue_dead_letter_nodes = int(nodes_summary.get("queue_dead_letter_nodes", 0) or 0)
|
||||
if managed_enabled > 0 and remote_access_ready == 0:
|
||||
blocking_reasons.append("remote_agent_ready=0")
|
||||
publish_blocking_reasons.append("remote_agent_ready=0")
|
||||
elif managed_enabled > 0 and remote_access_ready < managed_enabled:
|
||||
warnings.append(f"remote_agent_ready={remote_access_ready}/{managed_enabled}")
|
||||
publish_warnings.append(f"remote_agent_ready={remote_access_ready}/{managed_enabled}")
|
||||
|
||||
if queue_dead_letter_nodes > 0:
|
||||
warnings.append(f"queue_dead_letter_nodes={queue_dead_letter_nodes}")
|
||||
publish_warnings.append(f"queue_dead_letter_nodes={queue_dead_letter_nodes}")
|
||||
|
||||
if local_agent_installed and local_agent_active not in {"", "active"}:
|
||||
warnings.append(f"{local_node_agent_service_name}={local_agent_active}")
|
||||
|
||||
log_sync_enabled = bool(log_sync.get("enabled"))
|
||||
log_sync_state = ensure_str(log_sync.get("state"))
|
||||
log_sync_mode = ensure_str(log_sync.get("mode"))
|
||||
participating_nodes_total = int(log_sync.get("participating_node_count", 0) or 0)
|
||||
covered_participating_nodes = int(log_sync.get("covered_participating_node_count", 0) or 0)
|
||||
missing_participating_node_codes = [
|
||||
ensure_str(item)
|
||||
for item in ensure_list(log_sync.get("missing_participating_nodes"))
|
||||
if ensure_str(item)
|
||||
]
|
||||
if log_sync_enabled and participating_nodes_total > 0 and covered_participating_nodes == 0:
|
||||
warnings.append(f"log_sync_waiting_sample=0/{participating_nodes_total}")
|
||||
publish_warnings.append(f"log_sync_waiting_sample=0/{participating_nodes_total}")
|
||||
elif log_sync_enabled and participating_nodes_total > 0 and covered_participating_nodes < participating_nodes_total:
|
||||
warnings.append(f"log_sync_partial={covered_participating_nodes}/{participating_nodes_total}")
|
||||
publish_warnings.append(f"log_sync_partial={covered_participating_nodes}/{participating_nodes_total}")
|
||||
|
||||
operator_lane = ensure_str(operator_decision.get("lane"))
|
||||
operator_title = ensure_str(operator_decision.get("title"))
|
||||
operator_primary_command_key = ensure_str(operator_decision.get("primary_command_key"))
|
||||
if not operator_lane:
|
||||
if blocking_reasons:
|
||||
operator_lane = "recovery"
|
||||
elif warnings:
|
||||
operator_lane = "verification"
|
||||
else:
|
||||
operator_lane = "go-live"
|
||||
if not operator_title:
|
||||
operator_title = (
|
||||
ensure_str(overview_recommendation.get("title"))
|
||||
or ensure_str(launchpad_status.get("status_label"))
|
||||
or ("先处理阻塞项" if blocking_reasons else "可以进入上线复核")
|
||||
)
|
||||
|
||||
next_step_action_code = (
|
||||
ensure_str(next_step.get("action_code"))
|
||||
or ensure_str(overview_recommendation.get("primary_action_code"))
|
||||
or ensure_str(launchpad_status.get("recommended_action_code"))
|
||||
)
|
||||
next_step_reason = (
|
||||
ensure_str(next_step.get("reason"))
|
||||
or ensure_str(overview_recommendation.get("reason"))
|
||||
or "来自 launchpad/overview fallback"
|
||||
)
|
||||
if not operator_primary_command_key:
|
||||
operator_primary_command_key = next_step_action_code
|
||||
|
||||
go_live_status = "ready"
|
||||
if blocking_reasons:
|
||||
go_live_status = "blocked"
|
||||
elif warnings:
|
||||
go_live_status = "attention"
|
||||
|
||||
publish_status = "ready"
|
||||
if publish_blocking_reasons:
|
||||
publish_status = "blocked"
|
||||
elif publish_warnings:
|
||||
publish_status = "attention"
|
||||
publish_ready = publish_status == "ready"
|
||||
if publish_status == "ready":
|
||||
publish_status_label = "可发布"
|
||||
elif publish_status == "attention":
|
||||
publish_status_label = "可发布但建议先复核"
|
||||
else:
|
||||
publish_status_label = "暂不可发布"
|
||||
|
||||
action_commands = {
|
||||
"handover_first_gap": f"bash domain-api/deploy/multi-region/drive_ops_center.sh driver-resolve {base_url} handover_first_gap",
|
||||
"view_first_gap": f"bash domain-api/deploy/multi-region/drive_ops_center.sh driver-resolve {base_url} view_first_gap",
|
||||
"bootstrap_run": (
|
||||
f"bash domain-api/deploy/multi-region/drive_ops_center.sh node-bootstrap-run {base_url} {launchpad_recommended_target_node_code} cli"
|
||||
if launchpad_recommended_target_node_code
|
||||
else f"bash domain-api/deploy/multi-region/drive_ops_center.sh driver-resolve {base_url} bootstrap_run"
|
||||
),
|
||||
"run_acceptance": (
|
||||
f"bash domain-api/deploy/multi-region/drive_ops_center.sh node-acceptance-run {base_url} {launchpad_recommended_target_node_code} cli"
|
||||
if launchpad_recommended_target_node_code
|
||||
else f"bash domain-api/deploy/multi-region/drive_ops_center.sh driver-resolve {base_url} run_acceptance"
|
||||
),
|
||||
"fix_managed_nodes": f"bash domain-api/deploy/multi-region/drive_ops_center.sh driver-resolve {base_url} fix_managed_nodes",
|
||||
"open_worker_logs_participating": f"bash domain-api/deploy/multi-region/drive_ops_center.sh driver-resolve {base_url} open_worker_logs_participating",
|
||||
"run_inspection_participating": f"bash domain-api/deploy/multi-region/drive_ops_center.sh driver-resolve {base_url} run_inspection_participating",
|
||||
}
|
||||
|
||||
recommended_commands = {
|
||||
"stack_summary": f"bash domain-api/deploy/multi-region/check_ops_center_stack.sh {base_url} summary",
|
||||
"contracts": f"bash domain-api/deploy/multi-region/check_ops_contracts.sh {base_url}",
|
||||
"ops_plane": f"bash domain-api/deploy/multi-region/check_ops_plane.sh {base_url}",
|
||||
"release_hub": f"bash domain-api/deploy/multi-region/check_release_hub.sh {base_url}",
|
||||
"inspection": f"bash domain-api/deploy/multi-region/check_ops_inspection.sh {base_url}",
|
||||
"node_agent": "bash domain-api/deploy/multi-region/check_node_agent.sh",
|
||||
"overview": f"bash domain-api/deploy/multi-region/drive_ops_center.sh overview {base_url}",
|
||||
"doctor_export": (
|
||||
f"bash domain-api/deploy/multi-region/drive_ops_center.sh doctor-export /tmp/domaincheck-go-live {base_url} {overseas_base_url}"
|
||||
if overseas_base_url
|
||||
else f"bash domain-api/deploy/multi-region/drive_ops_center.sh doctor-export /tmp/domaincheck-go-live {base_url}"
|
||||
),
|
||||
}
|
||||
if next_step_action_code and next_step_action_code in action_commands:
|
||||
recommended_commands["next_step"] = action_commands[next_step_action_code]
|
||||
if log_sync_enabled:
|
||||
if "open_worker_logs_participating" in action_commands:
|
||||
recommended_commands["log_sync_logs"] = action_commands["open_worker_logs_participating"]
|
||||
if "run_inspection_participating" in action_commands:
|
||||
recommended_commands["log_sync_inspection"] = action_commands["run_inspection_participating"]
|
||||
|
||||
summary = {
|
||||
"base_url": base_url,
|
||||
"overseas_base_url": overseas_base_url,
|
||||
"go_live_status": go_live_status,
|
||||
"publish_ready": publish_ready,
|
||||
"publish_status": publish_status,
|
||||
"publish_status_label": publish_status_label,
|
||||
"stack_status": stack_status,
|
||||
"contracts_ready": truthy_http(contracts_status_code) and len(contracts) > 0,
|
||||
"contracts_total": len(contracts),
|
||||
"launchpad_status": launchpad_status_code_value,
|
||||
"launchpad_status_label": ensure_str(launchpad_status.get("status_label")),
|
||||
"launchpad_recommended_action_code": ensure_str(launchpad_status.get("recommended_action_code")),
|
||||
"launchpad_recommended_target_node_code": launchpad_recommended_target_node_code,
|
||||
"launchpad_recommended_recovery_label": launchpad_recommended_recovery_label,
|
||||
"launchpad_recommended_recovery_summary": launchpad_recommended_recovery_summary,
|
||||
"launchpad_onboarding_bootstrap_pending_nodes": launchpad_onboarding_bootstrap_pending_nodes,
|
||||
"launchpad_onboarding_acceptance_ready_nodes": launchpad_onboarding_acceptance_ready_nodes,
|
||||
"route_surface_complete": surface_complete,
|
||||
"route_surface_missing_keys": [
|
||||
ensure_str(item) for item in ensure_list(route_surface.get("missing_keys")) if ensure_str(item)
|
||||
],
|
||||
"managed_enabled": managed_enabled,
|
||||
"remote_access_ready": remote_access_ready,
|
||||
"queue_dead_letter_nodes": queue_dead_letter_nodes,
|
||||
"participating_nodes_total": participating_nodes_total,
|
||||
"log_sync_enabled": log_sync_enabled,
|
||||
"log_sync_state": log_sync_state,
|
||||
"log_sync_mode": log_sync_mode,
|
||||
"log_sync_covered_nodes": covered_participating_nodes,
|
||||
"log_sync_missing_node_codes": missing_participating_node_codes,
|
||||
"next_step_action_code": next_step_action_code,
|
||||
"next_step_reason": next_step_reason,
|
||||
"operator_lane": operator_lane,
|
||||
"operator_title": operator_title,
|
||||
"operator_primary_command_key": operator_primary_command_key,
|
||||
"local_node_agent": {
|
||||
"service_name": local_node_agent_service_name,
|
||||
"installed": local_agent_installed,
|
||||
"enabled": local_agent_enabled,
|
||||
"active": local_agent_active,
|
||||
},
|
||||
"blocking_reasons": blocking_reasons,
|
||||
"warnings": warnings,
|
||||
"publish_blocking_reasons": publish_blocking_reasons,
|
||||
"publish_warnings": publish_warnings,
|
||||
"recommended_commands": recommended_commands,
|
||||
}
|
||||
|
||||
print(json.dumps(summary, ensure_ascii=False, indent=2))
|
||||
PY
|
||||
182
domain-api/deploy/multi-region/check_go_live_signoff.sh
Normal file
182
domain-api/deploy/multi-region/check_go_live_signoff.sh
Normal file
@@ -0,0 +1,182 @@
|
||||
#!/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}"
|
||||
|
||||
TARGET_OR_BASE_URL="${1:-}"
|
||||
SECOND_ARG="${2:-}"
|
||||
|
||||
REPORT_DIR=""
|
||||
MANIFEST_PATH=""
|
||||
GENERATED_BUNDLE="false"
|
||||
MAINLAND_BASE_URL=""
|
||||
OVERSEAS_BASE_URL=""
|
||||
|
||||
if [[ -n "${TARGET_OR_BASE_URL}" && ( -f "${TARGET_OR_BASE_URL}" || -d "${TARGET_OR_BASE_URL}" ) ]]; then
|
||||
if [[ -d "${TARGET_OR_BASE_URL}" ]]; then
|
||||
REPORT_DIR="${TARGET_OR_BASE_URL%/}"
|
||||
MANIFEST_PATH="${REPORT_DIR}/manifest.json"
|
||||
else
|
||||
MANIFEST_PATH="${TARGET_OR_BASE_URL}"
|
||||
REPORT_DIR="$(cd "$(dirname "${MANIFEST_PATH}")" && pwd)"
|
||||
fi
|
||||
else
|
||||
MAINLAND_BASE_URL="${TARGET_OR_BASE_URL:-$(ops_center_resolve_mainland_api_base_url)}"
|
||||
OVERSEAS_BASE_URL="$(normalize_optional_base_url_arg "${SECOND_ARG:-}" "$(ops_center_resolve_overseas_api_base_url)")"
|
||||
REPORT_DIR="${OPS_CENTER_REPORT_ROOT}/go-live-bundles/go-live-signoff-$(timestamp_now)"
|
||||
ops_center_ensure_report_dir "${REPORT_DIR}"
|
||||
bash "${SCRIPT_DIR}/export_go_live_bundle.sh" "${REPORT_DIR}" "${MAINLAND_BASE_URL}" "${OVERSEAS_BASE_URL}" >/dev/null
|
||||
MANIFEST_PATH="${REPORT_DIR}/manifest.json"
|
||||
GENERATED_BUNDLE="true"
|
||||
fi
|
||||
|
||||
if [[ ! -f "${MANIFEST_PATH}" ]]; then
|
||||
echo "manifest not found: ${MANIFEST_PATH}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
REVIEW_JSON="$(bash "${SCRIPT_DIR}/review_go_live_bundle.sh" "${MANIFEST_PATH}")"
|
||||
DOCTOR_JSON="$(bash "${SCRIPT_DIR}/drive_ops_center.sh" doctor-decision "${MANIFEST_PATH}")"
|
||||
|
||||
python3 - <<'PY' \
|
||||
"${REVIEW_JSON}" \
|
||||
"${DOCTOR_JSON}" \
|
||||
"${GENERATED_BUNDLE}" \
|
||||
"${REPORT_DIR}" \
|
||||
"${MANIFEST_PATH}" \
|
||||
"${MAINLAND_BASE_URL}" \
|
||||
"${OVERSEAS_BASE_URL}"
|
||||
import json
|
||||
import sys
|
||||
|
||||
review = json.loads(sys.argv[1])
|
||||
doctor = json.loads(sys.argv[2])
|
||||
generated_bundle = str(sys.argv[3]).strip().lower() == "true"
|
||||
report_dir = str(sys.argv[4] or "").strip()
|
||||
manifest_path = str(sys.argv[5] or "").strip()
|
||||
mainland_base_url = str(sys.argv[6] or "").strip()
|
||||
overseas_base_url = str(sys.argv[7] or "").strip()
|
||||
|
||||
go_live_summary = dict(review.get("go_live_summary") or {})
|
||||
stack_diagnosis = dict(review.get("stack_diagnosis") or {})
|
||||
driver_feed = dict(review.get("driver_feed") or {})
|
||||
codex_brief = dict(review.get("codex_brief") or {})
|
||||
launchpad_alignment = dict(review.get("launchpad_alignment") or {})
|
||||
doctor_decision = dict(doctor.get("decision") or {})
|
||||
doctor_summary = dict(doctor.get("summary") or {})
|
||||
|
||||
review_status = str(review.get("status") or "").strip()
|
||||
doctor_status = str(doctor_decision.get("status") or "").strip()
|
||||
go_live_status = str(go_live_summary.get("go_live_status") or "").strip()
|
||||
publish_status = str(go_live_summary.get("publish_status") or "").strip()
|
||||
stack_status = str(stack_diagnosis.get("stack_status") or "").strip()
|
||||
driver_launch_status = str(driver_feed.get("launch_status") or "").strip()
|
||||
codex_launch_status = str(codex_brief.get("launch_status") or "").strip()
|
||||
|
||||
|
||||
def dedupe_commands(values):
|
||||
result = []
|
||||
seen = set()
|
||||
for item in values:
|
||||
text = str(item or "").strip()
|
||||
if not text or text in seen:
|
||||
continue
|
||||
seen.add(text)
|
||||
result.append(text)
|
||||
return result
|
||||
|
||||
|
||||
blocked_reasons = dedupe_commands(
|
||||
[]
|
||||
+ ([f"review:{review_status}"] if review_status == "blocked" else [])
|
||||
+ ([f"doctor:{doctor_status}"] if doctor_status == "blocked" else [])
|
||||
+ ([f"go_live:{go_live_status}"] if go_live_status == "blocked" else [])
|
||||
+ ([f"publish:{publish_status}"] if publish_status == "blocked" else [])
|
||||
+ ([f"stack:{stack_status}"] if stack_status == "blocked" else [])
|
||||
+ ([f"driver_launch:{driver_launch_status}"] if driver_launch_status == "blocked" else [])
|
||||
+ ([f"codex_launch:{codex_launch_status}"] if codex_launch_status == "blocked" else [])
|
||||
)
|
||||
|
||||
attention_reasons = dedupe_commands(
|
||||
[]
|
||||
+ ([f"review:{review_status}"] if review_status == "attention" else [])
|
||||
+ ([f"doctor:{doctor_status}"] if doctor_status == "attention" else [])
|
||||
+ ([f"go_live:{go_live_status}"] if go_live_status == "attention" else [])
|
||||
+ ([f"publish:{publish_status}"] if publish_status == "attention" else [])
|
||||
+ ([f"stack:{stack_status}"] if stack_status == "attention" else [])
|
||||
+ ([f"driver_launch:{driver_launch_status}"] if driver_launch_status == "attention" else [])
|
||||
+ ([f"codex_launch:{codex_launch_status}"] if codex_launch_status == "attention" else [])
|
||||
+ (["launchpad_alignment:inconsistent"] if not bool(launchpad_alignment.get("consistent", False)) else [])
|
||||
)
|
||||
|
||||
if blocked_reasons:
|
||||
signoff_status = "blocked"
|
||||
headline = (
|
||||
str(review.get("headline") or "").strip()
|
||||
or str(doctor_decision.get("headline") or "").strip()
|
||||
or "当前发布前验证仍存在阻断项,不应直接签字上线。"
|
||||
)
|
||||
elif attention_reasons:
|
||||
signoff_status = "attention"
|
||||
headline = (
|
||||
str(doctor_decision.get("headline") or "").strip()
|
||||
or str(review.get("headline") or "").strip()
|
||||
or "当前现场已进入收口区间,但仍建议先完成 attention 项再正式签字。"
|
||||
)
|
||||
else:
|
||||
signoff_status = "ready"
|
||||
headline = "当前证据包、doctor 结论与发布门禁一致,可进入最终人工签字或正式发布。"
|
||||
|
||||
recommended_commands = dedupe_commands(
|
||||
[]
|
||||
+ list(doctor_decision.get("recommended_commands") or [])
|
||||
+ [
|
||||
f"bash domain-api/deploy/multi-region/drive_ops_center.sh go-live-review {manifest_path}",
|
||||
f"bash domain-api/deploy/multi-region/drive_ops_center.sh doctor-decision {manifest_path}",
|
||||
]
|
||||
)
|
||||
|
||||
payload = {
|
||||
"signoff_status": signoff_status,
|
||||
"headline": headline,
|
||||
"generated_bundle": generated_bundle,
|
||||
"report_dir": report_dir or str(doctor.get("report_dir") or review.get("report_dir") or ""),
|
||||
"manifest_path": manifest_path or str(doctor.get("manifest_path") or ""),
|
||||
"base_urls": {
|
||||
"mainland": mainland_base_url,
|
||||
"overseas": overseas_base_url,
|
||||
},
|
||||
"release_gate": {
|
||||
"go_live_status": go_live_status,
|
||||
"publish_status": publish_status,
|
||||
"stack_status": stack_status,
|
||||
"driver_launch_status": driver_launch_status,
|
||||
"codex_launch_status": codex_launch_status,
|
||||
"bundle_review_status": review_status,
|
||||
"doctor_status": doctor_status,
|
||||
},
|
||||
"blocked_reasons": blocked_reasons,
|
||||
"attention_reasons": attention_reasons,
|
||||
"decision": {
|
||||
"operator_title": str(go_live_summary.get("operator_title") or "").strip(),
|
||||
"next_step_action_code": str(go_live_summary.get("next_step_action_code") or "").strip(),
|
||||
"doctor_reason_code": str(doctor_decision.get("reason_code") or "").strip(),
|
||||
"doctor_preferred_surface": str(doctor_decision.get("preferred_surface") or "").strip(),
|
||||
"launchpad_recommended_target_node_code": str(go_live_summary.get("launchpad_recommended_target_node_code") or "").strip(),
|
||||
"launchpad_recommended_recovery_label": str(go_live_summary.get("launchpad_recommended_recovery_label") or "").strip(),
|
||||
"launchpad_recommended_recovery_summary": str(go_live_summary.get("launchpad_recommended_recovery_summary") or "").strip(),
|
||||
},
|
||||
"launchpad_alignment": launchpad_alignment,
|
||||
"recommended_commands": recommended_commands,
|
||||
"bundle_review": review,
|
||||
"doctor_decision": {
|
||||
"summary": doctor_summary,
|
||||
"decision": doctor_decision,
|
||||
},
|
||||
}
|
||||
|
||||
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
||||
PY
|
||||
38
domain-api/deploy/multi-region/check_node_agent.sh
Executable file
38
domain-api/deploy/multi-region/check_node_agent.sh
Executable file
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SERVICE_NAME="${SERVICE_NAME:-domaincheck-node-agent}"
|
||||
ENV_FILE="${ENV_FILE:-/etc/default/domaincheck-node-agent}"
|
||||
|
||||
echo "[1/4] environment file"
|
||||
if [[ -f "${ENV_FILE}" ]]; then
|
||||
echo "env file: ${ENV_FILE}"
|
||||
else
|
||||
echo "missing env file: ${ENV_FILE}"
|
||||
fi
|
||||
echo
|
||||
|
||||
echo "[2/4] key env summary"
|
||||
if [[ -f "${ENV_FILE}" ]]; then
|
||||
grep -E '^(OPS_CONTROL_PLANE_BASE_URL|NODE_CODE|NODE_REGION|NODE_ROLE|OPS_AGENT_POLL_INTERVAL_SECONDS|WORKER_SERVICE_NAME|API_SERVICE_NAME|SYNC_AGENT_SERVICE_NAME)=' "${ENV_FILE}" || true
|
||||
else
|
||||
echo "skip"
|
||||
fi
|
||||
echo
|
||||
|
||||
echo "[3/4] systemd status"
|
||||
if systemctl list-unit-files "${SERVICE_NAME}.service" --no-pager >/dev/null 2>&1; then
|
||||
systemctl is-enabled "${SERVICE_NAME}" || true
|
||||
systemctl is-active "${SERVICE_NAME}" || true
|
||||
else
|
||||
echo "not-installed"
|
||||
echo "inactive"
|
||||
fi
|
||||
echo
|
||||
|
||||
echo "[4/4] service detail"
|
||||
if systemctl list-unit-files "${SERVICE_NAME}.service" --no-pager >/dev/null 2>&1; then
|
||||
systemctl status "${SERVICE_NAME}" --no-pager -l | sed -n '1,25p' || true
|
||||
else
|
||||
echo "service not found: ${SERVICE_NAME}"
|
||||
fi
|
||||
65
domain-api/deploy/multi-region/check_node_scene_log.sh
Normal file
65
domain-api/deploy/multi-region/check_node_scene_log.sh
Normal file
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
BASE_URL="${1:-http://127.0.0.1:8100}"
|
||||
NODE_CODE="${2:-}"
|
||||
LIMIT="${3:-80}"
|
||||
MODE="${4:-key}"
|
||||
CURL_CONNECT_TIMEOUT="${CURL_CONNECT_TIMEOUT:-3}"
|
||||
CURL_MAX_TIME="${CURL_MAX_TIME:-8}"
|
||||
|
||||
if [[ -z "${NODE_CODE}" ]]; then
|
||||
echo "usage: bash domain-api/deploy/multi-region/check_node_scene_log.sh [base_url] <node_code> [limit] [key|full]" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
RAW_RESPONSE="$(curl -sS \
|
||||
--connect-timeout "${CURL_CONNECT_TIMEOUT}" \
|
||||
--max-time "${CURL_MAX_TIME}" \
|
||||
"${BASE_URL}/api/v1/ops/nodes/${NODE_CODE}/scene-log?limit=${LIMIT}&mode=${MODE}")"
|
||||
|
||||
echo "[1/2] raw response"
|
||||
printf '%s\n\n' "${RAW_RESPONSE}"
|
||||
|
||||
echo "[2/2] condensed scene-log summary"
|
||||
python3 - "${RAW_RESPONSE}" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
|
||||
raw = str(sys.argv[1] or "")
|
||||
payload = json.loads(raw)
|
||||
data = dict(payload.get("data") or {})
|
||||
participation = dict(data.get("participation") or {})
|
||||
source_summary = dict(data.get("source_summary") or {})
|
||||
records = list(data.get("records") or [])
|
||||
latest = dict(data.get("latest_record") or {})
|
||||
|
||||
summary = {
|
||||
"node_code": str(data.get("node_code") or ""),
|
||||
"status": str(data.get("status") or ""),
|
||||
"status_label": str(data.get("status_label") or ""),
|
||||
"mode": str(data.get("mode") or ""),
|
||||
"summary": str(data.get("summary") or ""),
|
||||
"log_sync_enabled": bool(data.get("log_sync_enabled", False)),
|
||||
"records_total": int(data.get("records_total", len(records)) or 0),
|
||||
"missing_reason_code": str(data.get("missing_reason_code") or ""),
|
||||
"participation": {
|
||||
"detect_participating": bool(participation.get("detect_participating", False)),
|
||||
"participation_state": str(participation.get("participation_state") or ""),
|
||||
"participation_label": str(participation.get("participation_label") or ""),
|
||||
"participation_reason": str(participation.get("participation_reason") or ""),
|
||||
"current_load": int(participation.get("current_load", 0) or 0),
|
||||
},
|
||||
"source_summary": {
|
||||
"line_count": int(source_summary.get("line_count", 0) or 0),
|
||||
"key_line_count": int(source_summary.get("key_line_count", 0) or 0),
|
||||
"full_line_count": int(source_summary.get("full_line_count", 0) or 0),
|
||||
"last_at": str(source_summary.get("last_at") or ""),
|
||||
"last_line": str(source_summary.get("last_line") or ""),
|
||||
},
|
||||
"latest_record": latest,
|
||||
"sample_messages": [str(item.get("message") or "") for item in records[:8]],
|
||||
}
|
||||
|
||||
print(json.dumps(summary, ensure_ascii=False, indent=2))
|
||||
PY
|
||||
1272
domain-api/deploy/multi-region/check_ops_center_stack.sh
Normal file
1272
domain-api/deploy/multi-region/check_ops_center_stack.sh
Normal file
File diff suppressed because it is too large
Load Diff
55
domain-api/deploy/multi-region/check_ops_contracts.sh
Executable file
55
domain-api/deploy/multi-region/check_ops_contracts.sh
Executable file
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
BASE_URL="${1:-http://127.0.0.1:8100}"
|
||||
|
||||
contracts_json="$(curl -fsS "${BASE_URL}/api/v1/ops/contracts")"
|
||||
|
||||
echo "[1/3] ops contracts registry"
|
||||
printf '%s\n\n' "${contracts_json}"
|
||||
|
||||
echo "[2/3] condensed summary"
|
||||
python3 - <<'PY' "${contracts_json}"
|
||||
import json
|
||||
import sys
|
||||
|
||||
payload = json.loads(sys.argv[1])
|
||||
data = payload.get("data") or {}
|
||||
contracts = list(data.get("contracts") or [])
|
||||
|
||||
summary = {
|
||||
"registry_version": str(data.get("registry_version") or ""),
|
||||
"schema_version": str(data.get("schema_version") or ""),
|
||||
"contracts_total": len(contracts),
|
||||
"contract_keys": [str(item.get("key") or "") for item in contracts if str(item.get("key") or "")],
|
||||
"docs_root": str(data.get("docs_root") or ""),
|
||||
"discovery_entrypoints": list(data.get("discovery_entrypoints") or []),
|
||||
}
|
||||
|
||||
print(json.dumps(summary, ensure_ascii=False, indent=2))
|
||||
PY
|
||||
echo
|
||||
|
||||
echo "[3/3] contract rows"
|
||||
python3 - <<'PY' "${contracts_json}"
|
||||
import json
|
||||
import sys
|
||||
|
||||
payload = json.loads(sys.argv[1])
|
||||
data = payload.get("data") or {}
|
||||
contracts = list(data.get("contracts") or [])
|
||||
|
||||
rows = [
|
||||
{
|
||||
"key": str(item.get("key") or ""),
|
||||
"version": str(item.get("version") or ""),
|
||||
"status": str(item.get("status") or ""),
|
||||
"primary_endpoint": str(item.get("primary_endpoint") or ""),
|
||||
"schema_doc_path": str(item.get("schema_doc_path") or ""),
|
||||
"consumers": list(item.get("consumers") or []),
|
||||
}
|
||||
for item in contracts
|
||||
]
|
||||
|
||||
print(json.dumps(rows, ensure_ascii=False, indent=2))
|
||||
PY
|
||||
72
domain-api/deploy/multi-region/check_ops_inspection.sh
Executable file
72
domain-api/deploy/multi-region/check_ops_inspection.sh
Executable file
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
BASE_URL="${1:-http://127.0.0.1:8100}"
|
||||
CURL_CONNECT_TIMEOUT="${CURL_CONNECT_TIMEOUT:-3}"
|
||||
CURL_MAX_TIME="${CURL_MAX_TIME:-10}"
|
||||
|
||||
curl_json() {
|
||||
curl -fsS \
|
||||
--connect-timeout "${CURL_CONNECT_TIMEOUT}" \
|
||||
--max-time "${CURL_MAX_TIME}" \
|
||||
"$1"
|
||||
}
|
||||
|
||||
INSPECTION_JSON="$(curl_json "${BASE_URL}/api/v1/ops/inspection-overview?limit=120")"
|
||||
|
||||
echo "[1/5] inspection overview"
|
||||
printf '%s\n\n' "${INSPECTION_JSON}"
|
||||
|
||||
echo "[2/5] condensed summary"
|
||||
python3 - <<'PY' "${INSPECTION_JSON}"
|
||||
import json
|
||||
import sys
|
||||
|
||||
payload = json.loads(sys.argv[1]).get("data") or {}
|
||||
print(json.dumps({
|
||||
"rows_total": int(payload.get("rows_total") or 0),
|
||||
"filtered_rows_total": int(payload.get("filtered_rows_total") or 0),
|
||||
"visible_nodes_total": int(payload.get("visible_nodes_total") or 0),
|
||||
"eligible_nodes_total": int(payload.get("eligible_nodes_total") or 0),
|
||||
"handover_gap_nodes_total": int(payload.get("handover_gap_nodes_total") or 0),
|
||||
"participating_nodes_total": int(payload.get("participating_nodes_total") or 0),
|
||||
"summary": dict(payload.get("summary") or {}),
|
||||
"available_problem_kind_counts": dict(payload.get("available_problem_kind_counts") or {}),
|
||||
"filters": dict(payload.get("filters") or {}),
|
||||
}, ensure_ascii=False, indent=2))
|
||||
PY
|
||||
echo
|
||||
|
||||
echo "[3/5] priority queue"
|
||||
python3 - <<'PY' "${INSPECTION_JSON}"
|
||||
import json
|
||||
import sys
|
||||
|
||||
payload = json.loads(sys.argv[1]).get("data") or {}
|
||||
rows = list(payload.get("priority_queue") or [])
|
||||
print(json.dumps({
|
||||
"total": len(rows),
|
||||
"items": [
|
||||
{
|
||||
"node_code": str(item.get("node_code") or ""),
|
||||
"overall_status": str(item.get("overall_status") or ""),
|
||||
"problem_kind": str(item.get("problem_kind") or ""),
|
||||
"problem_label": str(item.get("problem_label") or ""),
|
||||
"recommended_action_code": str(item.get("recommended_action_code") or ""),
|
||||
"last_updated_at": str(item.get("last_updated_at") or ""),
|
||||
"issue_summary": str(item.get("issue_summary") or ""),
|
||||
}
|
||||
for item in rows[:12]
|
||||
],
|
||||
}, ensure_ascii=False, indent=2))
|
||||
PY
|
||||
echo
|
||||
|
||||
echo "[4/5] attention only"
|
||||
curl_json "${BASE_URL}/api/v1/ops/inspection-overview?limit=80&status=attention&only_problem=true"
|
||||
echo
|
||||
echo
|
||||
|
||||
echo "[5/5] participating problems only"
|
||||
curl_json "${BASE_URL}/api/v1/ops/inspection-overview?limit=80&only_problem=true&only_participating=true"
|
||||
echo
|
||||
35
domain-api/deploy/multi-region/check_ops_link_snapshot.sh
Normal file
35
domain-api/deploy/multi-region/check_ops_link_snapshot.sh
Normal file
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
BASE_URL="${1:-http://127.0.0.1:8100}"
|
||||
PYTHON_BIN="${PYTHON_BIN:-python3}"
|
||||
|
||||
echo "[1/2] ops link snapshot"
|
||||
SNAPSHOT_JSON="$(curl -fsS "${BASE_URL}/api/v1/ops/link-snapshot")"
|
||||
echo "${SNAPSHOT_JSON}"
|
||||
echo
|
||||
echo
|
||||
|
||||
echo "[2/2] condensed summary"
|
||||
"${PYTHON_BIN}" - <<'PY' "$SNAPSHOT_JSON"
|
||||
import json
|
||||
import sys
|
||||
|
||||
payload = json.loads(sys.argv[1]).get("data", {})
|
||||
print(f"status: {payload.get('status', '-')}")
|
||||
print(f"headline: {payload.get('headline', '-')}")
|
||||
print()
|
||||
print("summary:")
|
||||
for line in payload.get("summary_lines", []) or []:
|
||||
print(f"- {line}")
|
||||
|
||||
actions = payload.get("next_actions", []) or []
|
||||
if actions:
|
||||
print()
|
||||
print("next_actions:")
|
||||
for item in actions:
|
||||
label = str(item.get("label", "") or "").strip() or "-"
|
||||
action_code = str(item.get("action_code", "") or "").strip() or "-"
|
||||
node_codes = "、".join(str(x).strip() for x in (item.get("node_codes", []) or []) if str(x).strip()) or "-"
|
||||
print(f"- {label} / {action_code} / nodes: {node_codes}")
|
||||
PY
|
||||
609
domain-api/deploy/multi-region/check_ops_plane.sh
Normal file
609
domain-api/deploy/multi-region/check_ops_plane.sh
Normal file
@@ -0,0 +1,609 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
BASE_URL="${1:-http://127.0.0.1:8100}"
|
||||
CURL_CONNECT_TIMEOUT="${CURL_CONNECT_TIMEOUT:-3}"
|
||||
CURL_MAX_TIME="${CURL_MAX_TIME:-10}"
|
||||
|
||||
curl_json() {
|
||||
curl -fsS \
|
||||
--connect-timeout "${CURL_CONNECT_TIMEOUT}" \
|
||||
--max-time "${CURL_MAX_TIME}" \
|
||||
"$1"
|
||||
}
|
||||
|
||||
curl_post_json() {
|
||||
local url="$1"
|
||||
local body="$2"
|
||||
curl -fsS \
|
||||
--connect-timeout "${CURL_CONNECT_TIMEOUT}" \
|
||||
--max-time "${CURL_MAX_TIME}" \
|
||||
-X POST "${url}" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d "${body}"
|
||||
}
|
||||
|
||||
OVERVIEW_JSON="$(curl_json "${BASE_URL}/api/v1/ops/overview")"
|
||||
NODES_JSON="$(curl_json "${BASE_URL}/api/v1/ops/nodes")"
|
||||
JOBS_JSON="$(curl_json "${BASE_URL}/api/v1/ops/jobs")"
|
||||
ACTIVITY_JSON="$(curl_json "${BASE_URL}/api/v1/ops/activity-stream")"
|
||||
LAUNCHPAD_JSON="$(curl_json "${BASE_URL}/api/v1/ops/releases/launchpad")"
|
||||
LATEST_RELEASE_JSON="$(curl_json "${BASE_URL}/api/v1/ops/releases/latest")"
|
||||
POLICY_PREVIEW_JSON="$(
|
||||
curl_post_json "${BASE_URL}/api/v1/ops/policy/preview" \
|
||||
'{"action":"runtime.restart_api","target_type":"node","target_node_code":"mainland-controller-01"}'
|
||||
)"
|
||||
|
||||
echo "[1/12] ops overview"
|
||||
printf '%s\n\n' "${OVERVIEW_JSON}"
|
||||
|
||||
echo "[2/12] top recommendation"
|
||||
python3 - <<'PY' "${OVERVIEW_JSON}"
|
||||
import json
|
||||
import sys
|
||||
|
||||
payload = json.loads(sys.argv[1])
|
||||
data = payload.get("data") or {}
|
||||
recommendation = dict(data.get("recommendation") or {})
|
||||
print(json.dumps({
|
||||
"source": str(recommendation.get("source") or ""),
|
||||
"key": str(recommendation.get("key") or ""),
|
||||
"priority": str(recommendation.get("priority") or ""),
|
||||
"summary": str(recommendation.get("summary") or ""),
|
||||
"primary_action_code": str(recommendation.get("primary_action_code") or ""),
|
||||
"secondary_action_code": str(recommendation.get("secondary_action_code") or ""),
|
||||
"node_codes": list(recommendation.get("node_codes") or []),
|
||||
}, ensure_ascii=False, indent=2))
|
||||
PY
|
||||
echo
|
||||
|
||||
echo "[3/12] driver recommendations (condensed)"
|
||||
python3 - <<'PY' "${OVERVIEW_JSON}"
|
||||
import json
|
||||
import sys
|
||||
|
||||
payload = json.loads(sys.argv[1])
|
||||
data = payload.get("data") or {}
|
||||
recommendations = list(data.get("driver_recommendations") or [])
|
||||
summary = {
|
||||
"total": len(recommendations),
|
||||
"items": [
|
||||
{
|
||||
"key": str(item.get("key") or ""),
|
||||
"title": str(item.get("title") or ""),
|
||||
"level_label": str(item.get("level_label") or ""),
|
||||
"primary_action_code": str(item.get("primary_action_code") or ""),
|
||||
"secondary_action_code": str(item.get("secondary_action_code") or ""),
|
||||
"meta_text": str(item.get("meta_text") or ""),
|
||||
}
|
||||
for item in recommendations[:8]
|
||||
],
|
||||
}
|
||||
print(json.dumps(summary, ensure_ascii=False, indent=2))
|
||||
PY
|
||||
echo
|
||||
|
||||
echo "[4/12] ops capabilities"
|
||||
curl_json "${BASE_URL}/api/v1/ops/capabilities"
|
||||
echo
|
||||
echo
|
||||
|
||||
echo "[5/12] ops blueprint"
|
||||
curl_json "${BASE_URL}/api/v1/ops/blueprint"
|
||||
echo
|
||||
echo
|
||||
|
||||
echo "[6/12] ops runbook"
|
||||
curl_json "${BASE_URL}/api/v1/ops/runbook"
|
||||
echo
|
||||
echo
|
||||
|
||||
echo "[7/12] ops nodes"
|
||||
printf '%s\n' "${NODES_JSON}"
|
||||
echo
|
||||
echo
|
||||
|
||||
echo "[8/12] ops jobs"
|
||||
printf '%s\n' "${JOBS_JSON}"
|
||||
echo
|
||||
echo
|
||||
|
||||
echo "[9/12] ops activity stream"
|
||||
printf '%s\n' "${ACTIVITY_JSON}"
|
||||
echo
|
||||
echo
|
||||
|
||||
echo "[10/12] release launchpad"
|
||||
printf '%s\n' "${LAUNCHPAD_JSON}"
|
||||
echo
|
||||
echo
|
||||
|
||||
echo "[11/12] latest release"
|
||||
printf '%s\n' "${LATEST_RELEASE_JSON}"
|
||||
echo
|
||||
echo
|
||||
|
||||
echo "[12/12] policy preview sample"
|
||||
printf '%s\n' "${POLICY_PREVIEW_JSON}"
|
||||
echo
|
||||
echo
|
||||
|
||||
echo "[13/13] condensed summary"
|
||||
python3 - <<'PY' \
|
||||
"${BASE_URL}" \
|
||||
"${OVERVIEW_JSON}" \
|
||||
"${NODES_JSON}" \
|
||||
"${JOBS_JSON}" \
|
||||
"${ACTIVITY_JSON}" \
|
||||
"${LAUNCHPAD_JSON}" \
|
||||
"${LATEST_RELEASE_JSON}" \
|
||||
"${POLICY_PREVIEW_JSON}"
|
||||
import json
|
||||
import sys
|
||||
|
||||
|
||||
def as_dict(payload):
|
||||
try:
|
||||
raw = json.loads(payload)
|
||||
except Exception:
|
||||
return {}
|
||||
return raw.get("data") or {}
|
||||
|
||||
|
||||
def ensure_dict(value):
|
||||
return value if isinstance(value, dict) else {}
|
||||
|
||||
|
||||
def ensure_list(value):
|
||||
return value if isinstance(value, list) else []
|
||||
|
||||
|
||||
def ensure_str(value):
|
||||
return value if isinstance(value, str) else ""
|
||||
|
||||
|
||||
def command(name, *parts):
|
||||
tokens = [
|
||||
"bash",
|
||||
"domain-api/deploy/multi-region/drive_ops_center.sh",
|
||||
name,
|
||||
base_url,
|
||||
]
|
||||
tokens.extend([str(part) for part in parts if str(part or "").strip()])
|
||||
return " ".join(tokens)
|
||||
|
||||
|
||||
def release_command(name, *parts):
|
||||
tokens = [
|
||||
"bash",
|
||||
"domain-api/deploy/multi-region/drive_release_hub.sh",
|
||||
base_url,
|
||||
name,
|
||||
]
|
||||
tokens.extend([str(part) for part in parts if str(part or "").strip()])
|
||||
return " ".join(tokens)
|
||||
|
||||
|
||||
def build_operator_decision():
|
||||
remote_access_state_counts = ensure_dict(node_summary.get("remote_access_state_counts"))
|
||||
pending_bootstrap = int(node_summary.get("pending_bootstrap", 0) or 0)
|
||||
runtime_only = int(node_summary.get("runtime_only", 0) or 0)
|
||||
online_total = int(node_summary.get("online", 0) or 0)
|
||||
participating_total = int(scene_log.get("participating_node_count", 0) or 0)
|
||||
launchpad_blocked = bool(launchpad_workers.get("blocked")) or bool(launchpad_controls.get("blocked"))
|
||||
running_total = int(jobs_summary.get("running_count", 0) or 0)
|
||||
queued_total = int(jobs_summary.get("queued_count", 0) or 0)
|
||||
|
||||
if scene_log.get("status") == "disabled" and participating_total > 0:
|
||||
return {
|
||||
"lane": "observability",
|
||||
"priority": "high",
|
||||
"reason_code": "scene_log_disabled",
|
||||
"title": "先恢复远端现场日志可见性",
|
||||
"summary": ensure_str(scene_log.get("summary")),
|
||||
"next_focus": ensure_dict(scene_log.get("focus_ref")),
|
||||
"primary_command_key": "scene_log",
|
||||
"secondary_command_key": "driver_focus_preview",
|
||||
}
|
||||
|
||||
if pending_bootstrap > 0 or remote_access_state_counts:
|
||||
return {
|
||||
"lane": "node_handover",
|
||||
"priority": "high",
|
||||
"reason_code": "managed_nodes_not_ready",
|
||||
"title": "先补齐节点接管与远程可达性",
|
||||
"summary": (
|
||||
f"当前 managed node 共 {int(node_summary.get('managed_total', 0) or 0)} 台,"
|
||||
f"pending_bootstrap={pending_bootstrap},runtime_only={runtime_only}。"
|
||||
),
|
||||
"next_focus": {
|
||||
"kind": "managed_node_handover",
|
||||
"node_code": first_attention_node_code,
|
||||
} if first_attention_node_code else {},
|
||||
"primary_command_key": "node_handover",
|
||||
"secondary_command_key": "node_bootstrap_plan",
|
||||
}
|
||||
|
||||
if launchpad_blocked or str(launchpad_status.get("status") or "") == "attention":
|
||||
return {
|
||||
"lane": "release",
|
||||
"priority": "medium",
|
||||
"reason_code": "release_launchpad_attention",
|
||||
"title": "检查发布驾驶舱与 rollout 阻断",
|
||||
"summary": ensure_str(launchpad_status.get("summary")),
|
||||
"next_focus": release_focus_ref,
|
||||
"primary_command_key": "release_launchpad",
|
||||
"secondary_command_key": "release_preview_latest",
|
||||
}
|
||||
|
||||
if running_total > 0 or queued_total > 0:
|
||||
return {
|
||||
"lane": "ops_jobs",
|
||||
"priority": "medium",
|
||||
"reason_code": "jobs_active",
|
||||
"title": "继续盯运行中任务与活动流",
|
||||
"summary": (
|
||||
f"当前 running={running_total},queued={queued_total},"
|
||||
f"activity_status={ensure_str(activity_summary.get('status')) or 'unknown'}。"
|
||||
),
|
||||
"next_focus": {
|
||||
"kind": "ops_jobs",
|
||||
"section": "activity_stream",
|
||||
},
|
||||
"primary_command_key": "jobs",
|
||||
"secondary_command_key": "activity_preview",
|
||||
}
|
||||
|
||||
return {
|
||||
"lane": "steady",
|
||||
"priority": "low",
|
||||
"reason_code": "steady_state",
|
||||
"title": "当前进入稳定观察态",
|
||||
"summary": (
|
||||
f"online={online_total},attention_nodes={len(attention_nodes)},"
|
||||
f"scene_log_status={ensure_str(scene_log.get('status')) or 'standby'}。"
|
||||
),
|
||||
"next_focus": recommendation_focus_ref or ensure_dict(scene_log.get("focus_ref")),
|
||||
"primary_command_key": "driver_feed",
|
||||
"secondary_command_key": "activity_preview",
|
||||
}
|
||||
|
||||
|
||||
def normalize_scene_log_observation(overview_data):
|
||||
driver_feed = ensure_dict(overview_data.get("driver_feed"))
|
||||
scene_log_data = ensure_dict(driver_feed.get("scene_log_observation"))
|
||||
if scene_log_data:
|
||||
normalized = dict(scene_log_data)
|
||||
normalized["status"] = ensure_str(normalized.get("status")) or "standby"
|
||||
normalized["status_label"] = ensure_str(normalized.get("status_label")) or "待命"
|
||||
normalized["summary"] = ensure_str(normalized.get("summary")) or "当前现场日志观察暂无额外结论。"
|
||||
normalized["focus_ref"] = ensure_dict(normalized.get("focus_ref"))
|
||||
normalized["target_node_code"] = ensure_str(normalized.get("target_node_code"))
|
||||
normalized["participating_node_count"] = int(normalized.get("participating_node_count", 0) or 0)
|
||||
normalized["covered_participating_node_count"] = int(normalized.get("covered_participating_node_count", 0) or 0)
|
||||
normalized["line_count"] = int(normalized.get("line_count", 0) or 0)
|
||||
return normalized
|
||||
|
||||
execution_scene = ensure_dict(overview_data.get("execution_scene"))
|
||||
log_sync = ensure_dict(execution_scene.get("log_sync"))
|
||||
participation = ensure_dict(execution_scene.get("participation_summary"))
|
||||
missing_nodes = ensure_list(log_sync.get("missing_participating_nodes"))
|
||||
participating_nodes = ensure_list(execution_scene.get("participating_nodes"))
|
||||
target_node_code = ""
|
||||
if missing_nodes:
|
||||
target_node_code = ensure_str(missing_nodes[0])
|
||||
elif participating_nodes:
|
||||
target_node_code = ensure_str(ensure_dict(participating_nodes[0]).get("node_code"))
|
||||
|
||||
participating_total = int(
|
||||
log_sync.get("participating_node_count", participation.get("participating_nodes", len(participating_nodes)))
|
||||
or participation.get("participating_nodes", len(participating_nodes))
|
||||
or len(participating_nodes)
|
||||
or 0
|
||||
)
|
||||
covered_total = int(log_sync.get("covered_participating_node_count", 0) or 0)
|
||||
line_count = int(log_sync.get("line_count", 0) or 0)
|
||||
enabled = bool(log_sync.get("enabled", False))
|
||||
mode = ensure_str(log_sync.get("mode")) or "key"
|
||||
status = "standby"
|
||||
status_label = "待命"
|
||||
summary = "当前没有参与检测节点,现场日志观察处于待命状态。"
|
||||
if participating_total > 0 and not enabled:
|
||||
status = "disabled"
|
||||
status_label = "未开启"
|
||||
summary = (
|
||||
f"当前有 {participating_total} 台参与节点,但远端日志回传仍关闭,"
|
||||
"海外控制面还看不到节点级现场日志。"
|
||||
)
|
||||
elif participating_total > 0 and (
|
||||
line_count <= 0 or covered_total <= 0 or int(log_sync.get("missing_participating_node_count", len(missing_nodes)) or len(missing_nodes)) > 0
|
||||
):
|
||||
status = "waiting_sample"
|
||||
status_label = "等待样本"
|
||||
summary = (
|
||||
f"远端日志回传已经开启,但参与节点只覆盖 {covered_total}/{participating_total},"
|
||||
f"样本 {line_count} 条,仍需继续观察或下钻节点现场日志。"
|
||||
)
|
||||
elif participating_total > 0:
|
||||
status = "ready"
|
||||
status_label = "可下钻"
|
||||
summary = (
|
||||
f"远端日志回传已经形成样本,当前覆盖 {covered_total}/{participating_total} 台参与节点,"
|
||||
"可以直接下钻节点现场日志。"
|
||||
)
|
||||
|
||||
focus_ref = {}
|
||||
if target_node_code:
|
||||
focus_ref = {
|
||||
"kind": "node_scene_log",
|
||||
"node_code": target_node_code,
|
||||
"mode": mode,
|
||||
"limit": 120 if status == "waiting_sample" else 80,
|
||||
}
|
||||
return {
|
||||
"status": status,
|
||||
"status_label": status_label,
|
||||
"summary": summary,
|
||||
"focus_ref": focus_ref,
|
||||
"target_node_code": target_node_code,
|
||||
"participating_node_count": participating_total,
|
||||
"covered_participating_node_count": covered_total,
|
||||
"line_count": line_count,
|
||||
}
|
||||
|
||||
|
||||
base_url = ensure_str(sys.argv[1]) or "http://127.0.0.1:8100"
|
||||
overview = as_dict(sys.argv[2])
|
||||
nodes = as_dict(sys.argv[3])
|
||||
jobs = as_dict(sys.argv[4])
|
||||
activity = as_dict(sys.argv[5])
|
||||
launchpad = as_dict(sys.argv[6])
|
||||
latest_release = as_dict(sys.argv[7])
|
||||
policy_preview = as_dict(sys.argv[8])
|
||||
|
||||
recommendation = ensure_dict(overview.get("recommendation"))
|
||||
scene_log = normalize_scene_log_observation(overview)
|
||||
managed_nodes = ensure_list(nodes.get("managed_nodes"))
|
||||
node_summary = ensure_dict(nodes.get("summary"))
|
||||
jobs_summary = ensure_dict(jobs.get("summary"))
|
||||
activity_summary = ensure_dict(activity.get("summary"))
|
||||
launchpad_release = ensure_dict(launchpad.get("release"))
|
||||
launchpad_rec = ensure_dict(launchpad.get("top_recommendation"))
|
||||
launchpad_workers = ensure_dict(launchpad.get("worker_rollout_preview"))
|
||||
launchpad_controls = ensure_dict(launchpad.get("control_rollout_preview"))
|
||||
launchpad_status = ensure_dict(launchpad.get("launchpad_status"))
|
||||
latest_release_entry = latest_release if isinstance(latest_release, dict) else {}
|
||||
latest_package_preview = ensure_dict(launchpad.get("latest_package_preview"))
|
||||
|
||||
online_nodes = [
|
||||
node for node in managed_nodes
|
||||
if str(node.get("status") or "").lower() in {"online", "busy"}
|
||||
]
|
||||
attention_nodes = [
|
||||
{
|
||||
"node_code": str(node.get("node_code") or ""),
|
||||
"status": str(node.get("status") or ""),
|
||||
"role": str(node.get("role") or ""),
|
||||
"stack_status_label": str(node.get("stack_status_label") or ""),
|
||||
"recommended_action_code": str(node.get("recommended_action_code") or ""),
|
||||
"recommended_action_label": str(node.get("recommended_action_label") or ""),
|
||||
"primary_issue": str(node.get("primary_issue") or ""),
|
||||
}
|
||||
for node in managed_nodes
|
||||
if str(node.get("status") or "").lower() not in {"online", "busy"}
|
||||
or str(node.get("stack_status") or "").lower() not in {"healthy", "ready", ""}
|
||||
]
|
||||
|
||||
activity_items = ensure_list(activity.get("items"))
|
||||
recent_activity = [
|
||||
{
|
||||
"kind": str(item.get("kind") or ""),
|
||||
"title": str(item.get("title") or ""),
|
||||
"level": str(item.get("level") or ""),
|
||||
"created_at": str(item.get("created_at") or ""),
|
||||
}
|
||||
for item in activity_items[:5]
|
||||
]
|
||||
|
||||
job_items = ensure_list(jobs.get("items"))
|
||||
running_jobs = [
|
||||
{
|
||||
"job_key": str(item.get("job_key") or ""),
|
||||
"status": str(item.get("status") or ""),
|
||||
"title": str(item.get("title") or ""),
|
||||
"progress_text": str(item.get("progress_text") or ""),
|
||||
}
|
||||
for item in job_items
|
||||
if str(item.get("status") or "").lower() in {"running", "queued", "waiting"}
|
||||
][:5]
|
||||
|
||||
recommendation_focus_ref = ensure_dict(recommendation.get("focus_ref"))
|
||||
first_attention = attention_nodes[0] if attention_nodes else {}
|
||||
first_attention_node_code = ensure_str(first_attention.get("node_code"))
|
||||
scene_log_target_node_code = ensure_str(scene_log.get("target_node_code"))
|
||||
latest_release_version = str(latest_release_entry.get("version") or latest_release_entry.get("release_version") or "")
|
||||
release_focus_ref = ensure_dict(launchpad_rec.get("focus_ref"))
|
||||
release_id = str(release_focus_ref.get("release_id") or "")
|
||||
operator_decision = build_operator_decision()
|
||||
primary_command_key = ensure_str(operator_decision.get("primary_command_key"))
|
||||
secondary_command_key = ensure_str(operator_decision.get("secondary_command_key"))
|
||||
|
||||
result = {
|
||||
"overview": {
|
||||
"top_recommendation": {
|
||||
"source": str(recommendation.get("source") or ""),
|
||||
"key": str(recommendation.get("key") or ""),
|
||||
"priority": str(recommendation.get("priority") or ""),
|
||||
"summary": str(recommendation.get("summary") or ""),
|
||||
"primary_action_code": str(recommendation.get("primary_action_code") or ""),
|
||||
"secondary_action_code": str(recommendation.get("secondary_action_code") or ""),
|
||||
"focus_ref": recommendation.get("focus_ref") or {},
|
||||
"node_codes": list(recommendation.get("node_codes") or []),
|
||||
},
|
||||
"scene_log_observation": {
|
||||
"status": str(scene_log.get("status") or ""),
|
||||
"status_label": str(scene_log.get("status_label") or ""),
|
||||
"summary": str(scene_log.get("summary") or ""),
|
||||
"target_node_code": str(scene_log.get("target_node_code") or ""),
|
||||
"participating_node_count": int(scene_log.get("participating_node_count") or 0),
|
||||
"covered_participating_node_count": int(scene_log.get("covered_participating_node_count") or 0),
|
||||
"line_count": int(scene_log.get("line_count") or 0),
|
||||
"focus_ref": scene_log.get("focus_ref") or {},
|
||||
},
|
||||
},
|
||||
"nodes": {
|
||||
"managed_total": int(node_summary.get("managed_total") or len(managed_nodes)),
|
||||
"online_or_busy_total": len(online_nodes),
|
||||
"offline_total": int(node_summary.get("offline_count") or 0),
|
||||
"attention_total": len(attention_nodes),
|
||||
"attention_nodes": attention_nodes[:8],
|
||||
},
|
||||
"jobs": {
|
||||
"running_total": int(jobs_summary.get("running_count") or 0),
|
||||
"queued_total": int(jobs_summary.get("queued_count") or 0),
|
||||
"failed_total": int(jobs_summary.get("failed_count") or 0),
|
||||
"recent_running_jobs": running_jobs,
|
||||
},
|
||||
"activity_stream": {
|
||||
"total": int(activity_summary.get("total") or len(activity_items)),
|
||||
"error_count": int(activity_summary.get("error_count") or 0),
|
||||
"warning_count": int(activity_summary.get("warning_count") or 0),
|
||||
"running_job_count": int(activity_summary.get("running_job_count") or 0),
|
||||
"recent_items": recent_activity,
|
||||
},
|
||||
"operator_decision": {
|
||||
"lane": ensure_str(operator_decision.get("lane")),
|
||||
"priority": ensure_str(operator_decision.get("priority")),
|
||||
"reason_code": ensure_str(operator_decision.get("reason_code")),
|
||||
"title": ensure_str(operator_decision.get("title")),
|
||||
"summary": ensure_str(operator_decision.get("summary")),
|
||||
"next_focus": ensure_dict(operator_decision.get("next_focus")),
|
||||
"primary_command_key": primary_command_key,
|
||||
"secondary_command_key": secondary_command_key,
|
||||
},
|
||||
"recommended_commands": {
|
||||
"stack_diagnosis": "bash domain-api/deploy/multi-region/check_ops_center_stack.sh",
|
||||
"driver_feed": command("driver-feed"),
|
||||
"activity_preview": command("activity-preview"),
|
||||
"driver_focus_preview": command("driver-focus-preview"),
|
||||
"driver_focus_run": command("driver-focus-run"),
|
||||
"top_recommendation_preview": (
|
||||
command("driver-preview", str(recommendation.get("primary_action_code") or ""))
|
||||
if str(recommendation.get("primary_action_code") or "").strip()
|
||||
else ""
|
||||
),
|
||||
"top_recommendation_focus_preview": (
|
||||
command("driver-focus-preview", f"entry:{str(recommendation.get('key') or '')}")
|
||||
if str(recommendation.get("key") or "").strip()
|
||||
else ""
|
||||
),
|
||||
"scene_log": (
|
||||
command("scene-node-log", scene_log_target_node_code, "120", "key")
|
||||
if scene_log_target_node_code
|
||||
else ""
|
||||
),
|
||||
"scene_log_direct": (
|
||||
f"bash domain-api/deploy/multi-region/check_node_scene_log.sh {base_url} {scene_log_target_node_code} 120 key"
|
||||
if scene_log_target_node_code
|
||||
else ""
|
||||
),
|
||||
"node_handover": (
|
||||
command("node-handover", first_attention_node_code)
|
||||
if first_attention_node_code
|
||||
else command("stack-first-gap-handover")
|
||||
),
|
||||
"node_bootstrap_plan": (
|
||||
command("node-bootstrap-plan", first_attention_node_code)
|
||||
if first_attention_node_code
|
||||
else ""
|
||||
),
|
||||
"delivery_queue": (
|
||||
command("delivery-queue", first_attention_node_code)
|
||||
if first_attention_node_code
|
||||
else ""
|
||||
),
|
||||
"release_launchpad": release_command("launchpad"),
|
||||
"release_preview_latest": release_command("preview-latest"),
|
||||
"release_preview_latest_worker": release_command("preview-latest-smart", "worker"),
|
||||
"release_preview_latest_control": release_command("preview-latest-smart", "control"),
|
||||
"release_focus": (
|
||||
command("driver-preview", "focus_release_hub")
|
||||
if str(launchpad_rec.get("action_code") or "").strip()
|
||||
else ""
|
||||
),
|
||||
"release_preview_selected": (
|
||||
release_command("preview-release", release_id, "worker")
|
||||
if release_id
|
||||
else ""
|
||||
),
|
||||
"jobs": command("activity-stream", "kind=ops_job"),
|
||||
},
|
||||
"next_actions": {
|
||||
"primary_command": "",
|
||||
"secondary_command": "",
|
||||
},
|
||||
"release_launchpad": {
|
||||
"status": str(launchpad_status.get("status") or ""),
|
||||
"status_label": str(launchpad_status.get("status_label") or ""),
|
||||
"summary": str(launchpad_status.get("summary") or ""),
|
||||
"recommended_action_code": str(launchpad_rec.get("action_code") or ""),
|
||||
"recommended_action_label": str(launchpad_rec.get("action_label") or ""),
|
||||
"recommended_execution_mode": str(launchpad_rec.get("execution_mode") or ""),
|
||||
"recommended_execution_mode_label": str(launchpad_rec.get("execution_mode_label") or ""),
|
||||
"focus_ref": launchpad_rec.get("focus_ref") or {},
|
||||
"release_version": str(launchpad_release.get("version") or launchpad_release.get("release_version") or ""),
|
||||
"worker_rollout_preview": {
|
||||
"available": bool(launchpad_workers.get("available")),
|
||||
"blocked": bool(launchpad_workers.get("blocked")),
|
||||
"execution_mode": str(launchpad_workers.get("execution_mode") or ""),
|
||||
"execution_mode_label": str(launchpad_workers.get("execution_mode_label") or ""),
|
||||
"target_node_codes": list(launchpad_workers.get("target_node_codes") or []),
|
||||
},
|
||||
"control_rollout_preview": {
|
||||
"available": bool(launchpad_controls.get("available")),
|
||||
"blocked": bool(launchpad_controls.get("blocked")),
|
||||
"execution_mode": str(launchpad_controls.get("execution_mode") or ""),
|
||||
"execution_mode_label": str(launchpad_controls.get("execution_mode_label") or ""),
|
||||
"target_node_codes": list(launchpad_controls.get("target_node_codes") or []),
|
||||
},
|
||||
},
|
||||
"latest_release": {
|
||||
"version": str(latest_release_entry.get("version") or latest_release_entry.get("release_version") or ""),
|
||||
"status": ensure_str(latest_release_entry.get("status")),
|
||||
"status_label": ensure_str(latest_release_entry.get("status_label")),
|
||||
"summary": ensure_str(latest_release_entry.get("summary") or latest_release_entry.get("summary_text")),
|
||||
},
|
||||
"policy_preview_sample": {
|
||||
"execution_mode": str(policy_preview.get("execution_mode") or ""),
|
||||
"risk_level": str(policy_preview.get("risk_level") or ""),
|
||||
"approval_required": bool(policy_preview.get("approval_required")),
|
||||
"blocked": bool(policy_preview.get("blocked")),
|
||||
"blocking_reasons": ensure_list(policy_preview.get("blocking_reasons"))[:5],
|
||||
"warnings": ensure_list(policy_preview.get("warnings"))[:5],
|
||||
},
|
||||
"focus_refs": {
|
||||
"top_recommendation": recommendation_focus_ref,
|
||||
"scene_log": ensure_dict(scene_log.get("focus_ref")),
|
||||
"release_hub": release_focus_ref,
|
||||
},
|
||||
"rollout_targets": {
|
||||
"worker": list(launchpad_workers.get("target_node_codes") or []),
|
||||
"control": list(launchpad_controls.get("target_node_codes") or []),
|
||||
},
|
||||
"package_preview": {
|
||||
"available": bool(latest_package_preview.get("release")),
|
||||
"preview_source": ensure_str(latest_package_preview.get("preview_source")),
|
||||
"message": ensure_str(latest_package_preview.get("message")),
|
||||
},
|
||||
}
|
||||
|
||||
result["next_actions"]["primary_command"] = ensure_str(
|
||||
ensure_dict(result.get("recommended_commands")).get(primary_command_key)
|
||||
)
|
||||
result["next_actions"]["secondary_command"] = ensure_str(
|
||||
ensure_dict(result.get("recommended_commands")).get(secondary_command_key)
|
||||
)
|
||||
|
||||
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||
PY
|
||||
echo
|
||||
373
domain-api/deploy/multi-region/check_release_hub.sh
Normal file
373
domain-api/deploy/multi-region/check_release_hub.sh
Normal file
@@ -0,0 +1,373 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
BASE_URL="${1:-http://127.0.0.1:8100}"
|
||||
INPUT_RELEASE_ID="${2:-}"
|
||||
CURL_CONNECT_TIMEOUT="${CURL_CONNECT_TIMEOUT:-3}"
|
||||
CURL_MAX_TIME="${CURL_MAX_TIME:-10}"
|
||||
|
||||
curl_json() {
|
||||
curl -fsS \
|
||||
--connect-timeout "${CURL_CONNECT_TIMEOUT}" \
|
||||
--max-time "${CURL_MAX_TIME}" \
|
||||
"$1"
|
||||
}
|
||||
|
||||
curl_post_json() {
|
||||
local url="$1"
|
||||
local body="$2"
|
||||
curl -fsS \
|
||||
--connect-timeout "${CURL_CONNECT_TIMEOUT}" \
|
||||
--max-time "${CURL_MAX_TIME}" \
|
||||
-X POST "${url}" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d "${body}"
|
||||
}
|
||||
|
||||
launchpad="$(curl_json "${BASE_URL}/api/v1/ops/releases/launchpad")"
|
||||
latest_release="$(curl_json "${BASE_URL}/api/v1/ops/releases/latest")"
|
||||
action_templates="$(curl_json "${BASE_URL}/api/v1/ops/action-templates")"
|
||||
cluster_json="$(curl_json "${BASE_URL}/api/v1/runtime/cluster")"
|
||||
release_id="${INPUT_RELEASE_ID}"
|
||||
|
||||
if [[ -z "${release_id}" ]]; then
|
||||
release_id="$(python3 -c '
|
||||
import json
|
||||
import sys
|
||||
|
||||
payload = json.loads(sys.argv[1])
|
||||
data = payload.get("data") or {}
|
||||
value = data.get("id") or 0
|
||||
print(value)
|
||||
' "${latest_release}")"
|
||||
fi
|
||||
|
||||
selected_release="${latest_release}"
|
||||
if [[ -n "${release_id}" && "${release_id}" != "0" ]]; then
|
||||
selected_release="$(curl_json "${BASE_URL}/api/v1/ops/releases/${release_id}")"
|
||||
fi
|
||||
|
||||
build_release_preview_payload() {
|
||||
local preview_mode="$1"
|
||||
python3 - <<'PY' "${selected_release}" "${cluster_json}" "${preview_mode}"
|
||||
import json
|
||||
import sys
|
||||
|
||||
release_payload = json.loads(sys.argv[1]).get("data") or {}
|
||||
cluster_payload = json.loads(sys.argv[2]).get("data") or {}
|
||||
preview_mode = str(sys.argv[3] or "").strip().lower()
|
||||
|
||||
nodes = list(cluster_payload.get("nodes") or [])
|
||||
|
||||
def is_online(node):
|
||||
return str(node.get("status") or "").strip() in {"online", "busy"}
|
||||
|
||||
control_nodes = [
|
||||
str(node.get("node_code") or "").strip()
|
||||
for node in nodes
|
||||
if is_online(node) and str(node.get("role") or "").strip() == "control"
|
||||
]
|
||||
worker_nodes = [
|
||||
str(node.get("node_code") or "").strip()
|
||||
for node in nodes
|
||||
if is_online(node)
|
||||
and str(node.get("role") or "").strip() == "worker"
|
||||
and bool(node.get("is_effective_worker", False))
|
||||
]
|
||||
online_nodes = [
|
||||
str(node.get("node_code") or "").strip()
|
||||
for node in nodes
|
||||
if is_online(node)
|
||||
]
|
||||
|
||||
payload = {
|
||||
"release_version": str(release_payload.get("release_version") or "").strip(),
|
||||
"artifact_url": str(release_payload.get("artifact_url") or "").strip(),
|
||||
"checksum": str(release_payload.get("checksum") or "").strip(),
|
||||
"install_root": "/opt/domaincheck",
|
||||
"health_check_timeout_seconds": 10,
|
||||
"health_check_retries": 2,
|
||||
"health_check_interval_seconds": 2,
|
||||
"rollback_on_failure": True,
|
||||
"switch_current": True,
|
||||
}
|
||||
|
||||
if preview_mode == "control":
|
||||
template_key = "deploy.release.control"
|
||||
target_node_codes = control_nodes
|
||||
payload.update(
|
||||
{
|
||||
"restart_services": ["domaincheck-api", "domaincheck-worker", "domaincheck-sync-agent"],
|
||||
"health_check_urls": ["http://127.0.0.1:8100/health"],
|
||||
"health_check_services": ["domaincheck-api", "domaincheck-worker", "domaincheck-sync-agent"],
|
||||
}
|
||||
)
|
||||
elif preview_mode == "worker":
|
||||
template_key = "deploy.release.worker"
|
||||
target_node_codes = worker_nodes
|
||||
payload.update(
|
||||
{
|
||||
"restart_services": ["domaincheck-worker"],
|
||||
"health_check_urls": [],
|
||||
"health_check_services": ["domaincheck-worker"],
|
||||
}
|
||||
)
|
||||
else:
|
||||
template_key = "deploy.release.custom"
|
||||
target_node_codes = (control_nodes[:1] + worker_nodes[:2]) or online_nodes[:3]
|
||||
payload.update(
|
||||
{
|
||||
"restart_services": [],
|
||||
"health_check_urls": [],
|
||||
"health_check_services": [],
|
||||
}
|
||||
)
|
||||
|
||||
request_payload = {
|
||||
"template_key": template_key,
|
||||
"action": "deploy.release",
|
||||
"target_type": "batch",
|
||||
"execution_mode": "remote-agent",
|
||||
"target_node_codes": [item for item in target_node_codes if item],
|
||||
"payload": payload,
|
||||
}
|
||||
|
||||
print(json.dumps(request_payload, ensure_ascii=False))
|
||||
PY
|
||||
}
|
||||
|
||||
print_preview_request_summary() {
|
||||
local preview_payload="$1"
|
||||
python3 - <<'PY' "${preview_payload}"
|
||||
import json
|
||||
import sys
|
||||
|
||||
payload = json.loads(sys.argv[1])
|
||||
body = payload.get("payload") or {}
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"template_key": payload.get("template_key") or "",
|
||||
"action": payload.get("action") or "",
|
||||
"execution_mode": payload.get("execution_mode") or "",
|
||||
"target_node_codes": payload.get("target_node_codes") or [],
|
||||
"release_version": body.get("release_version") or "",
|
||||
"artifact_url_present": bool(str(body.get("artifact_url") or "").strip()),
|
||||
"checksum_present": bool(str(body.get("checksum") or "").strip()),
|
||||
"restart_services": body.get("restart_services") or [],
|
||||
"health_check_urls": body.get("health_check_urls") or [],
|
||||
"health_check_services": body.get("health_check_services") or [],
|
||||
},
|
||||
ensure_ascii=False,
|
||||
indent=2,
|
||||
)
|
||||
)
|
||||
PY
|
||||
}
|
||||
|
||||
run_release_preview() {
|
||||
local step_no="$1"
|
||||
local preview_mode="$2"
|
||||
local section_title="$3"
|
||||
local preview_payload
|
||||
preview_payload="$(build_release_preview_payload "${preview_mode}")"
|
||||
|
||||
echo "[${step_no}/10] ${section_title}"
|
||||
print_preview_request_summary "${preview_payload}"
|
||||
echo
|
||||
curl_post_json "${BASE_URL}/api/v1/ops/policy/preview" "${preview_payload}"
|
||||
echo
|
||||
echo
|
||||
}
|
||||
|
||||
echo "[1/11] release launchpad"
|
||||
printf '%s\n\n' "${launchpad}"
|
||||
|
||||
echo "[2/11] latest release"
|
||||
printf '%s\n\n' "${selected_release}"
|
||||
|
||||
echo "[3/11] recent releases"
|
||||
curl_json "${BASE_URL}/api/v1/ops/releases?limit=8"
|
||||
echo
|
||||
echo
|
||||
|
||||
echo "[4/11] release deploy templates"
|
||||
python3 - <<'PY' "${action_templates}"
|
||||
import json
|
||||
import sys
|
||||
|
||||
payload = json.loads(sys.argv[1])
|
||||
groups = (payload.get("data") or {}).get("groups") or []
|
||||
release_group = next((item for item in groups if str(item.get("group_key") or "") == "release"), {})
|
||||
items = list(release_group.get("items") or [])
|
||||
summary = {
|
||||
"group_key": release_group.get("group_key") or "",
|
||||
"group_title": release_group.get("group_title") or "",
|
||||
"template_keys": [str(item.get("key") or "") for item in items],
|
||||
"field_types": sorted(
|
||||
{
|
||||
str(field.get("type") or "")
|
||||
for item in items
|
||||
for field in list(item.get("fields") or [])
|
||||
if str(field.get("type") or "")
|
||||
}
|
||||
),
|
||||
}
|
||||
print(json.dumps(summary, ensure_ascii=False, indent=2))
|
||||
PY
|
||||
echo
|
||||
|
||||
echo "[5/11] online release target summary"
|
||||
python3 - <<'PY' "${cluster_json}"
|
||||
import json
|
||||
import sys
|
||||
|
||||
payload = json.loads(sys.argv[1]).get("data") or {}
|
||||
nodes = list(payload.get("nodes") or [])
|
||||
|
||||
def is_online(node):
|
||||
return str(node.get("status") or "").strip() in {"online", "busy"}
|
||||
|
||||
summary = {
|
||||
"online_control_nodes": [
|
||||
str(node.get("node_code") or "").strip()
|
||||
for node in nodes
|
||||
if is_online(node) and str(node.get("role") or "").strip() == "control"
|
||||
],
|
||||
"online_worker_nodes": [
|
||||
str(node.get("node_code") or "").strip()
|
||||
for node in nodes
|
||||
if is_online(node) and str(node.get("role") or "").strip() == "worker" and bool(node.get("is_effective_worker", False))
|
||||
],
|
||||
"all_online_nodes": [
|
||||
{
|
||||
"node_code": str(node.get("node_code") or "").strip(),
|
||||
"role": str(node.get("role") or "").strip(),
|
||||
"status": str(node.get("status") or "").strip(),
|
||||
"is_effective_worker": bool(node.get("is_effective_worker", False)),
|
||||
}
|
||||
for node in nodes
|
||||
if is_online(node)
|
||||
],
|
||||
}
|
||||
print(json.dumps(summary, ensure_ascii=False, indent=2))
|
||||
PY
|
||||
echo
|
||||
echo
|
||||
|
||||
run_release_preview "6" "control" "deploy.release.control preview"
|
||||
run_release_preview "7" "worker" "deploy.release.worker preview"
|
||||
run_release_preview "8" "custom" "deploy.release.custom preview"
|
||||
|
||||
rollout_list='{"code":0,"message":"no release selected","data":{"rollouts":[]}}'
|
||||
rollout_id="0"
|
||||
if [[ -n "${release_id}" && "${release_id}" != "0" ]]; then
|
||||
rollout_list="$(curl_json "${BASE_URL}/api/v1/ops/releases/${release_id}/rollouts?limit=10")"
|
||||
rollout_id="$(python3 -c '
|
||||
import json
|
||||
import sys
|
||||
|
||||
payload = json.loads(sys.argv[1])
|
||||
rollouts = (payload.get("data") or {}).get("rollouts") or []
|
||||
print((rollouts[0] or {}).get("id") or 0 if rollouts else 0)
|
||||
' "${rollout_list}")"
|
||||
fi
|
||||
|
||||
echo "[9/11] rollout list for release ${release_id:-0}"
|
||||
printf '%s\n\n' "${rollout_list}"
|
||||
|
||||
echo "[10/11] primary rollout detail"
|
||||
if [[ "${rollout_id}" != "0" ]]; then
|
||||
curl_json "${BASE_URL}/api/v1/ops/rollouts/${rollout_id}"
|
||||
else
|
||||
echo '{"code":0,"message":"no rollout","data":{}}'
|
||||
fi
|
||||
echo
|
||||
echo
|
||||
|
||||
echo "[11/12] primary rollout jobs"
|
||||
if [[ "${rollout_id}" != "0" ]]; then
|
||||
curl_json "${BASE_URL}/api/v1/ops/rollouts/${rollout_id}/jobs?limit=50"
|
||||
else
|
||||
echo '{"code":0,"message":"no rollout","data":{"jobs":[]}}'
|
||||
fi
|
||||
echo
|
||||
|
||||
echo
|
||||
echo "[12/12] condensed summary"
|
||||
python3 - <<'PY' "${launchpad}" "${selected_release}" "${cluster_json}" "${rollout_list}" "${rollout_id}"
|
||||
import json
|
||||
import sys
|
||||
|
||||
launchpad_payload = json.loads(sys.argv[1] or "{}").get("data") or {}
|
||||
selected_release = json.loads(sys.argv[2] or "{}").get("data") or {}
|
||||
cluster_payload = json.loads(sys.argv[3] or "{}").get("data") or {}
|
||||
rollout_list_payload = json.loads(sys.argv[4] or "{}").get("data") or {}
|
||||
rollout_id = int(sys.argv[5] or 0)
|
||||
|
||||
launchpad_status = dict(launchpad_payload.get("launchpad_status") or {})
|
||||
worker_preview = dict(launchpad_payload.get("worker_rollout_preview") or {})
|
||||
worker_smart_rollout = dict(worker_preview.get("smart_rollout") or {})
|
||||
control_preview = dict(launchpad_payload.get("control_rollout_preview") or {})
|
||||
control_smart_rollout = dict(control_preview.get("smart_rollout") or {})
|
||||
nodes = list(cluster_payload.get("nodes") or [])
|
||||
rollouts = list(rollout_list_payload.get("rollouts") or [])
|
||||
|
||||
def is_online(node):
|
||||
return str(node.get("status") or "").strip() in {"online", "busy"}
|
||||
|
||||
summary = {
|
||||
"release": {
|
||||
"id": int(selected_release.get("id") or 0),
|
||||
"release_version": str(selected_release.get("release_version") or ""),
|
||||
"channel": str(selected_release.get("channel") or ""),
|
||||
"status": str(selected_release.get("status") or ""),
|
||||
},
|
||||
"launchpad": {
|
||||
"status": str(launchpad_status.get("status") or ""),
|
||||
"status_label": str(launchpad_status.get("status_label") or ""),
|
||||
"summary": str(launchpad_status.get("summary") or ""),
|
||||
"recommended_action_code": str(launchpad_status.get("recommended_action_code") or ""),
|
||||
"recommended_target_node_code": str(launchpad_status.get("recommended_target_node_code") or ""),
|
||||
"recommended_recovery_label": str(launchpad_status.get("recommended_recovery_label") or ""),
|
||||
"recommended_recovery_summary": str(launchpad_status.get("recommended_recovery_summary") or ""),
|
||||
"onboarding_bootstrap_pending_nodes": int(launchpad_status.get("onboarding_bootstrap_pending_nodes", 0) or 0),
|
||||
"onboarding_acceptance_ready_nodes": int(launchpad_status.get("onboarding_acceptance_ready_nodes", 0) or 0),
|
||||
"recommended_execution_mode": str(launchpad_status.get("recommended_execution_mode") or ""),
|
||||
"recommended_execution_mode_label": str(launchpad_status.get("recommended_execution_mode_label") or ""),
|
||||
"focus_ref": dict(launchpad_status.get("focus_ref") or {}),
|
||||
},
|
||||
"worker_rollout_preview": {
|
||||
"available": bool(worker_smart_rollout.get("available", False)),
|
||||
"blocked": bool(worker_preview.get("blocked", False)),
|
||||
"requires_confirmation": bool(worker_preview.get("requires_confirmation", False)),
|
||||
"execution_mode": str(worker_smart_rollout.get("execution_mode") or ""),
|
||||
"target_node_codes": list(worker_smart_rollout.get("target_node_codes") or []),
|
||||
},
|
||||
"control_rollout_preview": {
|
||||
"available": bool(control_smart_rollout.get("available", False)),
|
||||
"blocked": bool(control_preview.get("blocked", False)),
|
||||
"requires_confirmation": bool(control_preview.get("requires_confirmation", False)),
|
||||
"execution_mode": str(control_smart_rollout.get("execution_mode") or ""),
|
||||
"target_node_codes": list(control_smart_rollout.get("target_node_codes") or []),
|
||||
},
|
||||
"cluster": {
|
||||
"online_control_nodes": [
|
||||
str(node.get("node_code") or "").strip()
|
||||
for node in nodes
|
||||
if is_online(node) and str(node.get("role") or "").strip() == "control"
|
||||
],
|
||||
"online_worker_nodes": [
|
||||
str(node.get("node_code") or "").strip()
|
||||
for node in nodes
|
||||
if is_online(node) and str(node.get("role") or "").strip() == "worker" and bool(node.get("is_effective_worker", False))
|
||||
],
|
||||
},
|
||||
"rollouts": {
|
||||
"count": len(rollouts),
|
||||
"selected_rollout_id": rollout_id,
|
||||
},
|
||||
}
|
||||
print(json.dumps(summary, ensure_ascii=False, indent=2))
|
||||
PY
|
||||
echo
|
||||
@@ -4,15 +4,24 @@ set -euo pipefail
|
||||
MAINLAND_BASE_URL="${1:-http://127.0.0.1:8100}"
|
||||
OVERSEAS_BASE_URL="${2:-http://152.53.37.118:8100}"
|
||||
PYTHON_BIN="${PYTHON_BIN:-python3}"
|
||||
CURL_CONNECT_TIMEOUT="${CURL_CONNECT_TIMEOUT:-3}"
|
||||
CURL_MAX_TIME="${CURL_MAX_TIME:-10}"
|
||||
|
||||
curl_json() {
|
||||
curl -fsS \
|
||||
--connect-timeout "${CURL_CONNECT_TIMEOUT}" \
|
||||
--max-time "${CURL_MAX_TIME}" \
|
||||
"$1"
|
||||
}
|
||||
|
||||
echo "[1/5] mainland runtime cluster"
|
||||
MAINLAND_CLUSTER="$(curl -fsS "${MAINLAND_BASE_URL}/api/v1/runtime/cluster")"
|
||||
MAINLAND_CLUSTER="$(curl_json "${MAINLAND_BASE_URL}/api/v1/runtime/cluster")"
|
||||
echo "${MAINLAND_CLUSTER}"
|
||||
echo
|
||||
echo
|
||||
|
||||
echo "[2/5] mainland runtime readiness"
|
||||
MAINLAND_READINESS="$(curl -fsS "${MAINLAND_BASE_URL}/api/v1/runtime/readiness")"
|
||||
MAINLAND_READINESS="$(curl_json "${MAINLAND_BASE_URL}/api/v1/runtime/readiness")"
|
||||
echo "${MAINLAND_READINESS}"
|
||||
echo
|
||||
echo
|
||||
@@ -79,7 +88,7 @@ echo
|
||||
echo
|
||||
|
||||
echo "[4/5] overseas runtime cluster"
|
||||
OVERSEAS_CLUSTER="$(curl -fsS "${OVERSEAS_BASE_URL}/api/v1/runtime/cluster")"
|
||||
OVERSEAS_CLUSTER="$(curl_json "${OVERSEAS_BASE_URL}/api/v1/runtime/cluster")"
|
||||
echo "${OVERSEAS_CLUSTER}"
|
||||
echo
|
||||
echo
|
||||
|
||||
@@ -4,23 +4,44 @@ set -euo pipefail
|
||||
DB_NAME="${DB_NAME:-domain}"
|
||||
DB_USER="${DB_USER:-postgres}"
|
||||
PSQL_BIN="${PSQL_BIN:-/www/server/pgsql/bin/psql}"
|
||||
API_BASE_URL="${1:-${API_BASE_URL:-http://127.0.0.1:8100}}"
|
||||
CURL_CONNECT_TIMEOUT="${CURL_CONNECT_TIMEOUT:-3}"
|
||||
CURL_MAX_TIME="${CURL_MAX_TIME:-10}"
|
||||
|
||||
echo "[1/3] cluster nodes"
|
||||
curl_json() {
|
||||
curl -fsS \
|
||||
--connect-timeout "${CURL_CONNECT_TIMEOUT}" \
|
||||
--max-time "${CURL_MAX_TIME}" \
|
||||
"$1"
|
||||
}
|
||||
|
||||
RUNTIME_STATUS_JSON="$(curl_json "${API_BASE_URL}/api/v1/runtime/status")"
|
||||
|
||||
echo "[1/4] cluster nodes"
|
||||
"${PSQL_BIN}" -U "${DB_USER}" -d "${DB_NAME}" -Atc "
|
||||
select
|
||||
node_code || '|' ||
|
||||
role || '|' ||
|
||||
status || '|' ||
|
||||
coalesce(current_load, 0) || '|' ||
|
||||
coalesce(metadata_json->>'job_items_claimed', '0') || '|' ||
|
||||
coalesce(metadata_json->>'job_items_running', '0') || '|' ||
|
||||
coalesce(metadata_json->>'job_items_completed', '0') || '|' ||
|
||||
to_char(last_heartbeat_at, 'YYYY-MM-DD HH24:MI:SS') || '|' ||
|
||||
coalesce(metadata_json->>'worker_online', '') || '|' ||
|
||||
coalesce(metadata_json->>'detect_participating', '')
|
||||
(
|
||||
case
|
||||
when coalesce(current_load, 0) > 0 then 'true'
|
||||
when coalesce(metadata_json->>'detect_participating', '') in ('true', 'false') then metadata_json->>'detect_participating'
|
||||
else ''
|
||||
end
|
||||
)
|
||||
from detect_worker_nodes
|
||||
order by node_code;
|
||||
"
|
||||
echo
|
||||
|
||||
echo "[2/3] active job distribution"
|
||||
echo "[2/4] active job distribution"
|
||||
"${PSQL_BIN}" -U "${DB_USER}" -d "${DB_NAME}" -Atc "
|
||||
select
|
||||
coalesce(claimed_by, '') || '|' ||
|
||||
@@ -39,59 +60,43 @@ order by claimed_by, status;
|
||||
"
|
||||
echo
|
||||
|
||||
echo "[3/3] condensed summary"
|
||||
"${PSQL_BIN}" -U "${DB_USER}" -d "${DB_NAME}" -At <<'SQL'
|
||||
with latest_job as (
|
||||
select id, job_code, status
|
||||
from detect_jobs
|
||||
where status in ('pending','running')
|
||||
order by id desc
|
||||
limit 1
|
||||
),
|
||||
node_stats as (
|
||||
select
|
||||
node_code,
|
||||
role,
|
||||
status,
|
||||
coalesce(metadata_json->>'worker_online', '') as worker_online,
|
||||
coalesce(metadata_json->>'detect_participating', '') as detect_participating
|
||||
from detect_worker_nodes
|
||||
),
|
||||
item_stats as (
|
||||
select
|
||||
coalesce(claimed_by, '') as claimed_by,
|
||||
status,
|
||||
count(*) as cnt
|
||||
from detect_job_items
|
||||
where job_id = (select id from latest_job)
|
||||
group by claimed_by, status
|
||||
)
|
||||
select json_build_object(
|
||||
'job', (
|
||||
select json_build_object(
|
||||
'job_code', coalesce(job_code, ''),
|
||||
'status', coalesce(status, '')
|
||||
)
|
||||
from latest_job
|
||||
),
|
||||
'online_nodes', (
|
||||
select coalesce(json_agg(json_build_object(
|
||||
'node_code', node_code,
|
||||
'role', role,
|
||||
'status', status,
|
||||
'worker_online', worker_online,
|
||||
'detect_participating', detect_participating
|
||||
) order by node_code), '[]'::json)
|
||||
from node_stats
|
||||
where status in ('online', 'busy')
|
||||
),
|
||||
'claimed_distribution', (
|
||||
select coalesce(json_agg(json_build_object(
|
||||
'claimed_by', claimed_by,
|
||||
'status', status,
|
||||
'count', cnt
|
||||
) order by claimed_by, status), '[]'::json)
|
||||
from item_stats
|
||||
)
|
||||
);
|
||||
SQL
|
||||
echo "[3/4] runtime participation snapshot"
|
||||
printf "%s" "${RUNTIME_STATUS_JSON}" | python3 -c '
|
||||
import json, sys
|
||||
payload = json.load(sys.stdin).get("data", {})
|
||||
detect = payload.get("detect", {})
|
||||
print(json.dumps({
|
||||
"node": payload.get("node", {}),
|
||||
"participation_summary": detect.get("participation_summary", {}),
|
||||
"participating_nodes": detect.get("participating_nodes", []),
|
||||
"non_participating_nodes": detect.get("non_participating_nodes", detect.get("standby_nodes", [])),
|
||||
"log_sync": detect.get("log_sync", {}),
|
||||
}, ensure_ascii=False, indent=2))
|
||||
'
|
||||
echo
|
||||
|
||||
echo "[4/4] condensed summary"
|
||||
printf "%s" "${RUNTIME_STATUS_JSON}" | python3 -c '
|
||||
import json, sys
|
||||
payload = json.load(sys.stdin).get("data", {})
|
||||
detect = payload.get("detect", {})
|
||||
summary = detect.get("participation_summary", {}) or {}
|
||||
print(json.dumps({
|
||||
"node": payload.get("node", {}),
|
||||
"active_job": {
|
||||
"job_code": (detect.get("active_job") or {}).get("job_code", ""),
|
||||
"status": (detect.get("active_job") or {}).get("status", ""),
|
||||
},
|
||||
"participation_summary": summary,
|
||||
"dispatch_active_nodes": summary.get("dispatch_active_node_codes", []),
|
||||
"recent_only_nodes": summary.get("recent_only_node_codes", []),
|
||||
"non_participating_nodes": summary.get("non_participating_node_codes", []),
|
||||
"log_sync": {
|
||||
"enabled": bool((detect.get("log_sync") or {}).get("enabled", False)),
|
||||
"mode": (detect.get("log_sync") or {}).get("mode", ""),
|
||||
"line_count": (detect.get("log_sync") or {}).get("line_count", 0),
|
||||
"source_nodes": (detect.get("log_sync") or {}).get("source_nodes", []),
|
||||
"last_at": (detect.get("log_sync") or {}).get("last_at", ""),
|
||||
},
|
||||
}, ensure_ascii=False, indent=2))
|
||||
'
|
||||
|
||||
447
domain-api/deploy/multi-region/drive_ops_action.sh
Executable file
447
domain-api/deploy/multi-region/drive_ops_action.sh
Executable file
@@ -0,0 +1,447 @@
|
||||
#!/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 <base_url> <preview|execute> <action_code> [action_payload_json|@payload.json] [requested_by]
|
||||
bash domain-api/deploy/multi-region/drive_ops_action.sh <base_url> <action_code> [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
|
||||
7776
domain-api/deploy/multi-region/drive_ops_center.sh
Executable file
7776
domain-api/deploy/multi-region/drive_ops_center.sh
Executable file
File diff suppressed because it is too large
Load Diff
476
domain-api/deploy/multi-region/drive_release_hub.sh
Executable file
476
domain-api/deploy/multi-region/drive_release_hub.sh
Executable file
@@ -0,0 +1,476 @@
|
||||
#!/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_release_hub.sh <base_url> <command> [args...]
|
||||
|
||||
commands:
|
||||
launchpad
|
||||
latest-package
|
||||
latest-release
|
||||
preview-latest
|
||||
preview-latest-smart <worker|control>
|
||||
preview-release <release_id> <worker|control>
|
||||
create-latest-release
|
||||
list-releases [limit]
|
||||
list-rollouts <release_id> [limit]
|
||||
smart-latest <worker|control> [confirm]
|
||||
smart-release <release_id> <worker|control> [confirm]
|
||||
|
||||
examples:
|
||||
bash domain-api/deploy/multi-region/drive_release_hub.sh http://127.0.0.1:8100 launchpad
|
||||
bash domain-api/deploy/multi-region/drive_release_hub.sh http://127.0.0.1:8100 latest-package
|
||||
bash domain-api/deploy/multi-region/drive_release_hub.sh http://127.0.0.1:8100 preview-latest
|
||||
bash domain-api/deploy/multi-region/drive_release_hub.sh http://127.0.0.1:8100 preview-latest-smart worker
|
||||
bash domain-api/deploy/multi-region/drive_release_hub.sh http://127.0.0.1:8100 create-latest-release
|
||||
bash domain-api/deploy/multi-region/drive_release_hub.sh http://127.0.0.1:8100 smart-latest worker
|
||||
bash domain-api/deploy/multi-region/drive_release_hub.sh http://127.0.0.1:8100 preview-release 18 control
|
||||
bash domain-api/deploy/multi-region/drive_release_hub.sh http://127.0.0.1:8100 smart-release 18 control confirm
|
||||
EOF
|
||||
}
|
||||
|
||||
BASE_URL="${1:-$(ops_center_resolve_mainland_api_base_url)}"
|
||||
COMMAND="${2:-}"
|
||||
ARG1="${3:-}"
|
||||
ARG2="${4:-}"
|
||||
ARG3="${5:-}"
|
||||
REQUESTED_BY="${REQUESTED_BY:-$(ops_center_resolve_requested_by)}"
|
||||
|
||||
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 "${COMMAND}" ]]; then
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
METHOD="GET"
|
||||
PATH_SUFFIX=""
|
||||
REQUEST_JSON=""
|
||||
|
||||
confirm_to_bool() {
|
||||
local raw_value="${1:-}"
|
||||
case "$(printf '%s' "${raw_value}" | tr '[:upper:]' '[:lower:]')" in
|
||||
1|true|yes|y|confirm|confirmed)
|
||||
printf 'true'
|
||||
;;
|
||||
*)
|
||||
printf 'false'
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
case "${COMMAND}" in
|
||||
launchpad)
|
||||
CHANNEL="${ARG1:-stable}"
|
||||
PATH_SUFFIX="/api/v1/ops/releases/launchpad?channel=${CHANNEL}"
|
||||
;;
|
||||
latest-package)
|
||||
PATH_SUFFIX="/api/v1/ops/releases/package-metadata/latest"
|
||||
;;
|
||||
latest-release)
|
||||
PATH_SUFFIX="/api/v1/ops/releases/latest"
|
||||
;;
|
||||
preview-latest)
|
||||
METHOD="POST"
|
||||
PATH_SUFFIX="/api/v1/ops/releases/from-package/latest/preview"
|
||||
REQUEST_JSON="$(python3 - <<'PY' "${REQUESTED_BY}"
|
||||
import json
|
||||
import sys
|
||||
|
||||
requested_by = str(sys.argv[1] or "").strip() or "cli/drive_release_hub"
|
||||
payload = {
|
||||
"created_by": f"{requested_by}/latest-package-preview",
|
||||
}
|
||||
print(json.dumps(payload, ensure_ascii=False))
|
||||
PY
|
||||
)"
|
||||
;;
|
||||
preview-latest-smart)
|
||||
MODE="${ARG1:-}"
|
||||
if [[ "${MODE}" != "worker" && "${MODE}" != "control" ]]; then
|
||||
echo "mode must be worker or control" >&2
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
METHOD="POST"
|
||||
PATH_SUFFIX="/api/v1/ops/releases/from-package/latest/smart-rollout-preview"
|
||||
REQUEST_JSON="$(python3 - <<'PY' "${MODE}" "${REQUESTED_BY}"
|
||||
import json
|
||||
import sys
|
||||
|
||||
mode = str(sys.argv[1] or "").strip()
|
||||
requested_by = str(sys.argv[2] or "").strip() or "cli/drive_release_hub"
|
||||
|
||||
payload = {
|
||||
"mode": mode,
|
||||
"created_by": f"{requested_by}/latest-package-preview",
|
||||
"rollout_created_by": f"{requested_by}/latest-package-preview/rollout",
|
||||
}
|
||||
print(json.dumps(payload, ensure_ascii=False))
|
||||
PY
|
||||
)"
|
||||
;;
|
||||
create-latest-release)
|
||||
METHOD="POST"
|
||||
PATH_SUFFIX="/api/v1/ops/releases/from-package/latest"
|
||||
REQUEST_JSON="$(python3 - <<'PY' "${REQUESTED_BY}"
|
||||
import json
|
||||
import sys
|
||||
|
||||
requested_by = str(sys.argv[1] or "").strip() or "cli/drive_release_hub"
|
||||
payload = {
|
||||
"created_by": f"{requested_by}/latest-package",
|
||||
}
|
||||
print(json.dumps(payload, ensure_ascii=False))
|
||||
PY
|
||||
)"
|
||||
;;
|
||||
list-releases)
|
||||
RELEASE_LIMIT="${ARG1:-10}"
|
||||
PATH_SUFFIX="/api/v1/ops/releases?limit=${RELEASE_LIMIT}"
|
||||
;;
|
||||
list-rollouts)
|
||||
RELEASE_ID="${ARG1:-}"
|
||||
ROLLOUT_LIMIT="${ARG2:-10}"
|
||||
if [[ -z "${RELEASE_ID}" ]]; then
|
||||
echo "release_id is required for list-rollouts" >&2
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
PATH_SUFFIX="/api/v1/ops/releases/${RELEASE_ID}/rollouts?limit=${ROLLOUT_LIMIT}"
|
||||
;;
|
||||
smart-latest)
|
||||
MODE="${ARG1:-}"
|
||||
if [[ "${MODE}" != "worker" && "${MODE}" != "control" ]]; then
|
||||
echo "mode must be worker or control" >&2
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
METHOD="POST"
|
||||
PATH_SUFFIX="/api/v1/ops/releases/from-package/latest/smart-rollout"
|
||||
CONFIRM_RISKY="$(confirm_to_bool "${ARG2:-}")"
|
||||
REQUEST_JSON="$(python3 - <<'PY' "${MODE}" "${CONFIRM_RISKY}" "${REQUESTED_BY}"
|
||||
import json
|
||||
import sys
|
||||
|
||||
mode = str(sys.argv[1] or "").strip()
|
||||
confirm_risky = str(sys.argv[2] or "").strip().lower() == "true"
|
||||
requested_by = str(sys.argv[3] or "").strip() or "cli/drive_release_hub"
|
||||
|
||||
payload = {
|
||||
"mode": mode,
|
||||
"confirm_risky": confirm_risky,
|
||||
"created_by": f"{requested_by}/latest-package",
|
||||
"rollout_created_by": f"{requested_by}/latest-package/rollout",
|
||||
}
|
||||
print(json.dumps(payload, ensure_ascii=False))
|
||||
PY
|
||||
)"
|
||||
;;
|
||||
preview-release)
|
||||
RELEASE_ID="${ARG1:-}"
|
||||
MODE="${ARG2:-}"
|
||||
if [[ -z "${RELEASE_ID}" ]]; then
|
||||
echo "release_id is required for preview-release" >&2
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
if [[ "${MODE}" != "worker" && "${MODE}" != "control" ]]; then
|
||||
echo "mode must be worker or control" >&2
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
METHOD="POST"
|
||||
PATH_SUFFIX="/api/v1/ops/releases/${RELEASE_ID}/smart-rollout-preview"
|
||||
REQUEST_JSON="$(python3 - <<'PY' "${MODE}" "${REQUESTED_BY}"
|
||||
import json
|
||||
import sys
|
||||
|
||||
mode = str(sys.argv[1] or "").strip()
|
||||
requested_by = str(sys.argv[2] or "").strip() or "cli/drive_release_hub"
|
||||
|
||||
payload = {
|
||||
"mode": mode,
|
||||
"created_by": f"{requested_by}/existing-release-preview",
|
||||
"rollout_created_by": f"{requested_by}/existing-release-preview/rollout",
|
||||
}
|
||||
print(json.dumps(payload, ensure_ascii=False))
|
||||
PY
|
||||
)"
|
||||
;;
|
||||
smart-release)
|
||||
RELEASE_ID="${ARG1:-}"
|
||||
MODE="${ARG2:-}"
|
||||
if [[ -z "${RELEASE_ID}" ]]; then
|
||||
echo "release_id is required for smart-release" >&2
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
if [[ "${MODE}" != "worker" && "${MODE}" != "control" ]]; then
|
||||
echo "mode must be worker or control" >&2
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
METHOD="POST"
|
||||
PATH_SUFFIX="/api/v1/ops/releases/${RELEASE_ID}/smart-rollout"
|
||||
CONFIRM_RISKY="$(confirm_to_bool "${ARG3:-}")"
|
||||
REQUEST_JSON="$(python3 - <<'PY' "${MODE}" "${CONFIRM_RISKY}" "${REQUESTED_BY}"
|
||||
import json
|
||||
import sys
|
||||
|
||||
mode = str(sys.argv[1] or "").strip()
|
||||
confirm_risky = str(sys.argv[2] or "").strip().lower() == "true"
|
||||
requested_by = str(sys.argv[3] or "").strip() or "cli/drive_release_hub"
|
||||
|
||||
payload = {
|
||||
"mode": mode,
|
||||
"confirm_risky": confirm_risky,
|
||||
"created_by": f"{requested_by}/existing-release",
|
||||
"rollout_created_by": f"{requested_by}/existing-release/rollout",
|
||||
}
|
||||
print(json.dumps(payload, ensure_ascii=False))
|
||||
PY
|
||||
)"
|
||||
;;
|
||||
*)
|
||||
echo "unknown command: ${COMMAND}" >&2
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "[1/3] request"
|
||||
printf '%s\n' "method=${METHOD}"
|
||||
printf '%s\n' "url=${BASE_URL}${PATH_SUFFIX}"
|
||||
if [[ -n "${REQUEST_JSON}" ]]; then
|
||||
print_json_or_raw "${REQUEST_JSON}"
|
||||
else
|
||||
echo "(no request body)"
|
||||
fi
|
||||
echo
|
||||
echo
|
||||
|
||||
RAW_HTTP_RESPONSE="$(request_with_status "${METHOD}" "${BASE_URL}${PATH_SUFFIX}" "${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
|
||||
|
||||
raw_response = str(sys.argv[1] or "")
|
||||
http_status = int(sys.argv[2] or 0)
|
||||
try:
|
||||
payload = json.loads(raw_response)
|
||||
except Exception:
|
||||
print(json.dumps({
|
||||
"http_status": http_status,
|
||||
"code": 1,
|
||||
"message": "response is not valid json",
|
||||
"raw_response": raw_response[:500],
|
||||
}, ensure_ascii=False, indent=2))
|
||||
sys.exit(0 if 200 <= http_status < 300 else 1)
|
||||
|
||||
data = payload.get("data") 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 ""),
|
||||
}
|
||||
|
||||
if isinstance(data, dict) and "package" in data:
|
||||
package = dict(data.get("package") or {})
|
||||
summary["package"] = {
|
||||
"available": bool(package.get("available", False)),
|
||||
"package_name": str(package.get("package_name") or ""),
|
||||
"package_type": str(package.get("package_type") or ""),
|
||||
"generated_at": str(package.get("generated_at") or ""),
|
||||
"smoke_test_ok": bool(package.get("smoke_test_ok", False)),
|
||||
"smoke_test_message": str(package.get("smoke_test_message") or ""),
|
||||
}
|
||||
|
||||
if isinstance(data, dict) and "launchpad_status" in data:
|
||||
launchpad_status = dict(data.get("launchpad_status") or {})
|
||||
worker_preview = dict(data.get("worker_rollout_preview") or {})
|
||||
worker_smart_rollout = dict(worker_preview.get("smart_rollout") or {})
|
||||
control_preview = dict(data.get("control_rollout_preview") or {})
|
||||
control_smart_rollout = dict(control_preview.get("smart_rollout") or {})
|
||||
summary["launchpad"] = {
|
||||
"channel": str(data.get("channel") or ""),
|
||||
"status": str(launchpad_status.get("status") or ""),
|
||||
"status_label": str(launchpad_status.get("status_label") or ""),
|
||||
"summary": str(launchpad_status.get("summary") or ""),
|
||||
"recommended_action_code": str(launchpad_status.get("recommended_action_code") or ""),
|
||||
"recommended_target_node_code": str(launchpad_status.get("recommended_target_node_code") or ""),
|
||||
"recommended_recovery_label": str(launchpad_status.get("recommended_recovery_label") or ""),
|
||||
"recommended_recovery_summary": str(launchpad_status.get("recommended_recovery_summary") or ""),
|
||||
"onboarding_bootstrap_pending_nodes": int(launchpad_status.get("onboarding_bootstrap_pending_nodes", 0) or 0),
|
||||
"onboarding_acceptance_ready_nodes": int(launchpad_status.get("onboarding_acceptance_ready_nodes", 0) or 0),
|
||||
"recommended_command": str(launchpad_status.get("recommended_command") or ""),
|
||||
"recommended_execution_mode": str(launchpad_status.get("recommended_execution_mode") or ""),
|
||||
"recommended_execution_mode_label": str(launchpad_status.get("recommended_execution_mode_label") or ""),
|
||||
"focus_ref": dict(launchpad_status.get("focus_ref") or {}),
|
||||
"latest_package": dict(data.get("latest_package") or {}),
|
||||
"latest_release": {
|
||||
"id": int(((data.get("latest_release") or {}).get("id") or 0)),
|
||||
"release_version": str((data.get("latest_release") or {}).get("release_version") or ""),
|
||||
"status": str((data.get("latest_release") or {}).get("status") or ""),
|
||||
},
|
||||
"worker_rollout_preview": {
|
||||
"message": str(worker_preview.get("message") or ""),
|
||||
"available": bool(worker_smart_rollout.get("available", False)),
|
||||
"requires_confirmation": bool(worker_preview.get("requires_confirmation", False)),
|
||||
"blocked": bool(worker_preview.get("blocked", False)),
|
||||
"execution_mode": str(worker_smart_rollout.get("execution_mode") or ""),
|
||||
"execution_mode_label": str(worker_smart_rollout.get("execution_mode_label") or ""),
|
||||
"target_node_codes": list(worker_smart_rollout.get("target_node_codes") or []),
|
||||
},
|
||||
"control_rollout_preview": {
|
||||
"message": str(control_preview.get("message") or ""),
|
||||
"available": bool(control_smart_rollout.get("available", False)),
|
||||
"requires_confirmation": bool(control_preview.get("requires_confirmation", False)),
|
||||
"blocked": bool(control_preview.get("blocked", False)),
|
||||
"execution_mode": str(control_smart_rollout.get("execution_mode") or ""),
|
||||
"execution_mode_label": str(control_smart_rollout.get("execution_mode_label") or ""),
|
||||
"target_node_codes": list(control_smart_rollout.get("target_node_codes") or []),
|
||||
},
|
||||
}
|
||||
print(json.dumps(summary, ensure_ascii=False, indent=2))
|
||||
sys.exit(0)
|
||||
|
||||
if isinstance(data, dict) and "available" in data:
|
||||
summary["latest_package"] = {
|
||||
"available": bool(data.get("available", False)),
|
||||
"package_name": str(data.get("package_name") or ""),
|
||||
"package_type": str(data.get("package_type") or ""),
|
||||
"generated_at": str(data.get("generated_at") or ""),
|
||||
"commit_sha": str(data.get("commit_sha") or ""),
|
||||
"smoke_test_ok": bool(data.get("smoke_test_ok", False)),
|
||||
"smoke_test_message": str(data.get("smoke_test_message") or ""),
|
||||
"reason": str(data.get("reason") or ""),
|
||||
}
|
||||
elif isinstance(data, dict) and "releases" in data:
|
||||
releases = list(data.get("releases") or [])
|
||||
summary["releases"] = {
|
||||
"count": len(releases),
|
||||
"items": [
|
||||
{
|
||||
"id": int(item.get("id") or 0),
|
||||
"release_version": str(item.get("release_version") or ""),
|
||||
"channel": str(item.get("channel") or ""),
|
||||
"status": str(item.get("status") or ""),
|
||||
}
|
||||
for item in releases[:5]
|
||||
],
|
||||
}
|
||||
elif isinstance(data, dict) and "rollouts" in data:
|
||||
rollouts = list(data.get("rollouts") or [])
|
||||
summary["rollouts"] = {
|
||||
"count": len(rollouts),
|
||||
"items": [
|
||||
{
|
||||
"id": int(item.get("id") or 0),
|
||||
"rollout_code": str(item.get("rollout_code") or ""),
|
||||
"status": str(item.get("status") or ""),
|
||||
"jobs_total": int(item.get("jobs_total") or 0),
|
||||
"jobs_created": int(item.get("jobs_created") or 0),
|
||||
}
|
||||
for item in rollouts[:5]
|
||||
],
|
||||
}
|
||||
else:
|
||||
release = data.get("release") or data
|
||||
rollout = data.get("rollout") or {}
|
||||
smart_rollout = data.get("smart_rollout") or {}
|
||||
preview = data.get("preview") or {}
|
||||
summary["preview_source"] = str(data.get("preview_source") or "")
|
||||
summary["release"] = {
|
||||
"id": int(release.get("id") or 0),
|
||||
"release_version": str(release.get("release_version") or ""),
|
||||
"channel": str(release.get("channel") or ""),
|
||||
"status": str(release.get("status") or ""),
|
||||
"is_preview": bool(release.get("is_preview", False)),
|
||||
}
|
||||
if rollout:
|
||||
summary["rollout"] = {
|
||||
"id": int(rollout.get("id") or 0),
|
||||
"rollout_code": str(rollout.get("rollout_code") or ""),
|
||||
"status": str(rollout.get("status") or ""),
|
||||
}
|
||||
if smart_rollout:
|
||||
summary["smart_rollout"] = {
|
||||
"available": bool(smart_rollout.get("available", False)),
|
||||
"mode": str(smart_rollout.get("mode") or ""),
|
||||
"mode_label": str(smart_rollout.get("mode_label") or ""),
|
||||
"target_node_codes": list((smart_rollout.get("target_selector") or {}).get("node_codes") or []),
|
||||
"summary": str(smart_rollout.get("summary") or smart_rollout.get("reason") or ""),
|
||||
}
|
||||
if preview:
|
||||
summary["preview"] = {
|
||||
"blocked": bool(preview.get("blocked", False)),
|
||||
"approval_required": bool(preview.get("approval_required", False)),
|
||||
"warnings": list(preview.get("warnings") or []),
|
||||
}
|
||||
summary["result_flags"] = {
|
||||
"release_created": bool(data.get("release_created", False)),
|
||||
"release_deduplicated": bool(data.get("release_deduplicated", False)),
|
||||
"rollout_created": bool(data.get("rollout_created", False)),
|
||||
"requires_confirmation": bool(data.get("requires_confirmation", False)),
|
||||
"blocked": bool(data.get("blocked", False)),
|
||||
}
|
||||
|
||||
print(json.dumps(summary, ensure_ascii=False, indent=2))
|
||||
PY
|
||||
|
||||
if [[ "${HTTP_STATUS}" -lt 200 || "${HTTP_STATUS}" -ge 300 ]]; then
|
||||
exit 1
|
||||
fi
|
||||
556
domain-api/deploy/multi-region/export_go_live_bundle.sh
Executable file
556
domain-api/deploy/multi-region/export_go_live_bundle.sh
Executable file
@@ -0,0 +1,556 @@
|
||||
#!/usr/bin/env bash
|
||||
set -uo 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}"
|
||||
|
||||
MAINLAND_BASE_URL_DEFAULT="$(ops_center_resolve_mainland_api_base_url)"
|
||||
OVERSEAS_BASE_URL_DEFAULT="$(ops_center_resolve_overseas_api_base_url)"
|
||||
REPORT_ROOT_DEFAULT="${OPS_CENTER_REPORT_ROOT}/go-live-bundles"
|
||||
|
||||
REPORT_DIR_INPUT="${1:-}"
|
||||
MAINLAND_BASE_URL_INPUT="${2:-}"
|
||||
OVERSEAS_BASE_URL_INPUT="${3:-}"
|
||||
|
||||
timestamp_now() {
|
||||
date '+%Y%m%d_%H%M%S'
|
||||
}
|
||||
|
||||
normalize_optional_base_url() {
|
||||
local raw_value="${1:-}"
|
||||
local fallback_value="${2:-}"
|
||||
if [[ -z "${raw_value}" ]]; then
|
||||
printf '%s' "${fallback_value}"
|
||||
return
|
||||
fi
|
||||
case "${raw_value}" in
|
||||
-|none|null|disabled)
|
||||
printf ''
|
||||
;;
|
||||
*)
|
||||
printf '%s' "${raw_value}"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
if [[ -n "${REPORT_DIR_INPUT}" && "${REPORT_DIR_INPUT}" != http://* && "${REPORT_DIR_INPUT}" != https://* ]]; then
|
||||
REPORT_DIR="${REPORT_DIR_INPUT}"
|
||||
MAINLAND_BASE_URL="${MAINLAND_BASE_URL_INPUT:-${MAINLAND_BASE_URL_DEFAULT}}"
|
||||
OVERSEAS_BASE_URL="$(normalize_optional_base_url "${OVERSEAS_BASE_URL_INPUT:-}" "${OVERSEAS_BASE_URL_DEFAULT}")"
|
||||
else
|
||||
REPORT_DIR="${REPORT_ROOT_DEFAULT}/go-live-bundle-$(timestamp_now)"
|
||||
MAINLAND_BASE_URL="${REPORT_DIR_INPUT:-${MAINLAND_BASE_URL_DEFAULT}}"
|
||||
OVERSEAS_BASE_URL="$(normalize_optional_base_url "${MAINLAND_BASE_URL_INPUT:-}" "${OVERSEAS_BASE_URL_DEFAULT}")"
|
||||
fi
|
||||
|
||||
mkdir -p "${REPORT_DIR}"
|
||||
|
||||
CURL_CONNECT_TIMEOUT="${OPS_CENTER_CURL_CONNECT_TIMEOUT:-3}"
|
||||
CURL_MAX_TIME="${OPS_CENTER_CURL_MAX_TIME:-15}"
|
||||
STEP_COMMAND_TIMEOUT_SECONDS="${OPS_CENTER_REPORT_COMMAND_TIMEOUT_SECONDS:-45}"
|
||||
DOCTOR_DECISION_TIMEOUT_SECONDS="${OPS_CENTER_REPORT_DOCTOR_DECISION_TIMEOUT_SECONDS:-120}"
|
||||
DOCTOR_EXPORT_TIMEOUT_SECONDS="${OPS_CENTER_REPORT_DOCTOR_EXPORT_TIMEOUT_SECONDS:-180}"
|
||||
DOCTOR_INNER_REPORT_TIMEOUT_SECONDS="${OPS_CENTER_DOCTOR_INNER_REPORT_TIMEOUT_SECONDS:-60}"
|
||||
|
||||
ENV_AUDIT_TXT_PATH="${REPORT_DIR}/00_env_audit.txt"
|
||||
SUMMARY_TXT_PATH="${REPORT_DIR}/01_go_live_check_summary.txt"
|
||||
GO_LIVE_JSON_PATH="${REPORT_DIR}/02_go_live_summary.json"
|
||||
STACK_JSON_PATH="${REPORT_DIR}/03_stack_diagnosis.json"
|
||||
DRIVER_FEED_JSON_PATH="${REPORT_DIR}/04_driver_feed.json"
|
||||
CODEX_BRIEF_JSON_PATH="${REPORT_DIR}/05_codex_brief.json"
|
||||
RELEASE_LAUNCHPAD_JSON_PATH="${REPORT_DIR}/06_release_launchpad.json"
|
||||
DOCTOR_DECISION_JSON_PATH="${REPORT_DIR}/07_doctor_decision.json"
|
||||
DOCTOR_EXPORT_STDOUT_PATH="${REPORT_DIR}/08_doctor_export_stdout.txt"
|
||||
MANIFEST_PATH="${REPORT_DIR}/manifest.json"
|
||||
|
||||
declare -a STEP_RESULTS=()
|
||||
|
||||
pretty_print_json_file() {
|
||||
local source_path="${1:-}"
|
||||
python3 - "${source_path}" <<'PY'
|
||||
import json
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
path = pathlib.Path(sys.argv[1])
|
||||
raw_text = path.read_text(encoding="utf-8") if path.exists() else ""
|
||||
try:
|
||||
payload = json.loads(raw_text)
|
||||
except Exception:
|
||||
print(raw_text)
|
||||
else:
|
||||
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
||||
PY
|
||||
}
|
||||
|
||||
record_step_result() {
|
||||
local key="${1:-}"
|
||||
local path="${2:-}"
|
||||
local ok="${3:-false}"
|
||||
local http_status="${4:-}"
|
||||
local exit_code="${5:-0}"
|
||||
STEP_RESULTS+=("${key}|${path}|${ok}|${http_status}|${exit_code}")
|
||||
}
|
||||
|
||||
fetch_json_to_file() {
|
||||
local key="${1:-}"
|
||||
local url="${2:-}"
|
||||
local output_path="${3:-}"
|
||||
local tmp_path
|
||||
tmp_path="$(mktemp)"
|
||||
local raw_text http_status
|
||||
raw_text="$(curl -sS \
|
||||
--connect-timeout "${CURL_CONNECT_TIMEOUT}" \
|
||||
--max-time "${CURL_MAX_TIME}" \
|
||||
"${url}" \
|
||||
-w $'\nHTTP_STATUS=%{http_code}' || true)"
|
||||
http_status="${raw_text##*$'\n'HTTP_STATUS=}"
|
||||
printf '%s' "${raw_text%$'\n'HTTP_STATUS=*}" > "${tmp_path}"
|
||||
pretty_print_json_file "${tmp_path}" > "${output_path}"
|
||||
rm -f "${tmp_path}"
|
||||
if [[ "${http_status}" == 2* ]]; then
|
||||
record_step_result "${key}" "${output_path}" "true" "${http_status}" "0"
|
||||
return 0
|
||||
fi
|
||||
record_step_result "${key}" "${output_path}" "false" "${http_status}" "1"
|
||||
return 1
|
||||
}
|
||||
|
||||
capture_command_to_file() {
|
||||
local key="${1:-}"
|
||||
local output_path="${2:-}"
|
||||
shift 2 || true
|
||||
capture_command_to_file_with_timeout "${STEP_COMMAND_TIMEOUT_SECONDS}" "${key}" "${output_path}" "$@"
|
||||
}
|
||||
|
||||
capture_command_to_file_with_timeout() {
|
||||
local timeout_seconds="${1:-45}"
|
||||
local key="${2:-}"
|
||||
local output_path="${3:-}"
|
||||
shift 3 || true
|
||||
local -a wrapped_command=("$@")
|
||||
local had_errexit="false"
|
||||
case "$-" in
|
||||
*e*) had_errexit="true" ;;
|
||||
esac
|
||||
if command -v timeout >/dev/null 2>&1; then
|
||||
wrapped_command=(timeout "${timeout_seconds}" "$@")
|
||||
fi
|
||||
set +e
|
||||
"${wrapped_command[@]}" > "${output_path}" 2>&1
|
||||
local exit_code=$?
|
||||
if [[ "${had_errexit}" == "true" ]]; then
|
||||
set -e
|
||||
else
|
||||
set +e
|
||||
fi
|
||||
if [[ "${exit_code}" -eq 0 ]]; then
|
||||
record_step_result "${key}" "${output_path}" "true" "" "${exit_code}"
|
||||
return 0
|
||||
fi
|
||||
record_step_result "${key}" "${output_path}" "false" "" "${exit_code}"
|
||||
return "${exit_code}"
|
||||
}
|
||||
|
||||
materialize_go_live_summary_fallback() {
|
||||
local summary_path="${1:-}"
|
||||
local output_path="${2:-}"
|
||||
python3 - "${summary_path}" "${output_path}" <<'PY'
|
||||
import json
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
summary_path = pathlib.Path(sys.argv[1])
|
||||
output_path = pathlib.Path(sys.argv[2])
|
||||
raw_text = summary_path.read_text(encoding="utf-8") if summary_path.exists() else ""
|
||||
start = raw_text.find("{")
|
||||
end = raw_text.rfind("}")
|
||||
if start < 0 or end < start:
|
||||
sys.exit(1)
|
||||
json_text = raw_text[start:end + 1]
|
||||
payload = json.loads(json_text)
|
||||
if not isinstance(payload, dict):
|
||||
sys.exit(1)
|
||||
output_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
PY
|
||||
}
|
||||
|
||||
override_step_result() {
|
||||
local key="${1:-}"
|
||||
local path="${2:-}"
|
||||
local ok="${3:-false}"
|
||||
local http_status="${4:-}"
|
||||
local exit_code="${5:-0}"
|
||||
local -a updated_results=()
|
||||
local raw_item existing_key
|
||||
for raw_item in "${STEP_RESULTS[@]}"; do
|
||||
existing_key="${raw_item%%|*}"
|
||||
if [[ "${existing_key}" == "${key}" ]]; then
|
||||
continue
|
||||
fi
|
||||
updated_results+=("${raw_item}")
|
||||
done
|
||||
STEP_RESULTS=("${updated_results[@]}")
|
||||
record_step_result "${key}" "${path}" "${ok}" "${http_status}" "${exit_code}"
|
||||
}
|
||||
|
||||
set +e
|
||||
capture_command_to_file \
|
||||
"env_audit" \
|
||||
"${ENV_AUDIT_TXT_PATH}" \
|
||||
bash "${SCRIPT_DIR}/drive_ops_center.sh" env-audit "${MAINLAND_BASE_URL}"
|
||||
|
||||
capture_command_to_file \
|
||||
"go_live_check_summary" \
|
||||
"${SUMMARY_TXT_PATH}" \
|
||||
bash "${SCRIPT_DIR}/drive_ops_center.sh" go-live-check "${MAINLAND_BASE_URL}" "${OVERSEAS_BASE_URL}" summary
|
||||
|
||||
fetch_json_to_file \
|
||||
"go_live_summary" \
|
||||
"${MAINLAND_BASE_URL}/api/v1/ops/go-live-summary?base_url=${MAINLAND_BASE_URL}" \
|
||||
"${GO_LIVE_JSON_PATH}"
|
||||
|
||||
if [[ ! -s "${GO_LIVE_JSON_PATH}" || "$(sed -n '1p' "${GO_LIVE_JSON_PATH}" 2>/dev/null)" != "{"* ]]; then
|
||||
if materialize_go_live_summary_fallback "${SUMMARY_TXT_PATH}" "${GO_LIVE_JSON_PATH}" 2>/dev/null; then
|
||||
override_step_result "go_live_summary" "${GO_LIVE_JSON_PATH}" "true" "fallback" "0"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$(python3 - "${GO_LIVE_JSON_PATH}" <<'PY'
|
||||
import json
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
path = pathlib.Path(sys.argv[1])
|
||||
try:
|
||||
payload = json.loads(path.read_text(encoding="utf-8"))
|
||||
except Exception:
|
||||
print("invalid")
|
||||
raise SystemExit(0)
|
||||
|
||||
if isinstance(payload, dict) and payload.get("detail") == "Not Found":
|
||||
print("not_found")
|
||||
else:
|
||||
print("ok")
|
||||
PY
|
||||
)" == "not_found" ]]; then
|
||||
if materialize_go_live_summary_fallback "${SUMMARY_TXT_PATH}" "${GO_LIVE_JSON_PATH}" 2>/dev/null; then
|
||||
override_step_result "go_live_summary" "${GO_LIVE_JSON_PATH}" "true" "fallback" "0"
|
||||
fi
|
||||
fi
|
||||
|
||||
fetch_json_to_file \
|
||||
"stack_diagnosis" \
|
||||
"${MAINLAND_BASE_URL}/api/v1/ops/stack-diagnosis?base_url=${MAINLAND_BASE_URL}" \
|
||||
"${STACK_JSON_PATH}"
|
||||
|
||||
fetch_json_to_file \
|
||||
"driver_feed" \
|
||||
"${MAINLAND_BASE_URL}/api/v1/ops/driver-feed" \
|
||||
"${DRIVER_FEED_JSON_PATH}"
|
||||
|
||||
fetch_json_to_file \
|
||||
"codex_brief" \
|
||||
"${MAINLAND_BASE_URL}/api/v1/ops/codex-brief" \
|
||||
"${CODEX_BRIEF_JSON_PATH}"
|
||||
|
||||
fetch_json_to_file \
|
||||
"release_launchpad" \
|
||||
"${MAINLAND_BASE_URL}/api/v1/ops/releases/launchpad" \
|
||||
"${RELEASE_LAUNCHPAD_JSON_PATH}"
|
||||
|
||||
capture_command_to_file_with_timeout \
|
||||
"${DOCTOR_EXPORT_TIMEOUT_SECONDS}" \
|
||||
"doctor_export" \
|
||||
"${DOCTOR_EXPORT_STDOUT_PATH}" \
|
||||
env "OPS_CENTER_DOCTOR_REPORT_TIMEOUT_SECONDS=${DOCTOR_INNER_REPORT_TIMEOUT_SECONDS}" \
|
||||
bash "${SCRIPT_DIR}/drive_ops_center.sh" doctor-export "${REPORT_DIR}/doctor-export" "${MAINLAND_BASE_URL}" "${OVERSEAS_BASE_URL}"
|
||||
|
||||
capture_command_to_file_with_timeout \
|
||||
"${DOCTOR_DECISION_TIMEOUT_SECONDS}" \
|
||||
"doctor_decision" \
|
||||
"${DOCTOR_DECISION_JSON_PATH}" \
|
||||
bash "${SCRIPT_DIR}/drive_ops_center.sh" doctor-decision "${REPORT_DIR}/doctor-export"
|
||||
set -e
|
||||
|
||||
python3 - <<'PY' \
|
||||
"${MANIFEST_PATH}" \
|
||||
"${REPORT_DIR}" \
|
||||
"${MAINLAND_BASE_URL}" \
|
||||
"${OVERSEAS_BASE_URL}" \
|
||||
"${ENV_AUDIT_TXT_PATH}" \
|
||||
"${SUMMARY_TXT_PATH}" \
|
||||
"${GO_LIVE_JSON_PATH}" \
|
||||
"${STACK_JSON_PATH}" \
|
||||
"${DRIVER_FEED_JSON_PATH}" \
|
||||
"${CODEX_BRIEF_JSON_PATH}" \
|
||||
"${RELEASE_LAUNCHPAD_JSON_PATH}" \
|
||||
"${DOCTOR_DECISION_JSON_PATH}" \
|
||||
"${DOCTOR_EXPORT_STDOUT_PATH}" \
|
||||
"${STEP_RESULTS[@]}"
|
||||
import json
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
manifest_path = pathlib.Path(sys.argv[1])
|
||||
report_dir = pathlib.Path(sys.argv[2])
|
||||
mainland_base_url = sys.argv[3]
|
||||
overseas_base_url = sys.argv[4]
|
||||
|
||||
file_keys = [
|
||||
"env_audit",
|
||||
"go_live_check_summary",
|
||||
"go_live_summary",
|
||||
"stack_diagnosis",
|
||||
"driver_feed",
|
||||
"codex_brief",
|
||||
"release_launchpad",
|
||||
"doctor_decision",
|
||||
"doctor_export",
|
||||
]
|
||||
file_paths = {
|
||||
"env_audit": pathlib.Path(sys.argv[5]),
|
||||
"go_live_check_summary": pathlib.Path(sys.argv[6]),
|
||||
"go_live_summary": pathlib.Path(sys.argv[7]),
|
||||
"stack_diagnosis": pathlib.Path(sys.argv[8]),
|
||||
"driver_feed": pathlib.Path(sys.argv[9]),
|
||||
"codex_brief": pathlib.Path(sys.argv[10]),
|
||||
"release_launchpad": pathlib.Path(sys.argv[11]),
|
||||
"doctor_decision": pathlib.Path(sys.argv[12]),
|
||||
"doctor_export": pathlib.Path(sys.argv[13]),
|
||||
}
|
||||
|
||||
step_results = {}
|
||||
for raw_item in sys.argv[14:]:
|
||||
parts = raw_item.split("|", 4)
|
||||
if len(parts) != 5:
|
||||
continue
|
||||
key, path_value, ok_value, http_status, exit_code = parts
|
||||
step_results[key] = {
|
||||
"path": path_value,
|
||||
"ok": ok_value == "true",
|
||||
"http_status": http_status,
|
||||
"exit_code": int(exit_code or 0),
|
||||
}
|
||||
|
||||
|
||||
def load_json_file(path: pathlib.Path) -> dict:
|
||||
if not path.is_file():
|
||||
return {}
|
||||
try:
|
||||
payload = json.loads(path.read_text(encoding="utf-8"))
|
||||
except Exception:
|
||||
return {}
|
||||
return payload if isinstance(payload, dict) else {}
|
||||
|
||||
|
||||
def load_env_audit_summary(path: pathlib.Path) -> dict:
|
||||
if not path.is_file():
|
||||
return {}
|
||||
raw_text = path.read_text(encoding="utf-8", errors="replace")
|
||||
marker = "[8/8] condensed env audit summary"
|
||||
candidate_text = raw_text.split(marker, 1)[1].strip() if marker in raw_text else raw_text.strip()
|
||||
if not candidate_text:
|
||||
return {}
|
||||
try:
|
||||
payload = json.loads(candidate_text)
|
||||
except Exception:
|
||||
return {}
|
||||
return payload if isinstance(payload, dict) else {}
|
||||
|
||||
failures = []
|
||||
artifacts = []
|
||||
for key in file_keys:
|
||||
artifact_path = file_paths[key]
|
||||
result = dict(step_results.get(key) or {})
|
||||
if not result:
|
||||
result = {
|
||||
"path": str(artifact_path),
|
||||
"ok": artifact_path.exists(),
|
||||
"http_status": "",
|
||||
"exit_code": 0 if artifact_path.exists() else 1,
|
||||
}
|
||||
entry = {
|
||||
"key": key,
|
||||
"path": str(artifact_path),
|
||||
"exists": artifact_path.exists(),
|
||||
"ok": bool(result.get("ok", False)),
|
||||
"http_status": str(result.get("http_status") or ""),
|
||||
"exit_code": int(result.get("exit_code", 0) or 0),
|
||||
}
|
||||
artifacts.append(entry)
|
||||
if not entry["ok"]:
|
||||
failures.append(key)
|
||||
|
||||
go_live_summary_payload = load_json_file(file_paths["go_live_summary"])
|
||||
stack_diagnosis_payload = load_json_file(file_paths["stack_diagnosis"])
|
||||
driver_feed_payload = load_json_file(file_paths["driver_feed"])
|
||||
codex_brief_payload = load_json_file(file_paths["codex_brief"])
|
||||
env_audit_summary = load_env_audit_summary(file_paths["env_audit"])
|
||||
|
||||
stack_diagnosis = dict(stack_diagnosis_payload.get("diagnosis") or {})
|
||||
driver_feed_summary = dict(driver_feed_payload.get("summary") or {})
|
||||
codex_brief_summary = dict(codex_brief_payload.get("summary") or {})
|
||||
|
||||
|
||||
def first_nonempty(*values: object) -> str:
|
||||
for value in values:
|
||||
normalized = str(value or "").strip()
|
||||
if normalized:
|
||||
return normalized
|
||||
return ""
|
||||
|
||||
|
||||
def nonempty_distinct(values: dict[str, str]) -> list[str]:
|
||||
return sorted({str(value or "").strip() for value in values.values() if str(value or "").strip()})
|
||||
|
||||
|
||||
def distinct_ints(values: dict[str, int], available_sources: set[str]) -> list[int]:
|
||||
return sorted({int(values[source]) for source in values if source in available_sources})
|
||||
|
||||
|
||||
launchpad_target_node_codes = {
|
||||
"go_live_summary": str(go_live_summary_payload.get("launchpad_recommended_target_node_code") or "").strip(),
|
||||
"stack_diagnosis": str(stack_diagnosis.get("launchpad_recommended_target_node_code") or "").strip(),
|
||||
"driver_feed": str(driver_feed_summary.get("launchpad_recommended_target_node_code") or "").strip(),
|
||||
"codex_brief": str(codex_brief_summary.get("launchpad_recommended_target_node_code") or "").strip(),
|
||||
}
|
||||
launchpad_recovery_labels = {
|
||||
"go_live_summary": str(go_live_summary_payload.get("launchpad_recommended_recovery_label") or "").strip(),
|
||||
"stack_diagnosis": str(stack_diagnosis.get("launchpad_recommended_recovery_label") or "").strip(),
|
||||
"driver_feed": str(driver_feed_summary.get("launchpad_recommended_recovery_label") or "").strip(),
|
||||
"codex_brief": str(codex_brief_summary.get("launchpad_recommended_recovery_label") or "").strip(),
|
||||
}
|
||||
launchpad_recovery_summaries = {
|
||||
"go_live_summary": str(go_live_summary_payload.get("launchpad_recommended_recovery_summary") or "").strip(),
|
||||
"stack_diagnosis": str(stack_diagnosis.get("launchpad_recommended_recovery_summary") or "").strip(),
|
||||
"driver_feed": str(driver_feed_summary.get("launchpad_recommended_recovery_summary") or "").strip(),
|
||||
"codex_brief": str(codex_brief_summary.get("launchpad_recommended_recovery_summary") or "").strip(),
|
||||
}
|
||||
launchpad_bootstrap_pending_nodes = {
|
||||
"go_live_summary": int(go_live_summary_payload.get("launchpad_onboarding_bootstrap_pending_nodes", 0) or 0),
|
||||
"stack_diagnosis": int(stack_diagnosis.get("launchpad_onboarding_bootstrap_pending_nodes", 0) or 0),
|
||||
"driver_feed": int(driver_feed_summary.get("launchpad_onboarding_bootstrap_pending_nodes", 0) or 0),
|
||||
"codex_brief": int(codex_brief_summary.get("launchpad_onboarding_bootstrap_pending_nodes", 0) or 0),
|
||||
}
|
||||
launchpad_acceptance_ready_nodes = {
|
||||
"go_live_summary": int(go_live_summary_payload.get("launchpad_onboarding_acceptance_ready_nodes", 0) or 0),
|
||||
"stack_diagnosis": int(stack_diagnosis.get("launchpad_onboarding_acceptance_ready_nodes", 0) or 0),
|
||||
"driver_feed": int(driver_feed_summary.get("launchpad_onboarding_acceptance_ready_nodes", 0) or 0),
|
||||
"codex_brief": int(codex_brief_summary.get("launchpad_onboarding_acceptance_ready_nodes", 0) or 0),
|
||||
}
|
||||
available_alignment_sources = {
|
||||
source
|
||||
for source, payload_value in {
|
||||
"go_live_summary": go_live_summary_payload,
|
||||
"stack_diagnosis": stack_diagnosis_payload,
|
||||
"driver_feed": driver_feed_payload,
|
||||
"codex_brief": codex_brief_payload,
|
||||
}.items()
|
||||
if isinstance(payload_value, dict) and payload_value
|
||||
}
|
||||
launchpad_alignment = {
|
||||
"consistent": True,
|
||||
"target_node_code_consistent": True,
|
||||
"recovery_label_consistent": True,
|
||||
"recovery_summary_consistent": True,
|
||||
"bootstrap_pending_consistent": True,
|
||||
"acceptance_ready_consistent": True,
|
||||
"available_sources": sorted(available_alignment_sources),
|
||||
"target_node_codes": launchpad_target_node_codes,
|
||||
"recovery_labels": launchpad_recovery_labels,
|
||||
"recovery_summaries": launchpad_recovery_summaries,
|
||||
"bootstrap_pending_nodes": launchpad_bootstrap_pending_nodes,
|
||||
"acceptance_ready_nodes": launchpad_acceptance_ready_nodes,
|
||||
}
|
||||
launchpad_alignment["target_node_code_consistent"] = len(nonempty_distinct({
|
||||
source: value for source, value in launchpad_target_node_codes.items() if source in available_alignment_sources
|
||||
})) <= 1
|
||||
launchpad_alignment["recovery_label_consistent"] = len(nonempty_distinct({
|
||||
source: value for source, value in launchpad_recovery_labels.items() if source in available_alignment_sources
|
||||
})) <= 1
|
||||
launchpad_alignment["recovery_summary_consistent"] = len(nonempty_distinct({
|
||||
source: value for source, value in launchpad_recovery_summaries.items() if source in available_alignment_sources
|
||||
})) <= 1
|
||||
launchpad_alignment["bootstrap_pending_consistent"] = len(
|
||||
distinct_ints(launchpad_bootstrap_pending_nodes, available_alignment_sources)
|
||||
) <= 1
|
||||
launchpad_alignment["acceptance_ready_consistent"] = len(
|
||||
distinct_ints(launchpad_acceptance_ready_nodes, available_alignment_sources)
|
||||
) <= 1
|
||||
launchpad_alignment["consistent"] = all([
|
||||
bool(launchpad_alignment["target_node_code_consistent"]),
|
||||
bool(launchpad_alignment["recovery_label_consistent"]),
|
||||
bool(launchpad_alignment["recovery_summary_consistent"]),
|
||||
bool(launchpad_alignment["bootstrap_pending_consistent"]),
|
||||
bool(launchpad_alignment["acceptance_ready_consistent"]),
|
||||
])
|
||||
|
||||
env_audit_status = str(env_audit_summary.get("status") or "").strip()
|
||||
env_audit_runtime_may_need_restart = bool(((env_audit_summary.get("runtime") or {}).get("runtime_may_need_restart", False)))
|
||||
review_status_hint = "ready"
|
||||
review_headline = "核心上线证据已经齐备,可进入最终人工复核或正式发布门禁。"
|
||||
if failures or env_audit_status == "blocked":
|
||||
review_status_hint = "blocked"
|
||||
review_headline = "当前导出包仍有关键阻断项,不能直接作为正式上线终稿。"
|
||||
elif env_audit_status == "attention" or not launchpad_alignment["consistent"]:
|
||||
review_status_hint = "attention"
|
||||
if not launchpad_alignment["consistent"]:
|
||||
review_headline = "核心报告虽已导出,但 launchpad 摘要在不同 surface 之间存在漂移,建议先重新复核。"
|
||||
else:
|
||||
review_headline = "核心报告已齐,但环境审计仍提示存在待收口事项。"
|
||||
|
||||
payload = {
|
||||
"report_dir": str(report_dir),
|
||||
"mainland_base_url": mainland_base_url,
|
||||
"overseas_base_url": overseas_base_url,
|
||||
"artifacts": artifacts,
|
||||
"failures": failures,
|
||||
"summary": {
|
||||
"ok": len(failures) == 0,
|
||||
"artifact_total": len(artifacts),
|
||||
"failure_total": len(failures),
|
||||
"review_status_hint": review_status_hint,
|
||||
"review_headline": review_headline,
|
||||
"env_audit_status": env_audit_status,
|
||||
"env_audit_runtime_may_need_restart": env_audit_runtime_may_need_restart,
|
||||
"launchpad_recommended_target_node_code": first_nonempty(
|
||||
go_live_summary_payload.get("launchpad_recommended_target_node_code"),
|
||||
stack_diagnosis.get("launchpad_recommended_target_node_code"),
|
||||
driver_feed_summary.get("launchpad_recommended_target_node_code"),
|
||||
codex_brief_summary.get("launchpad_recommended_target_node_code"),
|
||||
),
|
||||
"launchpad_recommended_recovery_label": first_nonempty(
|
||||
go_live_summary_payload.get("launchpad_recommended_recovery_label"),
|
||||
stack_diagnosis.get("launchpad_recommended_recovery_label"),
|
||||
driver_feed_summary.get("launchpad_recommended_recovery_label"),
|
||||
codex_brief_summary.get("launchpad_recommended_recovery_label"),
|
||||
),
|
||||
"launchpad_recommended_recovery_summary": first_nonempty(
|
||||
go_live_summary_payload.get("launchpad_recommended_recovery_summary"),
|
||||
stack_diagnosis.get("launchpad_recommended_recovery_summary"),
|
||||
driver_feed_summary.get("launchpad_recommended_recovery_summary"),
|
||||
codex_brief_summary.get("launchpad_recommended_recovery_summary"),
|
||||
),
|
||||
"launchpad_onboarding_bootstrap_pending_nodes": max(launchpad_bootstrap_pending_nodes.values() or [0]),
|
||||
"launchpad_onboarding_acceptance_ready_nodes": max(launchpad_acceptance_ready_nodes.values() or [0]),
|
||||
"launchpad_alignment": launchpad_alignment,
|
||||
"recommended_reading_order": [
|
||||
"00_env_audit.txt",
|
||||
"01_go_live_check_summary.txt",
|
||||
"02_go_live_summary.json",
|
||||
"03_stack_diagnosis.json",
|
||||
"04_driver_feed.json",
|
||||
"05_codex_brief.json",
|
||||
"06_release_launchpad.json",
|
||||
"07_doctor_decision.json",
|
||||
"08_doctor_export_stdout.txt",
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
manifest_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
||||
PY
|
||||
135
domain-api/deploy/multi-region/init_ops_center_config.sh
Executable file
135
domain-api/deploy/multi-region/init_ops_center_config.sh
Executable file
@@ -0,0 +1,135 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
TEMPLATE_PATH="${SCRIPT_DIR}/templates/domaincheck-ops-center.env.example"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
usage:
|
||||
bash domain-api/deploy/multi-region/init_ops_center_config.sh \
|
||||
[config_path] [mainland_api_base_url] [overseas_api_base_url] [control_plane_base_url]
|
||||
|
||||
examples:
|
||||
bash domain-api/deploy/multi-region/init_ops_center_config.sh
|
||||
bash domain-api/deploy/multi-region/init_ops_center_config.sh /etc/default/domaincheck-ops-center \
|
||||
http://121.204.244.188:8100 \
|
||||
http://152.53.37.118:8100 \
|
||||
https://api.example.com
|
||||
|
||||
notes:
|
||||
- existing config files are preserved unless FORCE=true
|
||||
- advanced fields can be overridden with env vars:
|
||||
OPS_ACTIVE_PROFILE
|
||||
OPS_PROFILE_NAMES
|
||||
OPS_DEFAULT_REQUESTED_BY
|
||||
OPS_DEFAULT_ISSUED_BY
|
||||
OPS_DEFAULT_EXPIRES_IN_HOURS
|
||||
OPS_DEFAULT_ROOT_DIR
|
||||
OPS_REPORT_ROOT
|
||||
EOF
|
||||
}
|
||||
|
||||
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
|
||||
usage
|
||||
exit 0
|
||||
fi
|
||||
|
||||
TARGET_PATH="${1:-/etc/default/domaincheck-ops-center}"
|
||||
MAINLAND_API_BASE_URL="${2:-${OPS_MAINLAND_API_BASE_URL:-http://127.0.0.1:8100}}"
|
||||
OVERSEAS_API_BASE_URL="${3:-${OPS_OVERSEAS_API_BASE_URL:-http://127.0.0.1:8100}}"
|
||||
CONTROL_PLANE_BASE_URL="${4:-${OPS_DEFAULT_CONTROL_PLANE_BASE_URL:-}}"
|
||||
|
||||
ACTIVE_PROFILE="${OPS_ACTIVE_PROFILE:-default}"
|
||||
PROFILE_NAMES="${OPS_PROFILE_NAMES:-${ACTIVE_PROFILE}}"
|
||||
DEFAULT_REQUESTED_BY="${OPS_DEFAULT_REQUESTED_BY:-cli/ops-center}"
|
||||
DEFAULT_ISSUED_BY="${OPS_DEFAULT_ISSUED_BY:-ops-center}"
|
||||
DEFAULT_EXPIRES_IN_HOURS="${OPS_DEFAULT_EXPIRES_IN_HOURS:-72}"
|
||||
DEFAULT_ROOT_DIR="${OPS_DEFAULT_ROOT_DIR:-/opt/domaincheck}"
|
||||
REPORT_ROOT="${OPS_REPORT_ROOT:-}"
|
||||
NODE_AGENT_SERVICE_NAME="${OPS_NODE_AGENT_SERVICE_NAME:-domaincheck-node-agent}"
|
||||
NODE_AGENT_ENV_FILE="${OPS_NODE_AGENT_ENV_FILE:-/etc/default/domaincheck-node-agent}"
|
||||
FORCE_WRITE="${FORCE:-false}"
|
||||
|
||||
if [[ ! -f "${TEMPLATE_PATH}" ]]; then
|
||||
echo "template not found: ${TEMPLATE_PATH}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$(dirname "${TARGET_PATH}")"
|
||||
|
||||
if [[ ! -f "${TARGET_PATH}" || "${FORCE_WRITE}" == "true" ]]; then
|
||||
cp "${TEMPLATE_PATH}" "${TARGET_PATH}"
|
||||
fi
|
||||
|
||||
upsert_key() {
|
||||
local key="$1"
|
||||
local value="$2"
|
||||
if grep -q "^${key}=" "${TARGET_PATH}"; then
|
||||
sed -i "s#^${key}=.*#${key}=${value}#" "${TARGET_PATH}"
|
||||
else
|
||||
printf '%s=%s\n' "${key}" "${value}" >>"${TARGET_PATH}"
|
||||
fi
|
||||
}
|
||||
|
||||
upsert_commented_key() {
|
||||
local key="$1"
|
||||
local value="$2"
|
||||
if [[ -z "${value}" ]]; then
|
||||
return 0
|
||||
fi
|
||||
upsert_key "${key}" "${value}"
|
||||
}
|
||||
|
||||
upsert_key "OPS_ACTIVE_PROFILE" "${ACTIVE_PROFILE}"
|
||||
upsert_key "OPS_PROFILE_NAMES" "${PROFILE_NAMES}"
|
||||
upsert_key "OPS_MAINLAND_API_BASE_URL" "${MAINLAND_API_BASE_URL}"
|
||||
upsert_key "OPS_OVERSEAS_API_BASE_URL" "${OVERSEAS_API_BASE_URL}"
|
||||
upsert_key "OPS_DEFAULT_CONTROL_PLANE_BASE_URL" "${CONTROL_PLANE_BASE_URL}"
|
||||
upsert_key "OPS_DEFAULT_REQUESTED_BY" "${DEFAULT_REQUESTED_BY}"
|
||||
upsert_key "OPS_DEFAULT_ISSUED_BY" "${DEFAULT_ISSUED_BY}"
|
||||
upsert_key "OPS_DEFAULT_EXPIRES_IN_HOURS" "${DEFAULT_EXPIRES_IN_HOURS}"
|
||||
upsert_key "OPS_DEFAULT_ROOT_DIR" "${DEFAULT_ROOT_DIR}"
|
||||
upsert_key "OPS_NODE_AGENT_SERVICE_NAME" "${NODE_AGENT_SERVICE_NAME}"
|
||||
upsert_key "OPS_NODE_AGENT_ENV_FILE" "${NODE_AGENT_ENV_FILE}"
|
||||
upsert_commented_key "OPS_REPORT_ROOT" "${REPORT_ROOT}"
|
||||
|
||||
python3 - <<'PY' "${TARGET_PATH}"
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
config_path = Path(sys.argv[1])
|
||||
keys = [
|
||||
"OPS_ACTIVE_PROFILE",
|
||||
"OPS_PROFILE_NAMES",
|
||||
"OPS_MAINLAND_API_BASE_URL",
|
||||
"OPS_OVERSEAS_API_BASE_URL",
|
||||
"OPS_DEFAULT_CONTROL_PLANE_BASE_URL",
|
||||
"OPS_DEFAULT_REQUESTED_BY",
|
||||
"OPS_DEFAULT_ISSUED_BY",
|
||||
"OPS_DEFAULT_EXPIRES_IN_HOURS",
|
||||
"OPS_DEFAULT_ROOT_DIR",
|
||||
"OPS_NODE_AGENT_SERVICE_NAME",
|
||||
"OPS_NODE_AGENT_ENV_FILE",
|
||||
"OPS_REPORT_ROOT",
|
||||
]
|
||||
values = {}
|
||||
for line in config_path.read_text(encoding="utf-8").splitlines():
|
||||
if not line or line.startswith("#") or "=" not in line:
|
||||
continue
|
||||
key, value = line.split("=", 1)
|
||||
if key in keys:
|
||||
values[key] = value
|
||||
|
||||
print(json.dumps({
|
||||
"ok": True,
|
||||
"config_path": str(config_path),
|
||||
"summary": values,
|
||||
"next_commands": [
|
||||
"bash domain-api/deploy/multi-region/drive_ops_center.sh config",
|
||||
"bash domain-api/deploy/multi-region/drive_ops_center.sh doctor-export",
|
||||
"bash domain-api/deploy/multi-region/drive_ops_center.sh agent-check",
|
||||
],
|
||||
}, ensure_ascii=False, indent=2))
|
||||
PY
|
||||
@@ -100,7 +100,7 @@ REDIS_DB=0
|
||||
EOF
|
||||
|
||||
cd "${DOMAIN_API_DIR}"
|
||||
bash deploy/multi-region/bootstrap_mainland.sh /opt/domaincheck controller
|
||||
bash domain-api/deploy/multi-region/bootstrap_mainland.sh /opt/domaincheck controller
|
||||
|
||||
cat >/etc/default/domaincheck-worker <<EOF
|
||||
WORKER_MODE=linux-systemd
|
||||
@@ -138,4 +138,4 @@ chown -R www:www "${REPO_DIR}"
|
||||
|
||||
echo
|
||||
echo "国内 controller 部署完成,建议继续执行:"
|
||||
echo "cd ${DOMAIN_API_DIR} && bash deploy/multi-region/check_mainland_controller.sh"
|
||||
echo "cd ${DOMAIN_API_DIR} && bash domain-api/deploy/multi-region/check_mainland_controller.sh"
|
||||
|
||||
@@ -101,7 +101,7 @@ REDIS_DB=0
|
||||
EOF
|
||||
|
||||
cd "${DOMAIN_API_DIR}"
|
||||
bash deploy/multi-region/bootstrap_mainland.sh /opt/domaincheck worker
|
||||
bash domain-api/deploy/multi-region/bootstrap_mainland.sh /opt/domaincheck worker
|
||||
|
||||
cat >/etc/default/domaincheck-worker <<EOF
|
||||
WORKER_MODE=linux-systemd
|
||||
|
||||
@@ -185,6 +185,15 @@ sed -i 's#^NODE_ROLE=.*#NODE_ROLE=control#' /etc/default/domaincheck-api
|
||||
sed -i 's#^SYNC_PUSH_ENABLED=.*#SYNC_PUSH_ENABLED=false#' /etc/default/domaincheck-api
|
||||
sed -i "s#^CORS_ORIGINS=.*#CORS_ORIGINS='[\\\"https://${ADMIN_DOMAIN}\\\",\\\"http://127.0.0.1:3201\\\",\\\"http://localhost:3201\\\"]'#" /etc/default/domaincheck-api
|
||||
|
||||
OPS_DEFAULT_REQUESTED_BY="cli/${OVERSEAS_NODE_CODE}" \
|
||||
OPS_DEFAULT_ISSUED_BY="${OVERSEAS_NODE_CODE}" \
|
||||
OPS_DEFAULT_ROOT_DIR="${BASE_DIR}" \
|
||||
bash "${DOMAIN_API_DIR}/deploy/multi-region/init_ops_center_config.sh" \
|
||||
/etc/default/domaincheck-ops-center \
|
||||
http://127.0.0.1:8100 \
|
||||
http://127.0.0.1:8100 \
|
||||
"https://${API_DOMAIN}" >/dev/null
|
||||
|
||||
cp "${DOMAIN_API_DIR}/deploy/systemd/domain-api.service" /etc/systemd/system/domaincheck-api.service
|
||||
cp "${DOMAIN_API_DIR}/deploy/systemd/domain-worker.service" /etc/systemd/system/domaincheck-worker.service
|
||||
|
||||
@@ -206,3 +215,5 @@ echo "国外主控机部署完成,建议继续执行:"
|
||||
echo "curl http://127.0.0.1:8100/health"
|
||||
echo "curl http://127.0.0.1:8100/api/v1/runtime/preflight"
|
||||
echo "curl http://127.0.0.1:8100/api/v1/runtime/readiness"
|
||||
echo "bash ${DOMAIN_API_DIR}/deploy/multi-region/drive_ops_center.sh config"
|
||||
echo "如后续接入大陆 controller,请记得修改 /etc/default/domaincheck-ops-center 中的 OPS_MAINLAND_API_BASE_URL"
|
||||
|
||||
106
domain-api/deploy/multi-region/lib_ops_center.sh
Normal file
106
domain-api/deploy/multi-region/lib_ops_center.sh
Normal file
@@ -0,0 +1,106 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
ops_center_normalize_profile_key() {
|
||||
local raw_value="${1:-default}"
|
||||
printf '%s' "${raw_value}" | tr '[:lower:]-./' '[:upper:]____' | tr -cd 'A-Z0-9_'
|
||||
}
|
||||
|
||||
ops_center_load_config() {
|
||||
local caller_dir="${1:-}"
|
||||
if [[ -z "${caller_dir}" ]]; then
|
||||
caller_dir="$(pwd)"
|
||||
fi
|
||||
|
||||
local env_active_profile="${OPS_ACTIVE_PROFILE:-}"
|
||||
local env_profile_names="${OPS_PROFILE_NAMES:-}"
|
||||
local env_report_root="${OPS_REPORT_ROOT:-}"
|
||||
local env_mainland_api_base_url="${OPS_MAINLAND_API_BASE_URL:-}"
|
||||
local env_overseas_api_base_url="${OPS_OVERSEAS_API_BASE_URL:-}"
|
||||
local env_control_plane_base_url="${OPS_DEFAULT_CONTROL_PLANE_BASE_URL:-}"
|
||||
local env_requested_by="${OPS_DEFAULT_REQUESTED_BY:-}"
|
||||
local env_issued_by="${OPS_DEFAULT_ISSUED_BY:-}"
|
||||
local env_expires_in_hours="${OPS_DEFAULT_EXPIRES_IN_HOURS:-}"
|
||||
local env_root_dir="${OPS_DEFAULT_ROOT_DIR:-}"
|
||||
local env_node_agent_service_name="${OPS_NODE_AGENT_SERVICE_NAME:-}"
|
||||
local env_node_agent_env_file="${OPS_NODE_AGENT_ENV_FILE:-}"
|
||||
|
||||
OPS_CENTER_SCRIPT_DIR="${caller_dir}"
|
||||
OPS_CENTER_API_DIR="$(cd "${OPS_CENTER_SCRIPT_DIR}/../.." && pwd)"
|
||||
OPS_CENTER_PROJECT_ROOT="$(cd "${OPS_CENTER_API_DIR}/.." && pwd)"
|
||||
OPS_CENTER_RUNTIME_DIR="${OPS_CENTER_API_DIR}/runtime"
|
||||
OPS_CENTER_REPORT_ROOT_DEFAULT="${OPS_CENTER_RUNTIME_DIR}/ops-center-reports"
|
||||
OPS_CENTER_CONFIG_FILE="${DOMAINCHECK_OPS_CENTER_ENV:-${OPS_CENTER_ENV_FILE:-/etc/default/domaincheck-ops-center}}"
|
||||
|
||||
if [[ -f "${OPS_CENTER_CONFIG_FILE}" ]]; then
|
||||
# shellcheck disable=SC1090
|
||||
source "${OPS_CENTER_CONFIG_FILE}"
|
||||
fi
|
||||
|
||||
OPS_CENTER_ACTIVE_PROFILE="${OPS_CENTER_PROFILE:-${DOMAINCHECK_OPS_CENTER_PROFILE:-${env_active_profile:-${OPS_ACTIVE_PROFILE:-default}}}}"
|
||||
OPS_CENTER_ACTIVE_PROFILE_KEY="$(ops_center_normalize_profile_key "${OPS_CENTER_ACTIVE_PROFILE}")"
|
||||
OPS_CENTER_PROFILE_NAMES="${env_profile_names:-${OPS_PROFILE_NAMES:-${OPS_CENTER_ACTIVE_PROFILE}}}"
|
||||
OPS_CENTER_REPORT_ROOT="${env_report_root:-${OPS_REPORT_ROOT:-${OPS_CENTER_REPORT_ROOT_DEFAULT}}}"
|
||||
OPS_CENTER_ENV_MAINLAND_API_BASE_URL="${env_mainland_api_base_url}"
|
||||
OPS_CENTER_ENV_OVERSEAS_API_BASE_URL="${env_overseas_api_base_url}"
|
||||
OPS_CENTER_ENV_CONTROL_PLANE_BASE_URL="${env_control_plane_base_url}"
|
||||
OPS_CENTER_ENV_REQUESTED_BY="${env_requested_by}"
|
||||
OPS_CENTER_ENV_ISSUED_BY="${env_issued_by}"
|
||||
OPS_CENTER_ENV_EXPIRES_IN_HOURS="${env_expires_in_hours}"
|
||||
OPS_CENTER_ENV_ROOT_DIR="${env_root_dir}"
|
||||
OPS_CENTER_ENV_NODE_AGENT_SERVICE_NAME="${env_node_agent_service_name}"
|
||||
OPS_CENTER_ENV_NODE_AGENT_ENV_FILE="${env_node_agent_env_file}"
|
||||
}
|
||||
|
||||
ops_center_resolve_profile_value() {
|
||||
local suffix="$1"
|
||||
local fallback_value="${2:-}"
|
||||
local profile_key="${OPS_CENTER_ACTIVE_PROFILE_KEY:-DEFAULT}"
|
||||
local variable_name="OPS_PROFILE_${profile_key}_${suffix}"
|
||||
local profile_value="${!variable_name:-}"
|
||||
if [[ -n "${profile_value}" ]]; then
|
||||
printf '%s' "${profile_value}"
|
||||
else
|
||||
printf '%s' "${fallback_value}"
|
||||
fi
|
||||
}
|
||||
|
||||
ops_center_resolve_mainland_api_base_url() {
|
||||
ops_center_resolve_profile_value "MAINLAND_API_BASE_URL" "${OPS_CENTER_ENV_MAINLAND_API_BASE_URL:-${OPS_MAINLAND_API_BASE_URL:-http://127.0.0.1:8100}}"
|
||||
}
|
||||
|
||||
ops_center_resolve_overseas_api_base_url() {
|
||||
ops_center_resolve_profile_value "OVERSEAS_API_BASE_URL" "${OPS_CENTER_ENV_OVERSEAS_API_BASE_URL:-${OPS_OVERSEAS_API_BASE_URL:-http://127.0.0.1:8100}}"
|
||||
}
|
||||
|
||||
ops_center_resolve_control_plane_base_url() {
|
||||
ops_center_resolve_profile_value "DEFAULT_CONTROL_PLANE_BASE_URL" "${OPS_CENTER_ENV_CONTROL_PLANE_BASE_URL:-${OPS_DEFAULT_CONTROL_PLANE_BASE_URL:-}}"
|
||||
}
|
||||
|
||||
ops_center_resolve_requested_by() {
|
||||
ops_center_resolve_profile_value "DEFAULT_REQUESTED_BY" "${OPS_CENTER_ENV_REQUESTED_BY:-${OPS_DEFAULT_REQUESTED_BY:-cli/ops-center}}"
|
||||
}
|
||||
|
||||
ops_center_resolve_issued_by() {
|
||||
ops_center_resolve_profile_value "DEFAULT_ISSUED_BY" "${OPS_CENTER_ENV_ISSUED_BY:-${OPS_DEFAULT_ISSUED_BY:-ops-center}}"
|
||||
}
|
||||
|
||||
ops_center_resolve_expires_in_hours() {
|
||||
ops_center_resolve_profile_value "DEFAULT_EXPIRES_IN_HOURS" "${OPS_CENTER_ENV_EXPIRES_IN_HOURS:-${OPS_DEFAULT_EXPIRES_IN_HOURS:-72}}"
|
||||
}
|
||||
|
||||
ops_center_resolve_root_dir() {
|
||||
ops_center_resolve_profile_value "DEFAULT_ROOT_DIR" "${OPS_CENTER_ENV_ROOT_DIR:-${OPS_DEFAULT_ROOT_DIR:-/opt/domaincheck}}"
|
||||
}
|
||||
|
||||
ops_center_resolve_node_agent_service_name() {
|
||||
ops_center_resolve_profile_value "NODE_AGENT_SERVICE_NAME" "${OPS_CENTER_ENV_NODE_AGENT_SERVICE_NAME:-${OPS_NODE_AGENT_SERVICE_NAME:-domaincheck-node-agent}}"
|
||||
}
|
||||
|
||||
ops_center_resolve_node_agent_env_file() {
|
||||
ops_center_resolve_profile_value "NODE_AGENT_ENV_FILE" "${OPS_CENTER_ENV_NODE_AGENT_ENV_FILE:-${OPS_NODE_AGENT_ENV_FILE:-/etc/default/domaincheck-node-agent}}"
|
||||
}
|
||||
|
||||
ops_center_ensure_report_dir() {
|
||||
local report_dir="${1:-${OPS_CENTER_REPORT_ROOT_DEFAULT}}"
|
||||
mkdir -p "${report_dir}"
|
||||
}
|
||||
317
domain-api/deploy/multi-region/review_go_live_bundle.sh
Executable file
317
domain-api/deploy/multi-region/review_go_live_bundle.sh
Executable file
@@ -0,0 +1,317 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
TARGET_PATH="${1:-}"
|
||||
|
||||
if [[ -z "${TARGET_PATH}" ]]; then
|
||||
echo "usage: bash domain-api/deploy/multi-region/review_go_live_bundle.sh <manifest.json|report_dir>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -d "${TARGET_PATH}" ]]; then
|
||||
MANIFEST_PATH="${TARGET_PATH%/}/manifest.json"
|
||||
else
|
||||
MANIFEST_PATH="${TARGET_PATH}"
|
||||
fi
|
||||
|
||||
if [[ ! -f "${MANIFEST_PATH}" ]]; then
|
||||
echo "manifest not found: ${MANIFEST_PATH}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
python3 - "${MANIFEST_PATH}" <<'PY'
|
||||
import json
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
manifest_path = pathlib.Path(sys.argv[1])
|
||||
payload = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||||
|
||||
artifacts = payload.get("artifacts") or []
|
||||
failures = payload.get("failures") or []
|
||||
summary = payload.get("summary") or {}
|
||||
|
||||
critical_keys = {"go_live_summary", "stack_diagnosis", "driver_feed", "codex_brief", "release_launchpad"}
|
||||
artifact_by_key = {str(item.get("key") or ""): item for item in artifacts if isinstance(item, dict)}
|
||||
critical_failures = [key for key in critical_keys if not bool((artifact_by_key.get(key) or {}).get("ok", False))]
|
||||
noncritical_failures = [key for key in failures if key not in critical_keys]
|
||||
env_audit_artifact = artifact_by_key.get("env_audit") or {}
|
||||
|
||||
env_audit_status = ""
|
||||
env_audit_headline = ""
|
||||
env_audit_missing_items = []
|
||||
env_audit_recommended_actions = []
|
||||
env_audit_blocking = False
|
||||
env_audit_attention = False
|
||||
env_audit_runtime_may_need_restart = False
|
||||
go_live_summary_payload = {}
|
||||
stack_diagnosis_payload = {}
|
||||
driver_feed_payload = {}
|
||||
codex_brief_payload = {}
|
||||
|
||||
def load_json_artifact(key: str) -> dict:
|
||||
artifact = artifact_by_key.get(key) or {}
|
||||
artifact_path = pathlib.Path(str(artifact.get("path") or ""))
|
||||
if not artifact_path.is_file():
|
||||
return {}
|
||||
try:
|
||||
loaded = json.loads(artifact_path.read_text(encoding="utf-8"))
|
||||
except Exception:
|
||||
return {}
|
||||
return loaded if isinstance(loaded, dict) else {}
|
||||
|
||||
env_audit_path = pathlib.Path(str(env_audit_artifact.get("path") or ""))
|
||||
if env_audit_path.is_file():
|
||||
raw_text = env_audit_path.read_text(encoding="utf-8", errors="replace")
|
||||
marker = "[8/8] condensed env audit summary"
|
||||
candidate_text = raw_text.split(marker, 1)[1].strip() if marker in raw_text else raw_text.strip()
|
||||
if candidate_text:
|
||||
try:
|
||||
env_payload = json.loads(candidate_text)
|
||||
except Exception:
|
||||
env_payload = {}
|
||||
env_audit_status = str(env_payload.get("status") or "")
|
||||
env_audit_headline = str(env_payload.get("headline") or "")
|
||||
env_audit_missing_items = list(env_payload.get("missing_items") or [])
|
||||
env_audit_recommended_actions = [str(item).strip() for item in list(env_payload.get("recommended_actions") or []) if str(item).strip()]
|
||||
env_audit_runtime_may_need_restart = bool(((env_payload.get("runtime") or {}).get("runtime_may_need_restart", False)))
|
||||
env_audit_blocking = env_audit_status == "blocked"
|
||||
env_audit_attention = env_audit_status == "attention"
|
||||
|
||||
go_live_summary_payload = load_json_artifact("go_live_summary")
|
||||
stack_diagnosis_payload = load_json_artifact("stack_diagnosis")
|
||||
driver_feed_payload = load_json_artifact("driver_feed")
|
||||
codex_brief_payload = load_json_artifact("codex_brief")
|
||||
|
||||
go_live_launchpad_target_node_code = str(go_live_summary_payload.get("launchpad_recommended_target_node_code") or "").strip()
|
||||
go_live_launchpad_recovery_label = str(go_live_summary_payload.get("launchpad_recommended_recovery_label") or "").strip()
|
||||
go_live_launchpad_recovery_summary = str(go_live_summary_payload.get("launchpad_recommended_recovery_summary") or "").strip()
|
||||
go_live_launchpad_bootstrap_pending_nodes = int(go_live_summary_payload.get("launchpad_onboarding_bootstrap_pending_nodes", 0) or 0)
|
||||
go_live_launchpad_acceptance_ready_nodes = int(go_live_summary_payload.get("launchpad_onboarding_acceptance_ready_nodes", 0) or 0)
|
||||
stack_diagnosis = stack_diagnosis_payload.get("diagnosis") or {}
|
||||
stack_launchpad_target_node_code = str(stack_diagnosis.get("launchpad_recommended_target_node_code") or "").strip()
|
||||
stack_launchpad_recovery_label = str(stack_diagnosis.get("launchpad_recommended_recovery_label") or "").strip()
|
||||
stack_launchpad_recovery_summary = str(stack_diagnosis.get("launchpad_recommended_recovery_summary") or "").strip()
|
||||
stack_launchpad_bootstrap_pending_nodes = int(stack_diagnosis.get("launchpad_onboarding_bootstrap_pending_nodes", 0) or 0)
|
||||
stack_launchpad_acceptance_ready_nodes = int(stack_diagnosis.get("launchpad_onboarding_acceptance_ready_nodes", 0) or 0)
|
||||
driver_feed_summary = driver_feed_payload.get("summary") or {}
|
||||
codex_brief_summary = codex_brief_payload.get("summary") or {}
|
||||
|
||||
|
||||
def nonempty_distinct(values: dict[str, str]) -> list[str]:
|
||||
return sorted({str(value or "").strip() for value in values.values() if str(value or "").strip()})
|
||||
|
||||
|
||||
def distinct_ints(values: dict[str, int], available_sources: set[str]) -> list[int]:
|
||||
return sorted({int(values[source]) for source in values if source in available_sources})
|
||||
|
||||
|
||||
launchpad_target_node_codes = {
|
||||
"go_live_summary": go_live_launchpad_target_node_code,
|
||||
"stack_diagnosis": stack_launchpad_target_node_code,
|
||||
"driver_feed": str(driver_feed_summary.get("launchpad_recommended_target_node_code") or "").strip(),
|
||||
"codex_brief": str(codex_brief_summary.get("launchpad_recommended_target_node_code") or "").strip(),
|
||||
}
|
||||
launchpad_recovery_labels = {
|
||||
"go_live_summary": go_live_launchpad_recovery_label,
|
||||
"stack_diagnosis": stack_launchpad_recovery_label,
|
||||
"driver_feed": str(driver_feed_summary.get("launchpad_recommended_recovery_label") or "").strip(),
|
||||
"codex_brief": str(codex_brief_summary.get("launchpad_recommended_recovery_label") or "").strip(),
|
||||
}
|
||||
launchpad_recovery_summaries = {
|
||||
"go_live_summary": go_live_launchpad_recovery_summary,
|
||||
"stack_diagnosis": stack_launchpad_recovery_summary,
|
||||
"driver_feed": str(driver_feed_summary.get("launchpad_recommended_recovery_summary") or "").strip(),
|
||||
"codex_brief": str(codex_brief_summary.get("launchpad_recommended_recovery_summary") or "").strip(),
|
||||
}
|
||||
launchpad_bootstrap_pending_nodes = {
|
||||
"go_live_summary": go_live_launchpad_bootstrap_pending_nodes,
|
||||
"stack_diagnosis": stack_launchpad_bootstrap_pending_nodes,
|
||||
"driver_feed": int(driver_feed_summary.get("launchpad_onboarding_bootstrap_pending_nodes", 0) or 0),
|
||||
"codex_brief": int(codex_brief_summary.get("launchpad_onboarding_bootstrap_pending_nodes", 0) or 0),
|
||||
}
|
||||
launchpad_acceptance_ready_nodes = {
|
||||
"go_live_summary": go_live_launchpad_acceptance_ready_nodes,
|
||||
"stack_diagnosis": stack_launchpad_acceptance_ready_nodes,
|
||||
"driver_feed": int(driver_feed_summary.get("launchpad_onboarding_acceptance_ready_nodes", 0) or 0),
|
||||
"codex_brief": int(codex_brief_summary.get("launchpad_onboarding_acceptance_ready_nodes", 0) or 0),
|
||||
}
|
||||
|
||||
available_alignment_sources = {
|
||||
source
|
||||
for source, payload_value in {
|
||||
"go_live_summary": go_live_summary_payload,
|
||||
"stack_diagnosis": stack_diagnosis_payload,
|
||||
"driver_feed": driver_feed_payload,
|
||||
"codex_brief": codex_brief_payload,
|
||||
}.items()
|
||||
if isinstance(payload_value, dict) and payload_value
|
||||
}
|
||||
|
||||
launchpad_target_consistent = len(nonempty_distinct({
|
||||
source: value for source, value in launchpad_target_node_codes.items() if source in available_alignment_sources
|
||||
})) <= 1
|
||||
launchpad_recovery_label_consistent = len(nonempty_distinct({
|
||||
source: value for source, value in launchpad_recovery_labels.items() if source in available_alignment_sources
|
||||
})) <= 1
|
||||
launchpad_recovery_summary_consistent = len(nonempty_distinct({
|
||||
source: value for source, value in launchpad_recovery_summaries.items() if source in available_alignment_sources
|
||||
})) <= 1
|
||||
launchpad_bootstrap_pending_consistent = len(distinct_ints(launchpad_bootstrap_pending_nodes, available_alignment_sources)) <= 1
|
||||
launchpad_acceptance_ready_consistent = len(distinct_ints(launchpad_acceptance_ready_nodes, available_alignment_sources)) <= 1
|
||||
launchpad_alignment_consistent = all([
|
||||
launchpad_target_consistent,
|
||||
launchpad_recovery_label_consistent,
|
||||
launchpad_recovery_summary_consistent,
|
||||
launchpad_bootstrap_pending_consistent,
|
||||
launchpad_acceptance_ready_consistent,
|
||||
])
|
||||
|
||||
node_agent_env_missing = any(
|
||||
str(item).strip() == "missing_env:/etc/default/domaincheck-node-agent"
|
||||
for item in env_audit_missing_items
|
||||
)
|
||||
|
||||
if critical_failures or env_audit_blocking:
|
||||
status = "blocked"
|
||||
if critical_failures and env_audit_blocking:
|
||||
headline = "关键报告存在缺失,且环境审计已给出阻断结论,当前不适合上线。"
|
||||
elif critical_failures:
|
||||
headline = "上线证据包仍缺关键报告,暂不建议直接宣称收口完成。"
|
||||
else:
|
||||
headline = "环境审计已给出阻断结论,需先修复现场环境后再进入发布门禁。"
|
||||
elif noncritical_failures or env_audit_attention or not launchpad_alignment_consistent:
|
||||
status = "attention"
|
||||
if not launchpad_alignment_consistent:
|
||||
headline = "核心报告已齐,但 launchpad 摘要在不同 surface 之间存在漂移,建议先复核再发版。"
|
||||
elif noncritical_failures and env_audit_attention:
|
||||
headline = "核心报告已齐,但补充报告和环境审计都提示仍有缺口,建议先复核再发版。"
|
||||
elif noncritical_failures:
|
||||
headline = "核心报告已齐,但仍有补充报告超时或失败,建议先复核再发版。"
|
||||
else:
|
||||
headline = "核心报告已齐,但环境审计仍提示基础缺口,建议补齐后再发版。"
|
||||
else:
|
||||
status = "ready"
|
||||
headline = "核心上线证据已经齐备,可进入最终人工复核或正式发布门禁。"
|
||||
|
||||
result = {
|
||||
"report_dir": str(payload.get("report_dir") or manifest_path.parent),
|
||||
"status": status,
|
||||
"headline": headline,
|
||||
"artifact_total": int(summary.get("artifact_total", len(artifacts)) or len(artifacts)),
|
||||
"failure_total": int(summary.get("failure_total", len(failures)) or len(failures)),
|
||||
"critical_failures": critical_failures,
|
||||
"noncritical_failures": noncritical_failures,
|
||||
"env_audit": {
|
||||
"ok": bool(env_audit_artifact.get("ok", False)),
|
||||
"status": env_audit_status,
|
||||
"headline": env_audit_headline,
|
||||
"missing_items": env_audit_missing_items,
|
||||
"runtime_may_need_restart": env_audit_runtime_may_need_restart,
|
||||
"recommended_actions": env_audit_recommended_actions,
|
||||
},
|
||||
"go_live_summary": {
|
||||
"go_live_status": str(go_live_summary_payload.get("go_live_status") or "").strip(),
|
||||
"publish_status": str(go_live_summary_payload.get("publish_status") or "").strip(),
|
||||
"operator_title": str(go_live_summary_payload.get("operator_title") or "").strip(),
|
||||
"next_step_action_code": str(go_live_summary_payload.get("next_step_action_code") or "").strip(),
|
||||
"launchpad_recommended_target_node_code": go_live_launchpad_target_node_code,
|
||||
"launchpad_recommended_recovery_label": go_live_launchpad_recovery_label,
|
||||
"launchpad_recommended_recovery_summary": go_live_launchpad_recovery_summary,
|
||||
"launchpad_onboarding_bootstrap_pending_nodes": go_live_launchpad_bootstrap_pending_nodes,
|
||||
"launchpad_onboarding_acceptance_ready_nodes": go_live_launchpad_acceptance_ready_nodes,
|
||||
},
|
||||
"stack_diagnosis": {
|
||||
"stack_status": str((stack_diagnosis_payload.get("diagnosis") or {}).get("stack_status") or "").strip(),
|
||||
"operator_title": str(((stack_diagnosis_payload.get("diagnosis") or {}).get("operator_decision") or {}).get("title") or "").strip(),
|
||||
"next_step_action_code": str(((stack_diagnosis_payload.get("diagnosis") or {}).get("next_step") or {}).get("action_code") or "").strip(),
|
||||
"launchpad_recommended_target_node_code": stack_launchpad_target_node_code,
|
||||
"launchpad_recommended_recovery_label": stack_launchpad_recovery_label,
|
||||
"launchpad_recommended_recovery_summary": stack_launchpad_recovery_summary,
|
||||
"launchpad_onboarding_bootstrap_pending_nodes": stack_launchpad_bootstrap_pending_nodes,
|
||||
"launchpad_onboarding_acceptance_ready_nodes": stack_launchpad_acceptance_ready_nodes,
|
||||
},
|
||||
"driver_feed": {
|
||||
"headline": str(driver_feed_payload.get("headline") or "").strip(),
|
||||
"launch_status": str(((driver_feed_payload.get("automation_coverage") or {}).get("launch_status") or "")).strip(),
|
||||
"launch_ready": bool(((driver_feed_payload.get("automation_coverage") or {}).get("launch_ready", False))),
|
||||
"launchpad_recommended_target_node_code": str(
|
||||
((driver_feed_payload.get("summary") or {}).get("launchpad_recommended_target_node_code") or "")
|
||||
).strip(),
|
||||
"launchpad_recommended_recovery_label": str(
|
||||
((driver_feed_payload.get("summary") or {}).get("launchpad_recommended_recovery_label") or "")
|
||||
).strip(),
|
||||
},
|
||||
"codex_brief": {
|
||||
"headline": str(codex_brief_payload.get("headline") or "").strip(),
|
||||
"focus_entry_key": str(((codex_brief_payload.get("focus") or {}).get("entry_key") or "")).strip(),
|
||||
"launch_status": str(((codex_brief_payload.get("automation_coverage") or {}).get("launch_status") or "")).strip(),
|
||||
"launch_ready": bool(((codex_brief_payload.get("automation_coverage") or {}).get("launch_ready", False))),
|
||||
"launchpad_recommended_target_node_code": str(
|
||||
((codex_brief_payload.get("summary") or {}).get("launchpad_recommended_target_node_code") or "")
|
||||
).strip(),
|
||||
"launchpad_recommended_recovery_label": str(
|
||||
((codex_brief_payload.get("summary") or {}).get("launchpad_recommended_recovery_label") or "")
|
||||
).strip(),
|
||||
},
|
||||
"launchpad_alignment": {
|
||||
"consistent": launchpad_alignment_consistent,
|
||||
"target_node_code_consistent": launchpad_target_consistent,
|
||||
"recovery_label_consistent": launchpad_recovery_label_consistent,
|
||||
"recovery_summary_consistent": launchpad_recovery_summary_consistent,
|
||||
"bootstrap_pending_consistent": launchpad_bootstrap_pending_consistent,
|
||||
"acceptance_ready_consistent": launchpad_acceptance_ready_consistent,
|
||||
"available_sources": sorted(available_alignment_sources),
|
||||
"target_node_codes": launchpad_target_node_codes,
|
||||
"recovery_labels": launchpad_recovery_labels,
|
||||
"recovery_summaries": launchpad_recovery_summaries,
|
||||
"bootstrap_pending_nodes": launchpad_bootstrap_pending_nodes,
|
||||
"acceptance_ready_nodes": launchpad_acceptance_ready_nodes,
|
||||
},
|
||||
"recommended_reading_order": list(summary.get("recommended_reading_order") or []),
|
||||
"recommended_next_steps": [
|
||||
"先看 00_env_audit.txt,确认当前机器环境是否存在阻断或注意项",
|
||||
"先看 02_go_live_summary.json 和 03_stack_diagnosis.json",
|
||||
"再看 04_driver_feed.json 和 05_codex_brief.json",
|
||||
"若涉及发布门禁,再看 06_release_launchpad.json",
|
||||
"若 manifest 里仍有失败项,再看对应 artifact 原文件",
|
||||
]
|
||||
+ (
|
||||
[
|
||||
"当前缺口指向 Node Agent 未接管:先执行 agent-gap-export,拿到首个缺口节点的接管包",
|
||||
"随后执行 node-bootstrap-plan,为缺口节点生成可直接落地的接管脚本",
|
||||
"如果仓库代码已更新但 bootstrap-plan 输出仍偏旧,先重启控制面 API 再重新导出接管计划",
|
||||
]
|
||||
if node_agent_env_missing
|
||||
else []
|
||||
)
|
||||
+ (
|
||||
[
|
||||
"env-audit 已识别运行中 API 可能仍是旧代码:先执行 drive_ops_center.sh runtime-refresh-recover,按固定链路完成 API 重启与复检",
|
||||
"若仍需人工逐条确认,再执行 recommended_actions 里的 API 重启与复检命令"
|
||||
]
|
||||
if env_audit_runtime_may_need_restart
|
||||
else []
|
||||
)
|
||||
+ (
|
||||
[
|
||||
f"当前 launchpad 缺口已经收敛到节点 {go_live_launchpad_target_node_code},建议优先执行:{go_live_launchpad_recovery_label or '节点恢复'}",
|
||||
"先对照 02_go_live_summary.json、04_driver_feed.json、05_codex_brief.json 中的 launchpad 字段,确认三处摘要是否一致",
|
||||
(go_live_launchpad_recovery_summary or "如果 recovery_summary 仍为空,再回到 06_release_launchpad.json 查看 gap rows 详情"),
|
||||
]
|
||||
if go_live_launchpad_target_node_code
|
||||
else []
|
||||
)
|
||||
+ (
|
||||
[
|
||||
"当前 bundle 内的 launchpad 摘要在 go_live_summary / stack_diagnosis / driver_feed / codex_brief 之间存在不一致,先不要直接交付。",
|
||||
"优先复核 02_go_live_summary.json 与 03_stack_diagnosis.json,再检查 04_driver_feed.json 与 05_codex_brief.json 是否使用了同一版控制面数据。",
|
||||
"若只有局部摘要偏旧,先执行 drive_ops_center.sh runtime-refresh-recover,并重新导出 go-live bundle。",
|
||||
]
|
||||
if not launchpad_alignment_consistent
|
||||
else []
|
||||
),
|
||||
}
|
||||
|
||||
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||
PY
|
||||
@@ -0,0 +1,15 @@
|
||||
OPS_CONTROL_PLANE_BASE_URL=http://127.0.0.1:8100
|
||||
OPS_AGENT_TOKEN=
|
||||
OPS_AGENT_POLL_INTERVAL_SECONDS=5
|
||||
|
||||
NODE_CODE=mainland-worker-01
|
||||
NODE_REGION=mainland
|
||||
NODE_ROLE=worker
|
||||
|
||||
WORKER_SERVICE_NAME=domaincheck-worker
|
||||
API_SERVICE_NAME=domaincheck-api
|
||||
SYNC_AGENT_SERVICE_NAME=domaincheck-sync-agent
|
||||
NODE_AGENT_SERVICE_NAME=domaincheck-node-agent
|
||||
|
||||
OPS_AGENT_CAPABILITIES=["service.start","service.stop","service.restart","service.status","runtime.start_worker","runtime.stop_worker","runtime.restart_api","runtime.start_sync_agent","runtime.stop_sync_agent","health.snapshot","logs.collect","diagnostics.collect","deploy.release"]
|
||||
OPS_AGENT_LABELS={}
|
||||
@@ -0,0 +1,49 @@
|
||||
# Overseas control-plane unified entrypoint config
|
||||
#
|
||||
# Recommended target path:
|
||||
# /etc/default/domaincheck-ops-center
|
||||
#
|
||||
# Usage:
|
||||
# export DOMAINCHECK_OPS_CENTER_ENV=/etc/default/domaincheck-ops-center
|
||||
# bash domain-api/deploy/multi-region/drive_ops_center.sh doctor
|
||||
|
||||
# Active profile name
|
||||
OPS_ACTIVE_PROFILE=default
|
||||
|
||||
# Optional profile list shown by `drive_ops_center.sh config`
|
||||
# OPS_PROFILE_NAMES=default,prod,staging
|
||||
|
||||
# Mainland controller API base URL
|
||||
OPS_MAINLAND_API_BASE_URL=http://127.0.0.1:8100
|
||||
|
||||
# Overseas control-plane API base URL
|
||||
OPS_OVERSEAS_API_BASE_URL=http://127.0.0.1:8100
|
||||
|
||||
# Public control-plane base URL used in generated node-agent bootstrap plans
|
||||
OPS_DEFAULT_CONTROL_PLANE_BASE_URL=https://api.example.com
|
||||
|
||||
# Default request identity used by CLI wrappers
|
||||
OPS_DEFAULT_REQUESTED_BY=cli/ops-center
|
||||
|
||||
# Default metadata used when generating node-agent bootstrap plans
|
||||
OPS_DEFAULT_ISSUED_BY=ops-center
|
||||
OPS_DEFAULT_EXPIRES_IN_HOURS=72
|
||||
OPS_DEFAULT_ROOT_DIR=/opt/domaincheck
|
||||
|
||||
# Optional report export root
|
||||
# OPS_REPORT_ROOT=/opt/domaincheck/domain-api/runtime/ops-center-reports
|
||||
|
||||
# Local node-agent status check defaults
|
||||
OPS_NODE_AGENT_SERVICE_NAME=domaincheck-node-agent
|
||||
OPS_NODE_AGENT_ENV_FILE=/etc/default/domaincheck-node-agent
|
||||
|
||||
# Optional per-profile overrides:
|
||||
# OPS_PROFILE_PROD_MAINLAND_API_BASE_URL=http://10.0.0.10:8100
|
||||
# OPS_PROFILE_PROD_OVERSEAS_API_BASE_URL=https://api.example.com
|
||||
# OPS_PROFILE_PROD_DEFAULT_CONTROL_PLANE_BASE_URL=https://api.example.com
|
||||
# OPS_PROFILE_PROD_DEFAULT_REQUESTED_BY=cli/ops-center-prod
|
||||
# OPS_PROFILE_PROD_DEFAULT_ISSUED_BY=ops-center-prod
|
||||
# OPS_PROFILE_PROD_DEFAULT_EXPIRES_IN_HOURS=72
|
||||
# OPS_PROFILE_PROD_DEFAULT_ROOT_DIR=/opt/domaincheck
|
||||
# OPS_PROFILE_PROD_NODE_AGENT_SERVICE_NAME=domaincheck-node-agent
|
||||
# OPS_PROFILE_PROD_NODE_AGENT_ENV_FILE=/etc/default/domaincheck-node-agent
|
||||
17
domain-api/deploy/systemd/domain-node-agent.service
Normal file
17
domain-api/deploy/systemd/domain-node-agent.service
Normal file
@@ -0,0 +1,17 @@
|
||||
[Unit]
|
||||
Description=domainCheck Node Agent
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=/opt/domaincheck/domain-api
|
||||
EnvironmentFile=-/etc/default/domaincheck-node-agent
|
||||
Environment="PATH=/home/www/.nvm/versions/node/v20.20.2/bin:/home/www/.local/bin:/home/www/bin:/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin"
|
||||
ExecStart=/opt/domaincheck/domainCheck/.venv/bin/python -m app.node_agent
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
User=www
|
||||
Group=www
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
Reference in New Issue
Block a user