"""CloudCone 适配器(REST API v2) API 文档:https://cloudcone-api.readme.io/reference 所需配置:{"api_key": "...", "api_hash": "..."} 认证请求头:App-Secret = API Key,Hash = API Hash 覆盖:实例清单、开机/关机/重启。 CloudCone 没有公开的账单/到期/余额接口,故不声明 get_account, 也不提供续费动作——拿不到到期日就别假装能核对。 """ import httpx from app.adapters.base import BaseAdapter, NormalizedVPS from app.adapters.registry import register #: CloudCone state.name -> 台账状态 _STATUS_MAP = { "active": "active", "starting": "active", "rebooting": "active", "stopped": "stopped", "shutdown": "stopped", "suspended": "stopped", } @register("cloudcone-api") class CloudConeAdapter(BaseAdapter): required_config = ["api_key", "api_hash"] BASE = "https://api.cloudcone.com/api/v2" def _headers(self) -> dict: return { "App-Secret": self.config.get("api_key", ""), "Hash": self.config.get("api_hash", ""), } def _request(self, method: str, path: str) -> dict: with httpx.Client(timeout=30) as client: resp = client.request(method, self.BASE + path, headers=self._headers()) resp.raise_for_status() data = resp.json() if resp.content else {} # CloudCone 用 status=1 表示成功,0 时 message 里才有可读原因 if isinstance(data, dict) and data.get("status") not in (1, "1", None): raise RuntimeError(data.get("message") or "CloudCone 接口返回失败") return data def _get(self, path: str) -> dict: return self._request("GET", path) def _post(self, path: str) -> dict: return self._request("POST", path) def test_connection(self) -> dict: try: instances = self.list_vps() return {"ok": True, "message": f"连接成功,读取到 {len(instances)} 台实例"} except httpx.HTTPStatusError as e: return {"ok": False, "message": f"HTTP {e.response.status_code}:API Key/Hash 无效或权限不足"} except Exception as e: # noqa: BLE001 return {"ok": False, "message": str(e)} def list_vps(self) -> list: data = self._get("/compute/instances") raw_instances = (data.get("__data") or {}).get("instances") or [] result = [] for inst in raw_instances: state = (inst.get("state") or {}).get("name") or "unknown" ips = str(inst.get("ips") or "").strip() result.append( NormalizedVPS( external_id=str(inst.get("id")), name=inst.get("hostname") or inst.get("nodename") or str(inst.get("id")), ip_address=ips.split(",")[0].strip() if ips else None, region=inst.get("nodename"), os=inst.get("distro"), cpu_cores=inst.get("cpu"), memory_gb=inst.get("ram"), disk_gb=inst.get("disk"), status=_STATUS_MAP.get(state.lower(), "unknown"), currency="USD", charge_type="prepaid", raw=inst, ) ) return result # ---------------- 实例管理动作 ---------------- def _instance_action(self, external_id: str, verb: str, label: str) -> dict: try: self._post(f"/compute/{external_id}/{verb}") return {"ok": True, "message": f"{label}指令已下发"} except httpx.HTTPStatusError as e: return {"ok": False, "message": f"HTTP {e.response.status_code}:{label}失败"} except Exception as e: # noqa: BLE001 return {"ok": False, "message": f"{label}失败:{e}"} def start_instance(self, external_id: str) -> dict: return self._instance_action(external_id, "boot", "开机") def stop_instance(self, external_id: str) -> dict: return self._instance_action(external_id, "shutdown", "关机") def reboot_instance(self, external_id: str) -> dict: return self._instance_action(external_id, "reboot", "重启")