KVMCloud 文档
从零部署到 API 对接的完整说明。控制面提供声明式 REST API,
变更类操作统一返回 202 + task_id,客户端轮询任务至终态即可。
版本:本文档对应 v1.0.0。v1.0.0 新增「建机指定节点」与「节点级流量包」两项能力。
快速开始
三种形态,行为完全一致:单机全栈、主控 + 多计算节点、仅计算节点。
1. 单机全栈(体验 / 小规模生产)
自动识别发行版、换国内镜像源、开通 PostgreSQL 库、生成管理口令,装完打印控制台入口。
# 国内(Gitee 分发) curl -fsSL https://gitee.com/yuntianojo/kvmcloud/raw/main/install.sh -o /tmp/kc.sh \ && sudo bash /tmp/kc.sh all-in-one # 海外(德国 CDN 分发) curl -fsSL https://cdn.kvm.mianyun.cloud/install-overseas.sh -o /tmp/kc.sh \ && sudo bash /tmp/kc.sh all-in-one
2. 主控 + 多计算节点
主控只跑控制面与数据库;计算节点只装 KVM / libvirt / TC,不监听端口。
# 主控 sudo bash /tmp/kc.sh control # 每台计算节点(JOIN_TOKEN 从主控安装摘要或 control.env 获取) sudo bash /tmp/kc.sh agent \ --control-addr 10.0.0.10:9443 --token <JOIN_TOKEN> \ --cpu-ratio 4 --mem-ratio 1.5 --disk-ratio 2 --bw-ratio 1
3. 打开控制台
控制台已 go:embed 进主控二进制,装完即用。默认藏在随机路径下(防扫描),
安装摘要会打印完整入口,也可在主控执行:
cat /var/lib/kvmcloud/control-ui-path # 控制台路径 grep ADMIN_PASSWORD /etc/cloud/control.env # 初始管理员口令
生产环境务必配 HTTPS。控制面 HTTP 只允许绑定 loopback(fail-closed),
公网访问需在 /etc/cloud/control.env 配置 CONTROL_TLS_CERT /
CONTROL_TLS_KEY,或在控制台「系统设置」上传证书。
核心概念
四维超开
CPU / 内存 / 磁盘 / 带宽四个维度独立配置超开比,每个节点可单独调优。创建实例时任一维超限即拒绝(409 capacity_unavailable)。
| 维度 | 校验公式 | 默认比 |
|---|---|---|
| CPU | ΣvCPU ≤ 物理核 × ratio | 4.0 |
| 内存 | Σmem ≤ 物理内存 × ratio | 1.5 |
| 磁盘(精简置备) | Σdisk ≤ 物理磁盘 × ratio | 2.0 |
| 带宽 | Σmin_kbps ≤ 网卡带宽 × ratio | 1.0 |
带宽三语义
- 保障(min):创建策略时两级校验——节点侧
Σmin ≤ 网卡×ratio、池侧Σmin ≤ 池容量。 - 借用(borrow):池余量充足直接授予,
ceil从min提升到min + granted。 - 抢占(preempt):余量不足按优先级压回低优先级借用方(永不低于其 min),全部事件落审计。
指定节点建机 v1.0.0
POST /api/v1/vms 的 node_id 字段决定落点:省略由调度器按四维水位自动选;
指定则必须命中该节点,节点离线/不可调度/水位不足返回 409,不会静默改到别的节点。
显式 node_id 优先于「镜像所在节点」。
流量包两级 v1.0.0
- 租户级(不传
node_id):对所有节点的用量生效,全局 FIFO 抵扣。 - 节点级(传
node_id):只抵扣该节点上实例的用量,并下发traffic.grant任务到节点本地入账。 - 带
external_ref时由唯一索引保证外部计费系统重复推送幂等。
鉴权与幂等
两种凭据
| 类型 | 形态 | 用途 |
|---|---|---|
| 会话令牌 | POST /auth/login 返回 token |
控制台 / 管理后台,24 小时有效 |
| 对接密钥 | kc_ + 43 字符 base64url |
服务端对接,长期有效,可撤销 |
两者都用同一个请求头:
Authorization: Bearer <TOKEN>
幂等键
所有写操作(POST / PUT / DELETE)必须携带
Idempotency-Key,16–128 个可见 ASCII 字符,建议用 UUID。
Idempotency-Key: 6f1c2a9e-4b3d-4f8a-9c21-7e5b0d3a8f14
- 同键 + 同请求体 → 重放首次结果,不重复执行。
- 同键 + 不同请求体 →
409 idempotency_conflict。 - 登录、登出、节点安装令牌签发不接受该头(响应含一次性密钥,重放会泄漏),带上会返回
400 secret_replay_not_supported。
登录
{
"username": "admin",
"password": "<ADMIN_PASSWORD>"
}
响应:{"token":"...","expires_at":"2026-11-09T10:00:00Z"}
错误码与重试
错误响应统一为机器可读结构,不泄漏 SQL / 驱动 / 主机信息;每个响应带 X-Request-ID。
{
"error": {
"code": "capacity_unavailable",
"request_id": "9a3f…"
}
}
| HTTP | code | 含义 / 处理 |
|---|---|---|
| 400 | idempotency_key_required | 写请求缺幂等键,补上后重试 |
| 400 | secret_replay_not_supported | 该端点不接受幂等键,去掉请求头 |
| 401 | unauthorized | 令牌无效/过期,重新登录 |
| 403 | forbidden | 缺 scope,检查角色授权 |
| 404 | not_found | 资源不存在或不属于本租户(不区分) |
| 409 | capacity_unavailable | 节点水位不足 / 指定节点不可调度 |
| 409 | idempotency_conflict | 同键不同请求体,换键 |
| 422 | validation_failed | 字段级校验失败,details 定位字段 |
| 503 | database_unavailable | 依赖不可用,指数退避重试 |
重试建议:429 / 503 指数退避(1s 起、上限 30s、最多 5 次);
4xx 其余不要盲重试,先修请求。所有重试必须复用同一 Idempotency-Key。
异步任务模型
所有变更类操作返回 202 Accepted,真正的执行由计算节点上的 Agent 完成。
HTTP/1.1 202 Accepted
Location: /api/v1/tasks/3f2a9c11-…
{ "task_id": "3f2a9c11-…", "state": "running" }
轮询任务至终态(建议 2s 间隔、指数退避到 5s):
| state | 含义 |
|---|---|
running | 执行中,继续轮询 |
succeeded | 成功,终态 |
failed | 失败,读 error_code,终态 |
cancelled | 已取消,终态 |
虚拟机
镜像三选一:image_id(已就绪本地镜像)、image_source_name(名称匹配,未命中则自动拉取)、
或两者都不传会被拒绝。
{
"name": "web-fra-01",
"image_source_name": "debian-12",
"image_os_type": "linux",
"network_id": "00000000-0000-0000-0000-000000000003",
"node_id": "b1f0c2d3-…", // 省略 = 自动调度
"vcpus": 4,
"memory_mib": 8192,
"root_disk_gib": 40,
"ssh_public_keys": ["ssh-ed25519 AAAA… user@host"],
"bandwidth": {
"min_kbps": 20000,
"max_kbps": 100000,
"priority": 10
}
}
响应:202 + {"vm_id":"…","task_id":"…","state":"creating"}。
走远端镜像时额外返回 image_pull_required: true,实例在镜像就绪后自动创建。
默认最近 200 条(created_at DESC)。批量对账用 ids= 精确批查(≤100),不受列表窗口限制。
GET /api/v1/vms?state=running&limit=100 GET /api/v1/vms?ids=7d31…,9f02…
集成方按 ID 精确同步,不受列表窗口限制。
聚合私网地址与公网端口转发视角,开通后取登录地址用这个。
{ "vcpus": 8, "memory_mib": 16384 }
目标节点缺省时在集群内按水位自动选。
回收站保留期内可用 POST /api/v1/vms/{id}/restore 恢复。
Agent 扫描本地 libvirt 域并上报,控制面入库后补发 QoS。
电源与系统盘
可选查询参数 ?hard=1 走强制关机。返回 202,状态随 Agent 回报收敛。
curl -X POST "$BASE/api/v1/vms/$VM/actions/stop?hard=1" \ -H "Authorization: Bearer $TOKEN" \ -H "Idempotency-Key: $(uuidgen)"
新口令一次性返回,不落日志、不入数据库明文。仅运行中实例可调用。
{ "image_id": "…" }
// 或
{ "image_source_name": "ubuntu-24.04-lts" }
运行中实例先调 stop 并轮询到 stopped,否则返回 422 vm_running。
重装只重建系统盘,数据盘与快照不受影响;完成后保持关机,需自行开机。
带宽调度
{
"min_kbps": 50000,
"max_kbps": 200000,
"shared_pool_id": "…", // 可选:加入共享池借用
"priority": 20
}
池余量不足时按 priority 抢占低优先级借用方(永不低于其 min);凑不足整体拒绝。
对账用:账本是控制面记录的期望值,applied 是节点实际生效值,两者不一致说明下发未收敛。
返回近端时间窗的 ingress / egress 采样,用于画曲线与峰值统计。
云盘与快照
Agent 执行 qemu-img / RBD 操作,异步推进状态。
状态先置 deleting,Agent 执行后置 deleted。
网络与安全组
{
"public_ip_id": "…",
"external_port": 2222,
"internal_port": 22,
"protocol": "tcp"
}
Agent 侧 nftables 全量收敛,不破坏 TC 限速。
{ "name": "web", "description": "80/443 入站" }
镜像
镜像链路:源同步(控制面解析远端目录 + 校验和)→ 远端镜像库 → 节点本地拉取(建机时同位调度)。
扫描目录 + upsert,SHA-256 取自源的 SHA256SUMS。
{ "remote_image_id": "…", "node_id": "…" }
流量包
{
"product_id": "…",
"quantity": 1,
"node_id": "b1f0c2d3-…", // 省略 = 租户级
"external_ref": "BILL-2026-1108-001" // 外部计费幂等键
}
带 node_id 时响应额外返回 grant_task_id:控制面会下发 traffic.grant
到该节点,Agent 写入本地台账(DataDir/traffic-packages.json,0600,按 package_id 幂等)并 ack。
抵扣语义:租户级包对所有节点生效;节点级包只抵扣该节点上实例的用量。计费权威仍在控制面。
计费
| 端点 | 说明 |
|---|---|
GET /billing/ledger | 账单流水(append-only) |
GET /billing/orders | 流量包订单 |
GET /billing/wallets | 钱包余额(含乐观锁 version) |
POST /billing/wallets/{currency}/topups | 充值(幂等必填;余额+账本+审计单事务) |
GET /billing/usage-reports | 出向用量明细(Agent 上报原始增量) |
GET /billing/usage-summary | 用量汇总(对账口径) |
GET /billing/traffic-debits | 抵扣明细(包抵扣 / 超额) |
# 充值 POST /api/v1/billing/wallets/CNY/topups { "amount": 10000, "external_ref": "PAY-8891" }
节点与平台
| 端点 | 说明 |
|---|---|
GET /node-overviews | 节点概览(水位 + 实例台数;仪表盘地图数据源) |
GET /nodes/{id}/metrics | 四维水位与心跳新鲜度 |
GET|PUT /nodes/{id}/network | 节点网络配置(NAT/桥接 + 公网 IP + WAN 网卡) |
POST /nodes/install-token | 节点安装令牌签发 |
GET /tasks · GET /tasks/{id} | 任务列表 / 详情(所有 202 的轮询出口) |
GET /dashboard/summary | 仪表盘聚合 |
GET /audit-logs | 审计日志(append-only,keyset 分页) |
GET /control-plane/status | 主控自监控(CPU/RSS、Go 堆、连接池、磁盘) |
GET|POST /api-keys · DELETE /api-keys/{id} | 对接密钥签发 / 撤销(明文仅创建时返回) |
GET|PUT /settings/tls | 控制台证书状态 / 上传热生效 |
GET /tenants · GET /tenants/{id}/quotas | 租户清单 / 配额 |
能力开关:部分端点需要 platform:read 等 scope;
缺失时返回 403 forbidden,不会降级返回部分数据。
对接示例
cURL:开通一台指定节点的机器并等它跑起来
BASE="https://your-control:27400" TOKEN="$KVMCLOUD_TOKEN" VM_URL="$BASE/api/v1/vms" # 1) 创建(指定节点;省略 node_id 则自动调度) RESP=$(curl -sS -X POST "$VM_URL" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "name": "web-fra-01", "image_source_name": "debian-12", "network_id": "00000000-0000-0000-0000-000000000003", "node_id": "b1f0c2d3-…", "vcpus": 4, "memory_mib": 8192, "root_disk_gib": 40, "bandwidth": { "min_kbps": 20000, "max_kbps": 100000, "priority": 10 } }') TASK=$(echo "$RESP" | python3 -c 'import sys,json;print(json.load(sys.stdin)["task_id"])') # 2) 轮询任务到终态 while :; do ST=$(curl -sS "$BASE/api/v1/tasks/$TASK" -H "Authorization: Bearer $TOKEN" \ | python3 -c 'import sys,json;print(json.load(sys.stdin)["state"])') echo "state=$ST" case "$ST" in succeeded|failed|cancelled) break;; esac sleep 2 done
Python:批量对账已知实例状态
import json, urllib.request BASE = "https://your-control:27400" TOKEN = "kc_..." def get(path): req = urllib.request.Request(BASE + path, headers={ "Authorization": "Bearer " + TOKEN, }) with urllib.request.urlopen(req, timeout=15) as r: return json.load(r) # ids 精确批查(≤100),绕开 200 条列表窗口 ids = ["7d31…", "9f02…"] data = get("/api/v1/vms?ids=" + ",".join(ids)) for vm in data["items"]: print(vm["id"], vm["state"])
Python:购买节点级流量包
import json, uuid, urllib.request def post(path, body): req = urllib.request.Request( BASE + path, data=json.dumps(body).encode(), method="POST", headers={ "Authorization": "Bearer " + TOKEN, "Content-Type": "application/json", "Idempotency-Key": str(uuid.uuid4()), }) with urllib.request.urlopen(req, timeout=15) as r: return json.load(r) res = post(f"/api/v1/tenants/{TENANT_ID}/traffic-packages", { "product_id": PRODUCT_ID, "quantity": 1, "node_id": NODE_ID, # 省略 = 租户级 "external_ref": "BILL-2026-1108-001", }) print(res.get("grant_task_id")) # 节点下发任务 ID
Go:带重试的幂等写请求
func do(ctx context.Context, method, url string, body []byte) ([]byte, error) { key := uuid.NewString() // 重试必须复用同一个键 for attempt := 0; attempt < 5; attempt++ { req, _ := http.NewRequestWithContext(ctx, method, url, bytes.NewReader(body)) req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Content-Type", "application/json") req.Header.Set("Idempotency-Key", key) resp, err := http.DefaultClient.Do(req) if err != nil || resp.StatusCode >= 500 { time.Sleep(time.Duration(1<<attempt) * time.Second) continue } defer resp.Body.Close() if resp.StatusCode >= 400 { return nil, fmt.Errorf("api %d", resp.StatusCode) } return io.ReadAll(resp.Body) } return nil, errors.New("retries exhausted") }
常见问题
创建返回 409 capacity_unavailable,但节点是空的?
检查该节点的四维水位而不只是看实例数:磁盘和带宽也会先满。
指定 node_id 时不会自动换节点,需要放宽超开比或换节点。
实例一直是 creating,任务却 succeeded?
走 image_source_name 且远端未命中时,会先拉取镜像再建机。
看任务 result 里的 image_pull_required,
并用 GET /local-images 观察镜像状态到 available。
重装报 422 vm_running
先 stop,轮询 GET /vms/{id} 到 state == "stopped" 再重装。
带宽改了但实测没变?
对比 GET /vms/{id}/bandwidth 的期望值与 applied 值。
若 applied 未跟上,说明节点侧 TC 未收敛,查 Agent 日志与任务状态。
重复推送流量包会不会重复扣钱?
不会。带同一个 external_ref 的请求由唯一索引拦截,返回首次结果。
不带 external_ref 的请求请务必复用同一 Idempotency-Key。
镜像下载卡在排队中
优先用不降级的源(如 mirrors.tuna.tsinghua.edu.cn)。
aliyun 会把 ISO 302 到 http://,平台按安全策略拒绝跟随并明确报错。
控制台打不开 / 忘记入口
控制台默认走随机路径。在主控执行 cat /var/lib/kvmcloud/control-ui-path 找回。
版本说明
| 版本 | 要点 |
|---|---|
| v1.0.0 | 首个生产版本:建机 node_id 指定节点;流量包支持节点级 + external_ref 幂等;72 端点全量对接测试 |
| v0.9.9 | 镜像下载链路修复(策略 / 权限 / 数组 / 摘要);节点本地镜像方案 |
| v0.9.6 | 节点凭据(enroll + per-node secret);安全基线加固 |
| v0.9.0 | coreapi 新架构落地:RLS 多租户、幂等、事务性 outbox |