⚡ CVM按量计费自动开关机方案+脚本+workflow
This commit is contained in:
parent
397eb2baa3
commit
0b102638a6
24
.forgejo/workflows/cvm-power-off.yaml
Normal file
24
.forgejo/workflows/cvm-power-off.yaml
Normal 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
|
||||
24
.forgejo/workflows/cvm-power-on.yaml
Normal file
24
.forgejo/workflows/cvm-power-on.yaml
Normal 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
|
||||
24
.gitea/workflows/cvm-power-off.yaml
Normal file
24
.gitea/workflows/cvm-power-off.yaml
Normal 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
|
||||
24
.gitea/workflows/cvm-power-on.yaml
Normal file
24
.gitea/workflows/cvm-power-on.yaml
Normal 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
151
.github/cvm-auto-power-plan.md
vendored
Normal 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开关机权限。
|
||||
|
||||
### 方案B:Gitea 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
210
scripts/cvm-power/cvm-power.sh
Executable 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
|
||||
Loading…
x
Reference in New Issue
Block a user