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