hy-app-monitor/core/config.py

410 lines
19 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from __future__ import annotations
import json
import os
from pathlib import Path
from typing import Any
from .env import env_float, env_int, env_int_list, env_str, load_env_file
# 项目根目录。
ROOT = Path(__file__).resolve().parents[1]
# 运行期产物目录。
RUNTIME_DIR = ROOT / "run"
# 常见工作区根目录,便于本地开发时自动发现业务仓。
WORKSPACE_ROOT = ROOT.parent
# 静态资源目录。
STATIC_DIR = ROOT / "static"
# 默认 .env 路径。
DEFAULT_ENV_PATH = ROOT / ".env"
# 先加载项目根目录的 .env。
load_env_file(Path(env_str("APP_ENV_FILE", str(DEFAULT_ENV_PATH))).expanduser())
# 监控目标配置文件路径。
CONFIG_PATH = Path(env_str("TARGETS_FILE", str(ROOT / "config" / "targets.json"))).expanduser()
# HTTP 服务监听地址。
APP_BIND = env_str("APP_BIND", "0.0.0.0") or "0.0.0.0"
# HTTP 服务监听端口。
APP_PORT = env_int("APP_PORT", 2026)
# 当前 HTTP 应用挂载前缀原始值。
RAW_APP_BASE_PATH = env_str("APP_BASE_PATH", "").strip()
# 当前 HTTP 应用挂载前缀;空串表示直接挂在根路径。
APP_BASE_PATH = "/" + RAW_APP_BASE_PATH.strip("/") if RAW_APP_BASE_PATH else ""
# Yumi 内部实例固定走本机回环地址。
YUMI_INTERNAL_HOST = env_str("YUMI_INTERNAL_HOST", "127.0.0.1") or "127.0.0.1"
# Yumi 内部实例监听端口。
YUMI_INTERNAL_PORT = env_int("YUMI_INTERNAL_PORT", 2027)
# HaiYi 内部实例固定走本机回环地址。
HAIYI_INTERNAL_HOST = env_str("HAIYI_INTERNAL_HOST", "127.0.0.1") or "127.0.0.1"
# HaiYi 内部实例监听端口。
HAIYI_INTERNAL_PORT = env_int("HAIYI_INTERNAL_PORT", 2028)
# 网关反代到内部实例时统一带这一段共享密钥。
INTERNAL_PROXY_SHARED_SECRET = env_str("INTERNAL_PROXY_SHARED_SECRET", "hy-app-monitor-internal")
# 网关反代内部实例时的统一超时秒数。
GATEWAY_PROXY_TIMEOUT_SECONDS = env_int("GATEWAY_PROXY_TIMEOUT_SECONDS", 600)
# 本地账号库统一落到 run 目录,避免写入版本库。
AUTH_DB_PATH = Path(env_str("AUTH_DB_PATH", str(RUNTIME_DIR / "auth.sqlite3"))).expanduser()
# 会话 Cookie 名称固定成单值,前后端都按这个名字取。
AUTH_SESSION_COOKIE_NAME = env_str("AUTH_SESSION_COOKIE_NAME", "hy_app_monitor_session") or "hy_app_monitor_session"
# 简单本地账号默认保活 24 小时,过期后重新登录。
AUTH_SESSION_TTL_HOURS = env_int("AUTH_SESSION_TTL_HOURS", 24)
# 部署在 HTTPS 反代后时,可通过 .env 把 Cookie 切到 Secure。
AUTH_COOKIE_SECURE = env_str("AUTH_COOKIE_SECURE", "0").lower() in {"1", "true", "yes", "on"}
# 服务健康检查超时时间。
PROBE_TIMEOUT_SECONDS = env_float("PROBE_TIMEOUT_SECONDS", 3.0)
# 服务健康检查缓存秒数。
PROBE_CACHE_TTL_SECONDS = env_float("PROBE_CACHE_TTL_SECONDS", 5.0)
# 服务健康检查并发数。
PROBE_MAX_WORKERS = env_int("PROBE_MAX_WORKERS", 12)
# 主机指标缓存秒数。
HOST_METRIC_CACHE_TTL_SECONDS = env_float("HOST_METRIC_CACHE_TTL_SECONDS", 25.0)
# 主机指标远端采集超时时间;兼容旧的 TAT_* 变量名。
HOST_METRIC_TIMEOUT_SECONDS = env_int("HOST_METRIC_TIMEOUT_SECONDS", env_int("TAT_METRIC_TIMEOUT_SECONDS", 30))
# 主机指标 CPU 采样间隔;兼容旧的 TAT_* 变量名。
HOST_METRIC_CPU_SAMPLE_SECONDS = env_float("HOST_METRIC_CPU_SAMPLE_SECONDS", env_float("TAT_CPU_SAMPLE_SECONDS", 1.0))
# 兼容旧代码里仍在引用的变量名。
TAT_METRIC_TIMEOUT_SECONDS = HOST_METRIC_TIMEOUT_SECONDS
TAT_CPU_SAMPLE_SECONDS = HOST_METRIC_CPU_SAMPLE_SECONDS
# 业务运行目录根路径。
RUNTIME_BASE_DIR = env_str("RUNTIME_BASE_DIR", "/opt/likei")
# 远端宿主机默认 SSH 用户。
SSH_USER = env_str("SSH_USER", "root") or "root"
# 非 root 用户时,允许统一在远端命令前补 sudo -n。
SSH_USE_SUDO = env_str("SSH_USE_SUDO", "0").lower() in {"1", "true", "yes", "on"}
# 远端宿主机默认 SSH 端口。
SSH_PORT = env_int("SSH_PORT", 22)
# SSH 建连超时秒数。
SSH_CONNECT_TIMEOUT_SECONDS = env_int("SSH_CONNECT_TIMEOUT_SECONDS", 10)
# SSH 严格主机检查策略,默认首次连接自动接收。
SSH_STRICT_HOST_KEY_CHECKING = env_str("SSH_STRICT_HOST_KEY_CHECKING", "accept-new") or "accept-new"
# 运行期默认使用 deploy 机项目目录下的专用私钥。
SSH_PRIVATE_KEY_PATH = Path(env_str("SSH_PRIVATE_KEY_PATH", str(ROOT / "run" / "ssh" / "id_ed25519"))).expanduser()
# 已知主机文件同样放到项目 run 目录,避免污染系统级 known_hosts。
SSH_KNOWN_HOSTS_PATH = Path(
env_str("SSH_KNOWN_HOSTS_PATH", str(ROOT / "run" / "ssh" / "known_hosts"))
).expanduser()
# 滚动重启单实例最大等待秒数。
ROLLING_RESTART_TIMEOUT_SECONDS = env_int("ROLLING_RESTART_TIMEOUT_SECONDS", 180)
# 滚动重启探测轮询间隔。
ROLLING_RESTART_POLL_SECONDS = env_float("ROLLING_RESTART_POLL_SECONDS", 2.0)
# 单实例恢复后额外稳定等待秒数。
ROLLING_RESTART_STABILIZE_SECONDS = env_float("ROLLING_RESTART_STABILIZE_SECONDS", 2.0)
# 日志查看固定读取最近多少条。
SERVICE_LOG_PREVIEW_LINES = env_int("SERVICE_LOG_PREVIEW_LINES", 1000)
# 日志导出最大允许条数,避免单次下载打爆页面进程。
SERVICE_LOG_EXPORT_MAX_LINES = env_int("SERVICE_LOG_EXPORT_MAX_LINES", 10000)
# 服务更新时本地构建最长等待秒数。
SERVICE_BUILD_TIMEOUT_SECONDS = env_int("SERVICE_BUILD_TIMEOUT_SECONDS", 3600)
# 服务更新时目标机预拉镜像最长等待秒数。
SERVICE_PULL_TIMEOUT_SECONDS = env_int("SERVICE_PULL_TIMEOUT_SECONDS", 600)
# 固定构建工作区根目录,留空时按本地 / deploy 机路径自动推导。
SERVICE_BUILD_CACHE_ROOT = env_str("SERVICE_BUILD_CACHE_ROOT", "")
# Java Maven 并行线程,默认按 1 个 CPU 组装 reactor。
JAVA_MAVEN_THREADS = env_str("JAVA_MAVEN_THREADS", "1C")
# Java 镜像 build/push 最大并行数。
JAVA_IMAGE_BUILD_MAX_WORKERS = env_int("JAVA_IMAGE_BUILD_MAX_WORKERS", 3)
# 腾讯云区域SSH 引导和带外脚本会复用。
DEPLOY_REGION = env_str("DEPLOY_REGION", "me-saudi-arabia")
# 腾讯云 SecretId。
TENCENT_SECRET_ID = env_str("TENCENT_SECRET_ID", "")
# 腾讯云 SecretKey。
TENCENT_SECRET_KEY = env_str("TENCENT_SECRET_KEY", "")
def pick_existing_path(candidates: list[Path]) -> Path:
# 配置未显式指定时,优先挑当前机器上真正存在的路径。
for candidate in candidates:
if candidate.exists():
return candidate
return candidates[0]
def env_or_candidate_path(env_key: str, candidates: list[Path]) -> Path:
# 允许 .env 显式覆盖,也兼容本地开发和远端 deploy 两种目录布局。
override = os.environ.get(env_key, "").strip()
if override:
return Path(override).expanduser()
return pick_existing_path(candidates)
def truthy(value: Any, default: bool = False) -> bool:
# 配置里布尔值可能来自 JSON 布尔或字符串,统一在这里规范化。
if value is None:
return default
if isinstance(value, bool):
return value
if isinstance(value, str):
return value.strip().lower() in {"1", "true", "yes", "on"}
return bool(value)
# 页面默认刷新间隔。
DEFAULT_REFRESH_INTERVAL_SECONDS = env_int("DEFAULT_REFRESH_INTERVAL_SECONDS", 10)
# 页面可选刷新间隔。
REFRESH_INTERVAL_OPTIONS = env_int_list("REFRESH_INTERVAL_OPTIONS", [5, 10, 15, 30, 60])
# Nacos API 基础地址。
NACOS_BASE_URL = env_str("NACOS_BASE_URL", "http://10.2.1.17:8848").removesuffix("/nacos").rstrip("/")
# Nacos 目标命名空间 ID。
NACOS_NAMESPACE_ID = env_str("NACOS_NAMESPACE_ID", "f7a8594e-090c-4830-a3ad-b5c0f9afcc07")
# Nacos 目标命名空间名。
NACOS_NAMESPACE_NAME = env_str("NACOS_NAMESPACE_NAME", "RedCircleServiceProd")
# Nacos 默认注册分组。
NACOS_DISCOVERY_GROUP = env_str("NACOS_DISCOVERY_GROUP", "DEFAULT_GROUP")
# Nacos 默认注册集群。
NACOS_DISCOVERY_CLUSTER_NAME = env_str("NACOS_DISCOVERY_CLUSTER_NAME", "DEFAULT")
# Nacos 请求超时。
NACOS_TIMEOUT_SECONDS = env_float("NACOS_TIMEOUT_SECONDS", 6.0)
# Nacos 结果缓存秒数。
NACOS_CACHE_TTL_SECONDS = env_float("NACOS_CACHE_TTL_SECONDS", 15.0)
# Nacos service list 单页大小。
NACOS_PAGE_SIZE = env_int("NACOS_PAGE_SIZE", 200)
# Nacos 可选用户名。
NACOS_USERNAME = env_str("NACOS_USERNAME", "")
# Nacos 可选密码。
NACOS_PASSWORD = env_str("NACOS_PASSWORD", "")
# Harbor 镜像仓地址,服务更新会推送镜像并让目标机主动拉取。
HARBOR_REGISTRY = env_str("HARBOR_REGISTRY", "10.2.1.3:18082")
# Harbor 项目名。
HARBOR_PROJECT = env_str("HARBOR_PROJECT", "likei")
# Harbor 登录用户名。
HARBOR_USERNAME = env_str("HARBOR_USERNAME", "")
# Harbor 登录密码。
HARBOR_PASSWORD = env_str("HARBOR_PASSWORD", "")
# Docker 构建平台,和现有 Jenkins/构建脚本口径保持一致。
BUILD_DOCKER_PLATFORM = env_str("BUILD_DOCKER_PLATFORM", "linux/amd64")
# Java 服务默认拉取的 Git ref。
UPDATE_DEFAULT_JAVA_GIT_REF = env_str("UPDATE_DEFAULT_JAVA_GIT_REF", "main")
# Go 服务默认拉取的 Git ref。
UPDATE_DEFAULT_GO_GIT_REF = env_str("UPDATE_DEFAULT_GO_GIT_REF", "main")
# Admin 前端默认拉取的 Git ref。
UPDATE_DEFAULT_ADMIN_GIT_REF = env_str("UPDATE_DEFAULT_ADMIN_GIT_REF", "main")
# Java 仓库路径,默认兼容本地工作区和远端 deploy 机。
JAVA_REPO_ROOT = env_or_candidate_path(
"JAVA_REPO_ROOT",
[
WORKSPACE_ROOT / "chatapp3" / "chatapp3-java",
Path("/opt/deploy/sources/chatapp3-java"),
],
)
# Go 仓库路径,默认兼容本地工作区和远端 deploy 机。
GOLANG_REPO_ROOT = env_or_candidate_path(
"GOLANG_REPO_ROOT",
[
WORKSPACE_ROOT / "chatapp3" / "chatapp3-golang",
Path("/opt/deploy/sources/chatapp3-golang"),
],
)
# Admin 前端仓库地址;仓库缺失时允许按这里自动 clone。
ADMIN_FRONTEND_REPO_URL = env_str("ADMIN_FRONTEND_REPO_URL", "")
# Admin 前端仓库路径,默认兼容本地工作区和远端 deploy 机。
ADMIN_FRONTEND_REPO_ROOT = env_or_candidate_path(
"ADMIN_FRONTEND_REPO_ROOT",
[
WORKSPACE_ROOT / "chatapp3" / "chatapp3-admin",
Path("/opt/deploy/sources/chatapp3-admin"),
],
)
# Admin 前端在发布页里的服务名。
ADMIN_FRONTEND_SERVICE_NAME = env_str("ADMIN_FRONTEND_SERVICE_NAME", "admin").strip() or "admin"
# Admin 前端是否启用;未显式指定时,只要仓库和目标机信息齐备就自动开启。
ADMIN_FRONTEND_HOST = env_str("ADMIN_FRONTEND_HOST", "")
# Admin 前端目标机私网 IP。
ADMIN_FRONTEND_IP = env_str("ADMIN_FRONTEND_IP", "")
# Admin 前端目标机实例 ID主要用于 TAT 引导 SSH。
ADMIN_FRONTEND_INSTANCE_ID = env_str("ADMIN_FRONTEND_INSTANCE_ID", "")
# Admin 前端 SSH 主机,默认沿用私网 IP。
ADMIN_FRONTEND_SSH_HOST = env_str("ADMIN_FRONTEND_SSH_HOST", ADMIN_FRONTEND_IP).strip() or ADMIN_FRONTEND_IP
# Admin 前端 SSH 用户。
ADMIN_FRONTEND_SSH_USER = env_str("ADMIN_FRONTEND_SSH_USER", SSH_USER).strip() or SSH_USER
# Admin 前端 SSH 端口。
ADMIN_FRONTEND_SSH_PORT = env_int("ADMIN_FRONTEND_SSH_PORT", SSH_PORT)
# Admin 前端是否需要 sudo。
ADMIN_FRONTEND_SSH_USE_SUDO = env_str("ADMIN_FRONTEND_SSH_USE_SUDO", "0").lower() in {"1", "true", "yes", "on"}
# Admin 前端远端运行目录。
ADMIN_FRONTEND_REMOTE_DIR = env_str("ADMIN_FRONTEND_REMOTE_DIR", "/opt/yumi-admin/frontend-admin").strip() or "/opt/yumi-admin/frontend-admin"
# Admin 前端静态站点目录。
ADMIN_FRONTEND_SITE_DIR = (
env_str("ADMIN_FRONTEND_SITE_DIR", f"{ADMIN_FRONTEND_REMOTE_DIR.rstrip('/')}/site").strip()
or f"{ADMIN_FRONTEND_REMOTE_DIR.rstrip('/')}/site"
)
# Admin 前端 compose 文件路径。
ADMIN_FRONTEND_COMPOSE_FILE = (
env_str("ADMIN_FRONTEND_COMPOSE_FILE", f"{ADMIN_FRONTEND_REMOTE_DIR.rstrip('/')}/docker-compose.yml").strip()
or f"{ADMIN_FRONTEND_REMOTE_DIR.rstrip('/')}/docker-compose.yml"
)
# Admin 前端 compose 服务名。
ADMIN_FRONTEND_COMPOSE_SERVICE = env_str("ADMIN_FRONTEND_COMPOSE_SERVICE", "caddy").strip() or "caddy"
# Admin 前端当前发布元数据文件路径。
ADMIN_FRONTEND_RELEASE_META_PATH = (
env_str(
"ADMIN_FRONTEND_RELEASE_META_PATH",
f"{ADMIN_FRONTEND_REMOTE_DIR.rstrip('/')}/.hy-app-monitor-admin-release.json",
).strip()
or f"{ADMIN_FRONTEND_REMOTE_DIR.rstrip('/')}/.hy-app-monitor-admin-release.json"
)
# Admin 前端构建脚本。
ADMIN_FRONTEND_BUILD_SCRIPT = env_str("ADMIN_FRONTEND_BUILD_SCRIPT", "build:antdv-next").strip() or "build:antdv-next"
# Admin 前端构建产物目录。
ADMIN_FRONTEND_DIST_DIR = env_str("ADMIN_FRONTEND_DIST_DIR", "apps/dist").strip() or "apps/dist"
# Admin 前端对外访问地址,仅用于结果展示和排查。
ADMIN_FRONTEND_PUBLIC_URL = env_str("ADMIN_FRONTEND_PUBLIC_URL", "").strip()
# 未显式指定 ADMIN_FRONTEND_ENABLED 时,只要关键参数齐备就默认启用。
ADMIN_FRONTEND_ENABLED = truthy(
os.environ.get("ADMIN_FRONTEND_ENABLED"),
bool(
ADMIN_FRONTEND_SERVICE_NAME
and ADMIN_FRONTEND_HOST
and (ADMIN_FRONTEND_SSH_HOST or ADMIN_FRONTEND_IP)
and (ADMIN_FRONTEND_REPO_URL or str(ADMIN_FRONTEND_REPO_ROOT).strip())
),
)
# Mongo 连接串。
MONGO_URI = env_str("MONGO_URI", "")
# Mongo 默认关注的业务库名。
MONGO_DB_NAME = env_str("MONGO_DB_NAME", "tarab_all")
# Mongo 机器实例 ID主要用于和主机指标联动显示。
MONGO_INSTANCE_ID = env_str("MONGO_INSTANCE_ID", "ins-o9mmob66")
# Mongo 结果缓存秒数。
MONGO_CACHE_TTL_SECONDS = env_float("MONGO_CACHE_TTL_SECONDS", 20.0)
# Mongo 建连超时。
MONGO_CONNECT_TIMEOUT_MS = env_int("MONGO_CONNECT_TIMEOUT_MS", 3000)
# Mongo server selection 超时。
MONGO_SERVER_SELECTION_TIMEOUT_MS = env_int("MONGO_SERVER_SELECTION_TIMEOUT_MS", 3000)
# Mongo socket 超时。
MONGO_SOCKET_TIMEOUT_MS = env_int("MONGO_SOCKET_TIMEOUT_MS", 5000)
# MySQL 主机地址。
MYSQL_HOST = env_str("MYSQL_HOST", "")
# MySQL 端口。
MYSQL_PORT = env_int("MYSQL_PORT", 3306)
# MySQL 登录账号。
MYSQL_USER = env_str("MYSQL_USER", "")
# MySQL 登录密码;允许空串,兼容特殊场景。
MYSQL_PASSWORD = os.environ.get("MYSQL_PASSWORD", "")
# MySQL 默认库;为空时由页面显式选择。
MYSQL_DEFAULT_DATABASE = env_str("MYSQL_DEFAULT_DATABASE", "")
# MySQL 指标与元数据缓存秒数。
MYSQL_CACHE_TTL_SECONDS = env_float("MYSQL_CACHE_TTL_SECONDS", 8.0)
# MySQL 建连超时秒数。
MYSQL_CONNECT_TIMEOUT_SECONDS = env_int("MYSQL_CONNECT_TIMEOUT_SECONDS", 5)
# MySQL 读超时秒数。
MYSQL_READ_TIMEOUT_SECONDS = env_int("MYSQL_READ_TIMEOUT_SECONDS", 8)
# MySQL 写超时秒数。
MYSQL_WRITE_TIMEOUT_SECONDS = env_int("MYSQL_WRITE_TIMEOUT_SECONDS", 15)
# MySQL 表浏览默认分页大小。
MYSQL_DEFAULT_PAGE_SIZE = env_int("MYSQL_DEFAULT_PAGE_SIZE", 100)
# MySQL 表浏览最大分页大小。
MYSQL_MAX_PAGE_SIZE = env_int("MYSQL_MAX_PAGE_SIZE", 200)
# SQL 工作台单次最多返回多少行。
MYSQL_QUERY_MAX_ROWS = env_int("MYSQL_QUERY_MAX_ROWS", 500)
# CPU 告警阈值。
CPU_WARN_PERCENT = env_float("CPU_WARN_PERCENT", 60.0)
# CPU 严重阈值。
CPU_BAD_PERCENT = env_float("CPU_BAD_PERCENT", 85.0)
# 内存告警阈值。
MEMORY_WARN_PERCENT = env_float("MEMORY_WARN_PERCENT", 70.0)
# 内存严重阈值。
MEMORY_BAD_PERCENT = env_float("MEMORY_BAD_PERCENT", 90.0)
# 磁盘告警阈值。
DISK_WARN_PERCENT = env_float("DISK_WARN_PERCENT", 75.0)
# 磁盘严重阈值。
DISK_BAD_PERCENT = env_float("DISK_BAD_PERCENT", 90.0)
# 进程数告警阈值。
PROCESS_WARN_COUNT = env_int("PROCESS_WARN_COUNT", 180)
# 进程数严重阈值。
PROCESS_BAD_COUNT = env_int("PROCESS_BAD_COUNT", 260)
# 服务耗时告警阈值。
SERVICE_LATENCY_WARN_MS = env_int("SERVICE_LATENCY_WARN_MS", 1000)
# 服务耗时严重阈值。
SERVICE_LATENCY_BAD_MS = env_int("SERVICE_LATENCY_BAD_MS", 3000)
# 统一暴露给前端的阈值定义。
THRESHOLDS = {
"cpuPercent": {"warn": CPU_WARN_PERCENT, "bad": CPU_BAD_PERCENT},
"memoryPercent": {"warn": MEMORY_WARN_PERCENT, "bad": MEMORY_BAD_PERCENT},
"diskPercent": {"warn": DISK_WARN_PERCENT, "bad": DISK_BAD_PERCENT},
"processCount": {"warn": PROCESS_WARN_COUNT, "bad": PROCESS_BAD_COUNT},
"latencyMs": {"warn": SERVICE_LATENCY_WARN_MS, "bad": SERVICE_LATENCY_BAD_MS},
}
def utc_now() -> str:
# 时间函数延迟导入,避免这里堆太多全局依赖。
from datetime import datetime, timezone
# 统一输出无微秒的 UTC ISO 字符串。
return datetime.now(timezone.utc).replace(microsecond=0).isoformat()
def settings_payload() -> dict[str, Any]:
# 把页面需要的动态设置统一打包返回。
return {
"refreshIntervalSeconds": DEFAULT_REFRESH_INTERVAL_SECONDS,
"refreshIntervalOptions": list(REFRESH_INTERVAL_OPTIONS),
"thresholds": THRESHOLDS,
}
def load_hosts() -> list[dict[str, Any]]:
# 读取监控目标配置。
payload = json.loads(CONFIG_PATH.read_text(encoding="utf-8"))
# 没有 hosts 时回退为空列表。
hosts = payload.get("hosts") or []
# 规范化后的主机列表。
normalized: list[dict[str, Any]] = []
# 逐台主机做结构清洗。
for host in hosts:
normalized.append(
{
"host": str(host.get("name") or "").strip(),
"group": str(host.get("group") or "").strip(),
"ip": str(host.get("ip") or "").strip(),
"instanceId": str(host.get("instanceId") or "").strip(),
"sshHost": str(host.get("sshHost") or host.get("ip") or "").strip(),
"sshUser": str(host.get("sshUser") or SSH_USER).strip() or SSH_USER,
"sshUseSudo": truthy(host.get("sshUseSudo"), SSH_USE_SUDO),
"sshPort": int(host.get("sshPort") or SSH_PORT),
"services": host.get("services") or [],
}
)
# 返回可供各模块复用的主机列表。
return normalized
def flatten_services(hosts: list[dict[str, Any]]) -> list[dict[str, Any]]:
# 这里把主机结构拍平成服务列表,方便并发探测。
services: list[dict[str, Any]] = []
# 逐台主机展开服务。
for host in hosts:
for service in host.get("services") or []:
services.append(
{
"host": host["host"],
"group": host["group"],
"ip": host["ip"],
"instanceId": host["instanceId"],
"sshHost": host["sshHost"],
"sshUser": host["sshUser"],
"sshUseSudo": truthy(host.get("sshUseSudo"), SSH_USE_SUDO),
"sshPort": int(host["sshPort"]),
"service": str(service.get("name") or "").strip(),
"kind": str(service.get("kind") or "java").strip(),
"port": int(service.get("port")),
"path": str(service.get("path") or "/").strip(),
}
)
# 返回扁平服务列表。
return services