feat: 云厂商SDK适配层(Vultr/DO/Cloudflare/阿里云/腾讯云)+ 同步服务 + 测试连接/同步接口与前端

This commit is contained in:
gouki
2026-08-02 15:56:48 +00:00
parent cc7b2c67fe
commit 1aacf51bea
13 changed files with 930 additions and 3 deletions
+89
View File
@@ -0,0 +1,89 @@
"""DigitalOcean 适配器(REST API v2Bearer Token
API 文档:https://docs.digitalocean.com/reference/api/
所需配置:{"api_key": "..."}
"""
import httpx
from app.adapters.base import (
AccountInfo,
BaseAdapter,
NormalizedDomain,
NormalizedVPS,
)
from app.adapters.registry import register
@register("do-api")
class DigitalOceanAdapter(BaseAdapter):
required_config = ["api_key"]
BASE = "https://api.digitalocean.com/v2"
def _headers(self) -> dict:
return {"Authorization": f"Bearer {self.config.get('api_key', '')}"}
def _get(self, path: str) -> dict:
with httpx.Client(timeout=30) as client:
resp = client.get(self.BASE + path, headers=self._headers())
resp.raise_for_status()
return resp.json()
def test_connection(self) -> dict:
try:
data = self._get("/account")
balance = data.get("account", {}).get("balance")
return {"ok": True, "message": f"连接成功,账户余额 {balance} USD"}
except httpx.HTTPStatusError as e:
return {"ok": False, "message": f"HTTP {e.response.status_code}Token 无效或权限不足"}
except Exception as e: # noqa: BLE001
return {"ok": False, "message": str(e)}
def get_account(self) -> AccountInfo:
acc = self._get("/account").get("account", {})
return AccountInfo(balance=acc.get("balance"), currency="USD", raw=acc)
@staticmethod
def _main_ip(droplet: dict) -> str:
try:
v4 = droplet.get("networks", {}).get("v4", [])
return v4[0].get("ip_address") if v4 else None
except Exception: # noqa: BLE001
return None
def list_vps(self) -> list:
data = self._get("/droplets")
result = []
for d in data.get("droplets", []):
image = d.get("image", {})
os_name = f"{image.get('distribution', '')} {image.get('name', '')}".strip()
mem_mb = d.get("memory") or 0
region = d.get("region", {})
result.append(
NormalizedVPS(
external_id=str(d.get("id")),
name=d.get("name"),
ip_address=self._main_ip(d),
region=region.get("slug") or region.get("name"),
os=os_name or None,
cpu_cores=d.get("vcpus"),
memory_gb=round(mem_mb / 1024, 1) if mem_mb else None,
disk_gb=d.get("disk"),
status="active" if d.get("status") == "active" else (d.get("status") or "unknown"),
currency="USD",
raw=d,
)
)
return result
def list_domains(self) -> list:
data = self._get("/domains")
return [
NormalizedDomain(
external_id=d.get("name"),
domain_name=d.get("name"),
registrar="digitalocean",
raw=d,
)
for d in data.get("domains", [])
]