Files
vps-manager/app/adapters/tencent.py
T

126 lines
5.4 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.
"""腾讯云适配器(含国际版,CVM OpenAPI + TC3-HMAC-SHA256 签名)
API 文档:https://cloud.tencent.com/document/api/213/15753
所需配置:{"secret_id": "...", "secret_key": "...", "region": "ap-guangzhou"}
国际版(intl)将 region 设为海外区域即可,端点统一为 cvm.tencentcloudapi.com。
说明:此处实现 TC3-HMAC-SHA256 签名,可直接调用 CVM OpenAPI
生产环境也可替换为官方 tencentcloud-sdk-python,接口契约保持不变。
"""
import hashlib
import hmac
import json
import time
import httpx
from app.adapters.base import BaseAdapter, NormalizedVPS
from app.adapters.registry import register
@register("tencent-sdk", "tencent-intl-sdk")
class TencentAdapter(BaseAdapter):
required_config = ["secret_id", "secret_key"]
HOST = "cvm.tencentcloudapi.com"
SERVICE = "cvm"
VERSION = "2017-03-12"
ALGORITHM = "TC3-HMAC-SHA256"
def _region(self) -> str:
return self.config.get("region") or "ap-guangzhou"
@staticmethod
def _hmac_sha256(key: bytes, msg: str) -> bytes:
return hmac.new(key, msg.encode(), hashlib.sha256).digest()
def _build_authorization(self, action: str, payload_json: str, timestamp: int, date: str):
secret_id = self.config.get("secret_id", "")
secret_key = self.config.get("secret_key", "")
content_type = "application/json; charset=utf-8"
canonical_headers = (
f"content-type:{content_type}\nhost:{self.HOST}\nx-tc-action:{action.lower()}\n"
)
signed_headers = "content-type;host;x-tc-action"
hashed_payload = hashlib.sha256(payload_json.encode()).hexdigest()
canonical_request = (
f"POST\n/\n\n{canonical_headers}\n{signed_headers}\n{hashed_payload}"
)
credential_scope = f"{date}/{self.SERVICE}/tc3_request"
hashed_canonical = hashlib.sha256(canonical_request.encode()).hexdigest()
string_to_sign = (
f"{self.ALGORITHM}\n{timestamp}\n{credential_scope}\n{hashed_canonical}"
)
secret_date = self._hmac_sha256(("TC3" + secret_key).encode(), date)
secret_service = self._hmac_sha256(secret_date, self.SERVICE)
secret_signing = self._hmac_sha256(secret_service, "tc3_request")
signature = hmac.new(secret_signing, string_to_sign.encode(), hashlib.sha256).hexdigest()
return (
f"{self.ALGORITHM} Credential={secret_id}/{credential_scope}, "
f"SignedHeaders={signed_headers}, Signature={signature}"
)
def _call(self, action: str, payload: dict = None) -> dict:
payload_json = json.dumps(payload or {})
timestamp = int(time.time())
date = time.strftime("%Y-%m-%d", time.gmtime(timestamp))
authorization = self._build_authorization(action, payload_json, timestamp, date)
headers = {
"Authorization": authorization,
"Content-Type": "application/json; charset=utf-8",
"Host": self.HOST,
"X-TC-Action": action,
"X-TC-Timestamp": str(timestamp),
"X-TC-Version": self.VERSION,
"X-TC-Region": self._region(),
}
with httpx.Client(timeout=30) as client:
resp = client.post(f"https://{self.HOST}", content=payload_json, headers=headers)
resp.raise_for_status()
data = resp.json()
response = data.get("Response", {})
if response.get("Error"):
err = response["Error"]
raise RuntimeError(f"{err.get('Code')}: {err.get('Message')}")
return response
def test_connection(self) -> dict:
try:
data = self._call("DescribeRegions")
count = len(data.get("RegionSet", []))
return {"ok": True, "message": f"连接成功,可用区域 {count} 个"}
except httpx.HTTPStatusError as e:
return {"ok": False, "message": f"HTTP {e.response.status_code}:凭证无效"}
except Exception as e: # noqa: BLE001
return {"ok": False, "message": str(e)}
def list_vps(self) -> list:
# 分页拉取:单次最多 100 条,按 TotalCount 翻页,避免实例超 100 台时漏同步
result = []
status_map = {"RUNNING": "active", "STOPPED": "stopped"}
offset = 0
while True:
data = self._call("DescribeInstances", {"Limit": 100, "Offset": offset})
inst_set = data.get("InstanceSet", [])
for inst in inst_set:
public_ips = inst.get("PublicIpAddresses", [])
result.append(
NormalizedVPS(
external_id=inst.get("InstanceId"),
name=inst.get("InstanceName") or inst.get("InstanceId"),
ip_address=public_ips[0] if public_ips else None,
region=inst.get("Placement", {}).get("Zone"),
os=inst.get("OsName"),
cpu_cores=inst.get("CPU"),
memory_gb=inst.get("Memory"),
disk_gb=inst.get("SystemDisk", {}).get("DiskSize"),
status=status_map.get(inst.get("InstanceState"), inst.get("InstanceState") or "unknown"),
currency="CNY",
raw=inst,
)
)
total = int(data.get("TotalCount") or 0)
offset += len(inst_set)
if not inst_set or offset >= total:
break
return result