Documentation

KVMCloud 文档

从零部署到 API 对接的完整说明。控制面提供声明式 REST API, 变更类操作统一返回 202 + task_id,客户端轮询任务至终态即可。

10 分钟跑起来
单机全栈安装,主控 + 本机计算节点一次装完,自带控制台。
72 个 API 端点
虚拟机、带宽、网络、存储、镜像、计费、节点全覆盖。
声明式变更
只提交期望状态,不传 SSH 凭证、不传 Shell 命令。
多租户隔离
PostgreSQL RLS + 显式租户绑定,越权与不存在都返回 404。

版本:本文档对应 v1.0.0。v1.0.0 新增「建机指定节点」与「节点级流量包」两项能力。

快速开始

三种形态,行为完全一致:单机全栈、主控 + 多计算节点、仅计算节点。

1. 单机全栈(体验 / 小规模生产)

自动识别发行版、换国内镜像源、开通 PostgreSQL 库、生成管理口令,装完打印控制台入口。

bash
# 国内(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,不监听端口。

bash
# 主控
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 进主控二进制,装完即用。默认藏在随机路径下(防扫描), 安装摘要会打印完整入口,也可在主控执行:

bash
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 ≤ 物理核 × ratio4.0
内存Σmem ≤ 物理内存 × ratio1.5
磁盘(精简置备)Σdisk ≤ 物理磁盘 × ratio2.0
带宽Σmin_kbps ≤ 网卡带宽 × ratio1.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 服务端对接,长期有效,可撤销

两者都用同一个请求头:

http
Authorization: Bearer <TOKEN>

幂等键

所有写操作(POST / PUT / DELETE)必须携带 Idempotency-Key,16–128 个可见 ASCII 字符,建议用 UUID。

http
Idempotency-Key: 6f1c2a9e-4b3d-4f8a-9c21-7e5b0d3a8f14
  • 同键 + 同请求体 → 重放首次结果,不重复执行。
  • 同键 + 不同请求体 → 409 idempotency_conflict。
  • 登录、登出、节点安装令牌签发不接受该头(响应含一次性密钥,重放会泄漏),带上会返回 400 secret_replay_not_supported。

登录

POST /api/v1/auth/login 用户名密码换会话令牌
json
{
  "username": "admin",
  "password": "<ADMIN_PASSWORD>"
}

响应:{"token":"...","expires_at":"2026-11-09T10:00:00Z"}

错误码与重试

错误响应统一为机器可读结构,不泄漏 SQL / 驱动 / 主机信息;每个响应带 X-Request-ID。

json
{
  "error": {
    "code": "capacity_unavailable",
    "request_id": "9a3f…"
  }
}
HTTPcode含义 / 处理
400idempotency_key_required写请求缺幂等键,补上后重试
400secret_replay_not_supported该端点不接受幂等键,去掉请求头
401unauthorized令牌无效/过期,重新登录
403forbidden缺 scope,检查角色授权
404not_found资源不存在或不属于本租户(不区分)
409capacity_unavailable节点水位不足 / 指定节点不可调度
409idempotency_conflict同键不同请求体,换键
422validation_failed字段级校验失败,details 定位字段
503database_unavailable依赖不可用,指数退避重试

重试建议:429 / 503 指数退避(1s 起、上限 30s、最多 5 次); 4xx 其余不要盲重试,先修请求。所有重试必须复用同一 Idempotency-Key。

异步任务模型

所有变更类操作返回 202 Accepted,真正的执行由计算节点上的 Agent 完成。

http
HTTP/1.1 202 Accepted
Location: /api/v1/tasks/3f2a9c11-…

{ "task_id": "3f2a9c11-…", "state": "running" }

轮询任务至终态(建议 2s 间隔、指数退避到 5s):

state含义
running执行中,继续轮询
succeeded成功,终态
failed失败,读 error_code,终态
cancelled已取消,终态

虚拟机

POST /api/v1/vms 创建虚拟机(可指定节点) v1.0.0

镜像三选一:image_id(已就绪本地镜像)、image_source_name(名称匹配,未命中则自动拉取)、 或两者都不传会被拒绝。

json
{
  "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,实例在镜像就绪后自动创建。

GET /api/v1/vms 实例列表

默认最近 200 条(created_at DESC)。批量对账用 ids= 精确批查(≤100),不受列表窗口限制。

http
GET /api/v1/vms?state=running&limit=100
GET /api/v1/vms?ids=7d31…,9f02…
GET /api/v1/vms/{id} 单实例详情

集成方按 ID 精确同步,不受列表窗口限制。

GET /api/v1/vms/{id}/addresses IP 视图(私网 + 公网转发)

聚合私网地址与公网端口转发视角,开通后取登录地址用这个。

POST /api/v1/vms/{id}/resize 在线变配(至少一维;根盘只增不减)
json
{ "vcpus": 8, "memory_mib": 16384 }
POST /api/v1/vms/{id}/migrate 热迁移

目标节点缺省时在集群内按水位自动选。

DELETE /api/v1/vms/{id} 删除 → 回收站(软删除,默认 7 天)

回收站保留期内可用 POST /api/v1/vms/{id}/restore 恢复。

POST /api/v1/vms/adopt 纳管已有机器

Agent 扫描本地 libvirt 域并上报,控制面入库后补发 QoS。

电源与系统盘

POST /api/v1/vms/{id}/actions/{start|stop|reboot} 电源控制

可选查询参数 ?hard=1 走强制关机。返回 202,状态随 Agent 回报收敛。

bash
curl -X POST "$BASE/api/v1/vms/$VM/actions/stop?hard=1" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: $(uuidgen)"
POST /api/v1/vms/{id}/reset-password 重置 root 密码(仅 running)

新口令一次性返回,不落日志、不入数据库明文。仅运行中实例可调用。

POST /api/v1/vms/{id}/reinstall 重装系统盘(仅 stopped;保留 IP 与账本)
json
{ "image_id": "…" }
// 或
{ "image_source_name": "ubuntu-24.04-lts" }

运行中实例先调 stop 并轮询到 stopped,否则返回 422 vm_running。 重装只重建系统盘,数据盘与快照不受影响;完成后保持关机,需自行开机。

带宽调度

PUT /api/v1/vms/{id}/bandwidth 端口级带宽策略(声明式,Agent 执行 TC)
json
{
  "min_kbps": 50000,
  "max_kbps": 200000,
  "shared_pool_id": "…",   // 可选:加入共享池借用
  "priority": 20
}

池余量不足时按 priority 抢占低优先级借用方(永不低于其 min);凑不足整体拒绝。

GET /api/v1/vms/{id}/bandwidth 期望(账本)vs 已应用(Agent 回报)

对账用:账本是控制面记录的期望值,applied 是节点实际生效值,两者不一致说明下发未收敛。

GET /api/v1/vms/{id}/bandwidth-metrics 带宽指标(Redis 聚合,只读)

返回近端时间窗的 ingress / egress 采样,用于画曲线与峰值统计。

云盘与快照

GET/api/v1/volumes云盘列表
POST/api/v1/volumes/{id}/snapshots创建快照(crash 一致性)

Agent 执行 qemu-img / RBD 操作,异步推进状态。

GET/api/v1/volumes/{id}/snapshots该卷的快照列表
DELETE/api/v1/snapshots/{id}删除快照

状态先置 deleting,Agent 执行后置 deleted。

网络与安全组

GET/api/v1/networks可用网络(建机需要 network_id)
POST/api/v1/vms/{id}/port-forwardings单 IP NAT 端口转发
json
{
  "public_ip_id": "…",
  "external_port": 2222,
  "internal_port": 22,
  "protocol": "tcp"
}

Agent 侧 nftables 全量收敛,不破坏 TC 限速。

GET/api/v1/vms/{id}/port-forwardings该实例的转发映射
DELETE/api/v1/vms/{id}/port-forwardings/{fid}删除转发规则
GET/api/v1/security-groups安全组列表 + 规则(≤400 条)
POST/api/v1/security-groups创建安全组
json
{ "name": "web", "description": "80/443 入站" }
PUT/api/v1/vms/{id}/security-groups绑定安全组(全量替换)
GET/api/v1/vms/{id}/security-groups当前绑定的安全组
GET/api/v1/public-ips已登记公网 IP
POST/api/v1/public-ips登记公网 IPv4
DELETE/api/v1/public-ips/{id}停用公网 IP(软删除)

镜像

镜像链路:源同步(控制面解析远端目录 + 校验和)→ 远端镜像库 → 节点本地拉取(建机时同位调度)。

GET/api/v1/image-sources镜像源列表(不含下载地址与白名单)
POST/api/v1/image-sources添加镜像源(custom 源必须声明域名白名单)
PUT/api/v1/image-sources/{id}更新源(优先级 / 状态 / 白名单)
DELETE/api/v1/image-sources/{id}删除源(软禁用,保留缓存与审计)
POST/api/v1/image-sources/{id}/sync同步远端目录(异步)

扫描目录 + upsert,SHA-256 取自源的 SHA256SUMS。

GET/api/v1/remote-images远端镜像缓存
POST/api/v1/local-images/pull按需拉取到节点本地池
json
{ "remote_image_id": "…", "node_id": "…" }
GET/api/v1/local-images本地镜像库(不含 storage_ref 路径)

流量包

GET/api/v1/traffic-products在售商品
GET/api/v1/tenants/{id}/traffic-packages钱包余额 + 活跃流量包(FIFO 抵扣序)
POST /api/v1/tenants/{id}/traffic-packages 购买流量包(节点级 / 租户级) v1.0.0
json
{
  "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抵扣明细(包抵扣 / 超额)
json
# 充值
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:开通一台指定节点的机器并等它跑起来

bash
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:批量对账已知实例状态

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:购买节点级流量包

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:带重试的幂等写请求

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