2026-05-12 11:14:14 +00:00

211 lines
7.6 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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