CVM按量计费自动开关机方案+脚本+workflow

This commit is contained in:
铸渊 (ICE-GL-ZY001) 2026-05-12 11:14:14 +00:00
parent 397eb2baa3
commit 0b102638a6
6 changed files with 457 additions and 0 deletions

View File

@ -0,0 +1,24 @@
# 光湖 · CVM关机
name: CVM关机
on:
workflow_dispatch:
jobs:
power-off:
runs-on: ubuntu
steps:
- name: 同步代码
run: |
cd /opt/zhuyuan-cn/repo-mirror/20260509-1849/guanghulab
git fetch origin
git reset --hard origin/main
- name: 关机
env:
TENCENT_SECRET_ID: ${{ secrets.TENCENT_SECRET_ID }}
TENCENT_SECRET_KEY: ${{ secrets.TENCENT_SECRET_KEY }}
CVM_INSTANCE_ID: ${{ secrets.CVM_INSTANCE_ID }}
CVM_REGION: ${{ secrets.CVM_REGION }}
run: |
bash scripts/cvm-power/cvm-power.sh stop

View File

@ -0,0 +1,24 @@
# 光湖 · CVM开机
name: CVM开机
on:
workflow_dispatch:
jobs:
power-on:
runs-on: ubuntu
steps:
- name: 同步代码
run: |
cd /opt/zhuyuan-cn/repo-mirror/20260509-1849/guanghulab
git fetch origin
git reset --hard origin/main
- name: 开机
env:
TENCENT_SECRET_ID: ${{ secrets.TENCENT_SECRET_ID }}
TENCENT_SECRET_KEY: ${{ secrets.TENCENT_SECRET_KEY }}
CVM_INSTANCE_ID: ${{ secrets.CVM_INSTANCE_ID }}
CVM_REGION: ${{ secrets.CVM_REGION }}
run: |
bash scripts/cvm-power/cvm-power.sh start

View File

@ -0,0 +1,24 @@
# 光湖 · CVM关机
name: CVM关机
on:
workflow_dispatch:
jobs:
power-off:
runs-on: ubuntu
steps:
- name: 同步代码
run: |
cd /opt/zhuyuan-cn/repo-mirror/20260509-1849/guanghulab
git fetch origin
git reset --hard origin/main
- name: 关机
env:
TENCENT_SECRET_ID: ${{ secrets.TENCENT_SECRET_ID }}
TENCENT_SECRET_KEY: ${{ secrets.TENCENT_SECRET_KEY }}
CVM_INSTANCE_ID: ${{ secrets.CVM_INSTANCE_ID }}
CVM_REGION: ${{ secrets.CVM_REGION }}
run: |
bash scripts/cvm-power/cvm-power.sh stop

View File

@ -0,0 +1,24 @@
# 光湖 · CVM开机
name: CVM开机
on:
workflow_dispatch:
jobs:
power-on:
runs-on: ubuntu
steps:
- name: 同步代码
run: |
cd /opt/zhuyuan-cn/repo-mirror/20260509-1849/guanghulab
git fetch origin
git reset --hard origin/main
- name: 开机
env:
TENCENT_SECRET_ID: ${{ secrets.TENCENT_SECRET_ID }}
TENCENT_SECRET_KEY: ${{ secrets.TENCENT_SECRET_KEY }}
CVM_INSTANCE_ID: ${{ secrets.CVM_INSTANCE_ID }}
CVM_REGION: ${{ secrets.CVM_REGION }}
run: |
bash scripts/cvm-power/cvm-power.sh start

151
.github/cvm-auto-power-plan.md vendored Normal file
View File

@ -0,0 +1,151 @@
# ════════════════════════════════════════════════════════════════
# 光湖 · 按量计费CVM自动开关机方案
# 设计:铸渊 · ICE-GL-ZY001
# 需求来源:冰朔 TCS-0002∞ 2026-05-13
# ════════════════════════════════════════════════════════════════
#
# 冰朔原话:
# "你是否能开发一个你需要开发你就自动去开机。
# 你开发完了今天我说结束了。你就自动把机子关了的
# 这个自动Agent工作流"
#
# ════════════════════════════════════════════════════════════════
## 一、按量计费更划算吗?
**是的,对于开发用途。**
| 模式 | 4C8G广州月费 | 适合场景 |
|------|------------|---------|
| 包年包月 | ~200-300元/月 | 24/7运行 |
| 按量计费 | ~0.4-0.6元/小时 | 每天用8小时 ≈ 100-150元/月 |
按量计费的关键:**用的时候开机,不用的时候关机,关机不收计算费用(只收云盘费用)**。
铸渊的典型工作模式:
- 冰朔和铸渊对话 → 开机
- 铸渊开发+测试 → 保持开机
- 冰朔说"今天结束了" → 关机
- 深夜/无人时 → 自动关机(可选)
## 二、自动开关机架构
```
冰朔说"开始开发" 冰朔说"今天结束"
│ │
▼ ▼
┌──────────────┐ ┌──────────────┐
│ 广州轻量服务器 │ │ 广州轻量服务器 │
│ (常驻2C2G) │ │ (常驻2C2G) │
│ │ │ │
│ ① 调用腾讯云API│ │ ③ 调用腾讯云API│
│ 开机CVM │ │ 关机CVM │
│ │ │ │
│ ② 等CVM启动 │ │ ④ 确认关机 │
│ 通知铸渊 │ │ 通知冰朔 │
└──────────────┘ └──────────────┘
│ ▲
▼ │
┌──────────────────────────────────────────┐
│ 广州CVM按量计费4C8G+CBS云盘
│ │
│ 开机状态: │
│ Forgejo (代码仓库+Actions) │
│ Runner (执行workflow) │
│ Vault (密钥管理) │
│ Terminal Watcher (状态监控) │
│ Patrol Agent (自动巡检) │
│ 对话Agent (冰朔问答) │
│ │
│ 关机状态: │
│ CBS云盘保留所有数据 │
│ 下次开机数据完整恢复 │
└──────────────────────────────────────────┘
```
## 三、开关机实现方式
### 方案A腾讯云API推荐
```bash
# 开机
curl -s -X POST https://cvm.tencentcloudapi.com \
-d '{"InstanceIds":["ins-xxxxx"]}' \
-H "Authorization: TC3-HMAC-SHA256 ..." \
-H "Action: StartInstances"
# 关机
curl -s -X POST https://cvm.tencentcloudapi.com \
-d '{"InstanceIds":["ins-xxxxx"]}' \
-H "Authorization: TC3-HMAC-SHA256 ..." \
-H "Action: StopInstances"
```
需要在腾讯云创建一个子账号只给CVM开关机权限。
### 方案BGitea Workflow触发
在轻量服务器上创建两个workflow
- `power-on.yaml`调用腾讯云API开机 → 等待SSH可连 → 通知
- `power-off.yaml`调用腾讯云API关机 → 确认 → 通知
### 方案C冰朔在Gitea网页操作
最简单的方式冰朔在Gitea的Actions页面点"Run workflow"来开关机。
## 四、需要冰朔操作的(一次性)
1. **购买CVM**4C8G + 50GB CBS云盘广州地域按量计费
2. **配置CBS云盘**:挂载到 /data仓库数据全部存这里
3. **创建腾讯云子账号**只给CVM开关机权限密钥存入Vault
4. **安装基础环境**Node.js 20 + PM2 + Nginx + Forgejo
## 五、迁移计划
```
阶段1买服务器冰朔操作10分钟
→ 买4C8G CVM + 50GB CBS广州地域按量计费
→ 选择Ubuntu 22.04
→ 开机
阶段2基础环境冰朔执行一条命令15分钟
→ bash <(curl -fsSL 铸渊写的bootstrap脚本)
→ 自动安装Node.js + PM2 + Nginx + Forgejo
阶段3数据迁移铸渊通过Runner自动执行30分钟
→ 从广州轻量服务器rsync数据到CVM
→ 或从COS拉取备份
→ Forgejo仓库数据在CBS云盘上
阶段4域名切换冰朔在腾讯云DNS操作5分钟
→ guanghulab.com 指向CVM的IP
→ Nginx配置SSL
阶段5旧服务器降级铸渊自动执行
→ 广州2C2G变成测试机
→ 只保留Forgejo + Runner轻量级
阶段6自动开关机部署铸渊开发
→ 腾讯云API密钥入Vault
→ 开关机workflow上线
→ 冰朔在Gitea网页点按钮即可开关机
```
## 六、CVM购买建议
| 项目 | 建议 |
|------|------|
| 地域 | 广州(和轻量服务器同地域,内网互通免费) |
| 配置 | 4C8G |
| 系统盘 | 50GB SSD云硬盘 |
| 数据盘 | 50GB CBS高性能云盘挂载到/data |
| 带宽 | 按流量计费1Mbps基础够用 |
| 计费 | 按量计费 |
| 安全组 | 开放22(SSH) + 80(HTTP) + 443(HTTPS) + 3001(Forgejo内网) |
**同地域的好处**轻量服务器和CVM内网互通迁移数据走内网不花钱。
---
*铸渊 · 按量计费CVM方案 v1.0 · 2026-05-13*
*ICE-GL-ZY001 · TCS-0002∞ · 国作登字-2026-A-00037559*

210
scripts/cvm-power/cvm-power.sh Executable file
View File

@ -0,0 +1,210 @@
#!/usr/bin/env bash
# ════════════════════════════════════════════════════════════════
# 光湖 · 按量计费CVM自动开关机
# 开机/关机/状态查询
# ════════════════════════════════════════════════════════════════
#
# 用法:
# bash cvm-power.sh start # 开机
# bash cvm-power.sh stop # 关机
# bash cvm-power.sh status # 查看状态
#
# 环境变量从Vault或Forgejo Secrets读取:
# TENCENT_SECRET_ID 腾讯云API SecretId
# TENCENT_SECRET_KEY 腾讯云API SecretKey
# CVM_INSTANCE_ID CVM实例ID (ins-xxxxx)
# CVM_REGION 地域 (ap-guangzhou)
#
# ════════════════════════════════════════════════════════════════
set -euo pipefail
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m'
# 配置
SECRET_ID="${TENCENT_SECRET_ID:-}"
SECRET_KEY="${TENCENT_SECRET_KEY:-}"
INSTANCE_ID="${CVM_INSTANCE_ID:-}"
REGION="${CVM_REGION:-ap-guangzhou}"
SERVICE="cvm"
HOST="cvm.tencentcloudapi.com"
API_VERSION="2017-03-12"
# 检查配置
if [[ -z "$SECRET_ID" || -z "$SECRET_KEY" || -z "$INSTANCE_ID" ]]; then
echo -e "${RED}❌ 缺少环境变量${NC}"
echo "需要: TENCENT_SECRET_ID, TENCENT_SECRET_KEY, CVM_INSTANCE_ID"
echo "请在Forgejo Secrets中配置这些值"
exit 1
fi
# ─── 腾讯云API签名 ──────────────────────────────────────────────
call_api() {
local action=$1
local payload=$2
local timestamp=$(date +%s)
local date=$(date -u -d "@$timestamp" +%Y-%m-%d 2>/dev/null || date -u +%Y-%m-%d)
# 1. 拼接规范请求串
local http_method="POST"
local canonical_uri="/"
local canonical_querystring=""
local ct="application/json; charset=utf-8"
local hashed_payload=$(echo -n "$payload" | sha256sum | awk '{print $1}')
local canonical_headers="content-type:${ct}\nhost:${HOST}\nx-tc-action:${action,,}\n"
local signed_headers="content-type;host;x-tc-action"
local canonical_request="${http_method}\n${canonical_uri}\n${canonical_querystring}\n${canonical_headers}\n${signed_headers}\n${hashed_payload}"
# 2. 拼接待签名字符串
local algorithm="TC3-HMAC-SHA256"
local credential_scope="${date}/${SERVICE}/tc3_request"
local hashed_canonical=$(echo -n "$canonical_request" | sha256sum | awk '{print $1}')
local string_to_sign="${algorithm}\n${timestamp}\n${credential_scope}\n${hashed_canonical}"
# 3. 计算签名
local secret_date=$(echo -n "$date" | openssl dgst -sha256 -hmac "TC3${SECRET_KEY}" -binary | xxd -p -c 256)
local secret_service=$(echo -n "$SERVICE" | openssl dgst -sha256 -mac HMAC -macopt hexkey:"$secret_date" -binary | xxd -p -c 256)
local secret_signing=$(echo -n "tc3_request" | openssl dgst -sha256 -mac HMAC -macopt hexkey:"$secret_service" -binary | xxd -p -c 256)
local signature=$(echo -n "$string_to_sign" | openssl dgst -sha256 -mac HMAC -macopt hexkey:"$secret_signing" | awk '{print $NF}')
# 4. 构造Authorization
local authorization="${algorithm} Credential=${SECRET_ID}/${credential_scope}, SignedHeaders=${signed_headers}, Signature=${signature}"
# 5. 发送请求
curl -s -X POST "https://${HOST}" \
-H "Authorization: ${authorization}" \
-H "Content-Type: ${ct}" \
-H "Host: ${HOST}" \
-H "X-TC-Action: ${action}" \
-H "X-TC-Timestamp: ${timestamp}" \
-H "X-TC-Version: ${API_VERSION}" \
-H "X-TC-Region: ${REGION}" \
-d "$payload"
}
# ─── 操作函数 ───────────────────────────────────────────────────
do_start() {
echo -e "${YELLOW}[开机] 正在启动CVM ${INSTANCE_ID}...${NC}"
local payload="{\"InstanceIds\":[\"${INSTANCE_ID}\"]}"
local resp=$(call_api "StartInstances" "$payload")
if echo "$resp" | python3 -c "
import json,sys
d=json.load(sys.stdin)
if 'Response' in d and 'Error' not in d['Response']:
print('OK')
else:
err=d.get('Response',{}).get('Error',{}).get('Message','未知错误')
print(f'ERROR: {err}')
" 2>/dev/null | grep -q "OK"; then
echo -e "${GREEN}✅ 开机指令已发送${NC}"
echo " 等待实例启动约1-2分钟..."
wait_for_ssh
else
echo -e "${RED}❌ 开机失败${NC}"
echo "$resp" | python3 -c "import json,sys;d=json.load(sys.stdin);print(d.get('Response',{}).get('Error',{}).get('Message','未知'))" 2>/dev/null
exit 1
fi
}
do_stop() {
echo -e "${YELLOW}[关机] 正在停止CVM ${INSTANCE_ID}...${NC}"
local payload="{\"InstanceIds\":[\"${INSTANCE_ID}\"]}"
local resp=$(call_api "StopInstances" "$payload")
if echo "$resp" | python3 -c "
import json,sys
d=json.load(sys.stdin)
if 'Response' in d and 'Error' not in d['Response']:
print('OK')
else:
err=d.get('Response',{}).get('Error',{}).get('Message','未知错误')
print(f'ERROR: {err}')
" 2>/dev/null | grep -q "OK"; then
echo -e "${GREEN}✅ 关机指令已发送${NC}"
echo " 实例将在几分钟内完全停止"
echo " 停止后不再收取计算费用(仅收云盘费用)"
else
echo -e "${RED}❌ 关机失败${NC}"
echo "$resp" | python3 -c "import json,sys;d=json.load(sys.stdin);print(d.get('Response',{}).get('Error',{}).get('Message','未知'))" 2>/dev/null
exit 1
fi
}
do_status() {
echo -e "${YELLOW}[状态] 查询CVM ${INSTANCE_ID}...${NC}"
local payload="{\"InstanceIds\":[\"${INSTANCE_ID}\"]}"
local resp=$(call_api "DescribeInstancesStatus" "$payload")
echo "$resp" | python3 -c "
import json,sys
d=json.load(sys.stdin)
r=d.get('Response',{})
statuses=r.get('InstanceStatusSet',[])
if not statuses:
print(' 状态: 未找到实例')
else:
state=statuses[0].get('InstanceState','UNKNOWN')
icon={'RUNNING':'🟢','STOPPED':'🔴','STARTING':'🟡','STOPPING':'🟡'}.get(state,'⚪')
print(f' {icon} 实例状态: {state}')
" 2>/dev/null || echo " 查询失败"
}
wait_for_ssh() {
local max_wait=120
local waited=0
local ip=$(get_instance_ip)
if [[ -z "$ip" ]]; then
echo " ⚠️ 无法获取公网IP请手动检查"
return
fi
echo -n " 等待SSH可连 (${ip}:22) "
while [[ $waited -lt $max_wait ]]; do
if timeout 3 bash -c "echo >/dev/tcp/${ip}/22" 2>/dev/null; then
echo ""
echo -e "${GREEN} ✅ CVM已就绪: ${ip}${NC}"
return
fi
echo -n "."
sleep 5
waited=$((waited + 5))
done
echo ""
echo " ⚠️ SSH连接超时实例可能还在启动中"
}
get_instance_ip() {
local payload="{\"InstanceIds\":[\"${INSTANCE_ID}\"]}"
call_api "DescribeInstances" "$payload" | python3 -c "
import json,sys
d=json.load(sys.stdin)
instances=d.get('Response',{}).get('InstanceSet',[])
if instances:
ip=instances[0].get('PublicIpAddresses',[])[0] if instances[0].get('PublicIpAddresses') else ''
print(ip)
" 2>/dev/null || echo ""
}
# ─── 入口 ─────────────────────────────────────────────────────
case "${1:-}" in
start) do_start ;;
stop) do_stop ;;
status) do_status ;;
*)
echo "用法: bash cvm-power.sh {start|stop|status}"
echo ""
echo " start - 开机按量计费CVM"
echo " stop - 关机(停止计费)"
echo " status - 查询状态"
exit 1
;;
esac