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

118 lines
5.0 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:
data = self._call("DescribeInstances", {"Limit": 100})
result = []
status_map = {"RUNNING": "active", "STOPPED": "stopped"}
for inst in data.get("InstanceSet", []):
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,
)
)
return result