211 lines
7.6 KiB
Bash
211 lines
7.6 KiB
Bash
|
|
#!/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
|