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

104 lines
4.2 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.
"""阿里云适配器(含国际版,OpenAPI RPC 风格 + HMAC-SHA1 签名)
API 文档:https://help.aliyun.com/document_detail/25484.html
所需配置:{"access_key_id": "...", "access_key_secret": "...", "region": "cn-hangzhou"}
国际版(alibabacloud)只需将 region 设为海外区域(如 ap-southeast-1),端点自动按区域构造。
说明:此处实现 RPC V1 签名(HMAC-SHA1),可直接调用 ECS OpenAPI
生产环境也可替换为官方 alibabacloud-ecs SDK,接口契约保持不变。
"""
import base64
import hashlib
import hmac
import time
import uuid
from urllib.parse import quote
import httpx
from app.adapters.base import BaseAdapter, NormalizedVPS
from app.adapters.registry import register
@register("aliyun-sdk", "alibabacloud-sdk")
class AliyunAdapter(BaseAdapter):
required_config = ["access_key_id", "access_key_secret"]
API_VERSION = "2014-05-26"
def _region(self) -> str:
return self.config.get("region") or "cn-hangzhou"
def _endpoint(self) -> str:
return f"https://ecs.{self._region()}.aliyuncs.com"
@staticmethod
def _percent_encode(value) -> str:
return quote(str(value), safe="~")
def _sign(self, params: dict, method: str = "GET") -> str:
canonical = "&".join(
f"{self._percent_encode(k)}={self._percent_encode(v)}"
for k, v in sorted(params.items())
)
string_to_sign = f"{method}&{self._percent_encode('/')}&{self._percent_encode(canonical)}"
key = (self.config.get("access_key_secret", "") + "&").encode()
digest = hmac.new(key, string_to_sign.encode(), hashlib.sha1).digest()
return base64.b64encode(digest).decode()
def _call(self, action: str, biz_params: dict = None) -> dict:
params = {
"Format": "JSON",
"Version": self.API_VERSION,
"AccessKeyId": self.config.get("access_key_id", ""),
"SignatureMethod": "HMAC-SHA1",
"Timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
"SignatureVersion": "1.0",
"SignatureNonce": str(uuid.uuid4()),
"Action": action,
"RegionId": self._region(),
}
if biz_params:
params.update(biz_params)
params["Signature"] = self._sign(params)
with httpx.Client(timeout=30) as client:
resp = client.get(self._endpoint(), params=params)
resp.raise_for_status()
data = resp.json()
if data.get("Code"):
raise RuntimeError(f"{data.get('Code')}: {data.get('Message')}")
return data
def test_connection(self) -> dict:
try:
data = self._call("DescribeRegions")
count = len(data.get("Regions", {}).get("Region", []))
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", {"PageSize": "100"})
result = []
for inst in data.get("Instances", {}).get("Instance", []):
public_ips = inst.get("PublicIpAddress", {}).get("IpAddress", [])
eip = inst.get("EipAddress", {}).get("IpAddress")
mem_mb = inst.get("Memory") or 0
status_map = {"Running": "active", "Stopped": "stopped"}
result.append(
NormalizedVPS(
external_id=inst.get("InstanceId"),
name=inst.get("InstanceName") or inst.get("InstanceId"),
ip_address=eip or (public_ips[0] if public_ips else None),
region=inst.get("RegionId"),
os=inst.get("OSName"),
cpu_cores=inst.get("Cpu"),
memory_gb=round(mem_mb / 1024, 1) if mem_mb else None,
status=status_map.get(inst.get("Status"), inst.get("Status") or "unknown"),
currency="CNY",
raw=inst,
)
)
return result