hy-app-monitor/monitors/yumi/tencent_tat.py

194 lines
6.2 KiB
Python
Raw Permalink 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 base64
import json
import time
from typing import Any
from core.config import DEPLOY_REGION, TAT_METRIC_TIMEOUT_SECONDS, TENCENT_SECRET_ID, TENCENT_SECRET_KEY, utc_now
def decode_task_output(output: str) -> str:
# 没有输出时直接返回空串。
if not output:
return ""
# 把 TAT 返回的 base64 文本解码成普通字符串。
return base64.b64decode(output).decode("utf-8", errors="replace")
def build_tat_client() -> tuple[Any | None, str]:
# TAT 运行需要完整的腾讯云密钥和区域。
if not TENCENT_SECRET_ID or not TENCENT_SECRET_KEY or not DEPLOY_REGION:
return None, "missing TAT credentials in .env"
try:
# 运行时才导入 SDK避免没装依赖时整个服务启动失败。
from tencentcloud.common import credential
# 腾讯云异常类型用于重试。
from tencentcloud.common.profile.client_profile import ClientProfile
# 指定 TAT API endpoint。
from tencentcloud.common.profile.http_profile import HttpProfile
# TAT 客户端。
from tencentcloud.tat.v20201028 import tat_client
except Exception as exc: # noqa: BLE001
# SDK 不存在时直接返回错误。
return None, f"tencentcloud sdk unavailable: {exc}"
# 创建凭据对象。
cred = credential.Credential(TENCENT_SECRET_ID, TENCENT_SECRET_KEY)
# 创建 TAT 客户端。
client = tat_client.TatClient(
cred,
DEPLOY_REGION,
ClientProfile(httpProfile=HttpProfile(endpoint="tat.tencentcloudapi.com")),
)
# 成功时返回客户端。
return client, ""
def invoke_tat(action: Any, label: str, retries: int = 3, delay_seconds: int = 2) -> Any:
# 这里延迟导入异常类型,避免 SDK 缺失时模块导入失败。
from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException
# 记录最后一次异常。
last_error: Exception | None = None
# 简单重试几次,防止瞬时 API 抖动。
for attempt in range(1, retries + 1):
try:
# 成功时直接返回结果。
return action()
except TencentCloudSDKException as exc:
# 保存最后一次异常。
last_error = exc
# 最后一次重试失败时不再等待。
if attempt == retries:
break
# 做一个线性退避。
time.sleep(delay_seconds * attempt)
# 有异常时抛出最后一次异常。
if last_error is not None:
raise last_error
# 理论上不会走到这里,这里只是兜底。
raise RuntimeError(f"{label} failed without error")
def run_shell_command(
instance_ids: list[str],
script: str,
*,
command_name: str,
timeout_seconds: int = TAT_METRIC_TIMEOUT_SECONDS,
working_directory: str = "/root",
) -> dict[str, dict[str, Any]]:
# 先初始化 TAT 客户端。
client, error = build_tat_client()
# 客户端不可用时直接抛错。
if client is None:
raise RuntimeError(error)
# 这里延迟导入 TAT models。
from tencentcloud.tat.v20201028 import models as tat_models
# 创建命令执行请求。
req = tat_models.RunCommandRequest()
# 组装批量执行命令。
req.from_json_string(
json.dumps(
{
"CommandName": command_name,
"CommandType": "SHELL",
"Content": base64.b64encode(script.encode("utf-8")).decode("ascii"),
"InstanceIds": instance_ids,
"Timeout": timeout_seconds,
"WorkingDirectory": working_directory,
}
)
)
# 发起远端命令执行。
response = json.loads(invoke_tat(lambda: client.RunCommand(req), "RunCommand").to_json_string())
# 拿到 invocation id后面轮询执行结果。
invocation_id = response["InvocationId"]
# 设置轮询截止时间。
deadline = time.time() + timeout_seconds + 60
# 这里保存各实例的最终结果。
results: dict[str, dict[str, Any]] = {}
# 开始轮询任务结果。
while time.time() < deadline:
# 轮询间隔不必太短。
time.sleep(2)
# 创建查询结果请求。
query = tat_models.DescribeInvocationTasksRequest()
# 按 invocation id 查询。
query.from_json_string(
json.dumps(
{
"HideOutput": False,
"Limit": 50,
"Filters": [{"Name": "invocation-id", "Values": [invocation_id]}],
}
)
)
# 拉取当前命令所有实例的执行状态。
payload = json.loads(
invoke_tat(lambda: client.DescribeInvocationTasks(query), "DescribeInvocationTasks").to_json_string()
)
# 任务列表可能暂时为空。
tasks = payload.get("InvocationTaskSet") or []
# 没任务时继续等待。
if not tasks:
continue
# 遍历当前拿到的实例结果。
for task in tasks:
# 读取实例 ID。
instance_id = task.get("InstanceId", "")
# 读取任务状态。
status = task.get("TaskStatus", "")
# 只处理终态。
if status not in {"SUCCESS", "FAILED", "TIMEOUT", "CANCELLED", "START_FAILED"}:
continue
# 解码远端输出。
output = decode_task_output(task.get("TaskResult", {}).get("Output", ""))
# 统一存成结构化结果。
results[instance_id] = {
"status": status,
"output": output,
"updatedAt": utc_now(),
}
# 收齐全部实例结果后结束轮询。
if len(results) >= len(instance_ids):
break
# 为缺失结果的实例补超时占位。
for instance_id in instance_ids:
results.setdefault(
instance_id,
{
"status": "TIMEOUT",
"output": "",
"updatedAt": utc_now(),
},
)
# 返回按实例 ID 索引的结果。
return results