optimize guko fork installer and backup tools
This commit is contained in:
@@ -1,7 +1,10 @@
|
|||||||
name: guko
|
name: guko
|
||||||
services:
|
services:
|
||||||
guko-bot:
|
guko-bot:
|
||||||
image: ghcr.io/shuijiao1/guko:latest
|
image: guko:local
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: telegram-bot/Dockerfile
|
||||||
container_name: guko-bot
|
container_name: guko-bot
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
env_file: .env
|
env_file: .env
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ INSTALL_DIR="${GUKO_INSTALL_DIR:-/opt/guko}"
|
|||||||
COMPOSE_FILE="${INSTALL_DIR}/docker-compose.yml"
|
COMPOSE_FILE="${INSTALL_DIR}/docker-compose.yml"
|
||||||
ENV_FILE="${INSTALL_DIR}/.env"
|
ENV_FILE="${INSTALL_DIR}/.env"
|
||||||
SERVERS_FILE="${INSTALL_DIR}/servers.json"
|
SERVERS_FILE="${INSTALL_DIR}/servers.json"
|
||||||
|
IMAGE_NAME="${GUKO_IMAGE:-guko:local}"
|
||||||
|
DEFAULT_TG_ID="${GUKO_TG_ID:-165067365}"
|
||||||
|
BACKUP_DIR="${GUKO_BACKUP_DIR:-${INSTALL_DIR}/backups}"
|
||||||
|
|
||||||
need_root() {
|
need_root() {
|
||||||
if [ "$(id -u)" -ne 0 ]; then
|
if [ "$(id -u)" -ne 0 ]; then
|
||||||
@@ -23,6 +26,50 @@ install_docker() {
|
|||||||
systemctl enable --now docker >/dev/null 2>&1 || true
|
systemctl enable --now docker >/dev/null 2>&1 || true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
render_compose() {
|
||||||
|
cat > "${COMPOSE_FILE}" <<COMPOSE
|
||||||
|
name: guko
|
||||||
|
services:
|
||||||
|
guko-bot:
|
||||||
|
image: ${IMAGE_NAME}
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: telegram-bot/Dockerfile
|
||||||
|
container_name: guko-bot
|
||||||
|
restart: unless-stopped
|
||||||
|
env_file: .env
|
||||||
|
volumes:
|
||||||
|
- ./servers.json:/data/servers.json
|
||||||
|
- ./keys:/data/keys
|
||||||
|
- ./media:/data/media
|
||||||
|
- ./results:/data/results
|
||||||
|
- ./history.json:/data/history.json
|
||||||
|
- ./tmp:/data/tmp
|
||||||
|
COMPOSE
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch_source() {
|
||||||
|
echo "==> 下载 GUKO 源码"
|
||||||
|
local tmp
|
||||||
|
tmp="$(mktemp -d)"
|
||||||
|
fetch "${BASE_URL}/telegram-bot/Dockerfile" "${tmp}/Dockerfile.check"
|
||||||
|
rm -rf "${INSTALL_DIR}/telegram-bot" "${INSTALL_DIR}/optional" "${INSTALL_DIR}/scripts" \
|
||||||
|
"${INSTALL_DIR}/auth.py" "${INSTALL_DIR}/guko.py" "${INSTALL_DIR}/jiaoops.py" \
|
||||||
|
"${INSTALL_DIR}/Makefile" "${INSTALL_DIR}/README.md" "${INSTALL_DIR}/LICENSE"
|
||||||
|
fetch "https://mjjtop.com/admin/guko/archive/main.tar.gz" "${tmp}/guko.tar.gz"
|
||||||
|
tar -xzf "${tmp}/guko.tar.gz" -C "${tmp}"
|
||||||
|
local src
|
||||||
|
src="$(find "${tmp}" -maxdepth 1 -type d \( -name 'guko' -o -name 'guko-*' \) | head -1)"
|
||||||
|
if [ -z "${src}" ] || [ ! -d "${src}" ]; then
|
||||||
|
src="$(find "${tmp}" -maxdepth 1 -mindepth 1 -type d | head -1)"
|
||||||
|
fi
|
||||||
|
cp -a "${src}/auth.py" "${src}/guko.py" "${src}/jiaoops.py" "${src}/telegram-bot" "${INSTALL_DIR}/"
|
||||||
|
[ -d "${src}/optional" ] && cp -a "${src}/optional" "${INSTALL_DIR}/" || true
|
||||||
|
[ -d "${src}/scripts" ] && cp -a "${src}/scripts" "${INSTALL_DIR}/" || true
|
||||||
|
cp -a "${src}/README.md" "${src}/LICENSE" "${INSTALL_DIR}/" 2>/dev/null || true
|
||||||
|
rm -rf "${tmp}"
|
||||||
|
}
|
||||||
|
|
||||||
fetch() {
|
fetch() {
|
||||||
local url="$1" out="$2"
|
local url="$1" out="$2"
|
||||||
curl -fL --retry 3 --connect-timeout 10 "$url" -o "$out"
|
curl -fL --retry 3 --connect-timeout 10 "$url" -o "$out"
|
||||||
@@ -33,14 +80,15 @@ init_files() {
|
|||||||
mkdir -p "${INSTALL_DIR}" "${INSTALL_DIR}/keys" "${INSTALL_DIR}/media" "${INSTALL_DIR}/results" "${INSTALL_DIR}/tmp"
|
mkdir -p "${INSTALL_DIR}" "${INSTALL_DIR}/keys" "${INSTALL_DIR}/media" "${INSTALL_DIR}/results" "${INSTALL_DIR}/tmp"
|
||||||
chmod 700 "${INSTALL_DIR}/keys"
|
chmod 700 "${INSTALL_DIR}/keys"
|
||||||
|
|
||||||
fetch "${BASE_URL}/docker-compose.example.yml" "${COMPOSE_FILE}"
|
fetch_source
|
||||||
|
render_compose
|
||||||
|
|
||||||
if [ ! -f "${ENV_FILE}" ]; then
|
if [ ! -f "${ENV_FILE}" ]; then
|
||||||
fetch "${BASE_URL}/.env.example" "${ENV_FILE}"
|
fetch "${BASE_URL}/.env.example" "${ENV_FILE}"
|
||||||
sed -i \
|
sed -i \
|
||||||
-e 's/^BOT_TOKEN=.*/BOT_TOKEN=replace-me/' \
|
-e 's/^BOT_TOKEN=.*/BOT_TOKEN=replace-me/' \
|
||||||
-e 's/^ALLOWED_USERS=.*/ALLOWED_USERS=123456789/' \
|
-e "s/^ALLOWED_USERS=.*/ALLOWED_USERS=${DEFAULT_TG_ID}/" \
|
||||||
-e 's/^ADMIN_USERS=.*/ADMIN_USERS=123456789/' \
|
-e "s/^ADMIN_USERS=.*/ADMIN_USERS=${DEFAULT_TG_ID}/" \
|
||||||
"${ENV_FILE}"
|
"${ENV_FILE}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -60,7 +108,11 @@ GUKO 一键部署脚本
|
|||||||
bash <(curl -fsSL https://mjjtop.com/guko) restart 重启
|
bash <(curl -fsSL https://mjjtop.com/guko) restart 重启
|
||||||
bash <(curl -fsSL https://mjjtop.com/guko) logs 看日志
|
bash <(curl -fsSL https://mjjtop.com/guko) logs 看日志
|
||||||
bash <(curl -fsSL https://mjjtop.com/guko) status 状态
|
bash <(curl -fsSL https://mjjtop.com/guko) status 状态
|
||||||
bash <(curl -fsSL https://mjjtop.com/guko) update 更新镜像并重启
|
bash <(curl -fsSL https://mjjtop.com/guko) update 更新源码/重建镜像并重启
|
||||||
|
bash <(curl -fsSL https://mjjtop.com/guko) pull 只更新源码和 compose
|
||||||
|
bash <(curl -fsSL https://mjjtop.com/guko) backup 备份配置/密钥/历史
|
||||||
|
bash <(curl -fsSL https://mjjtop.com/guko) doctor 检查部署状态
|
||||||
|
bash <(curl -fsSL https://mjjtop.com/guko) repo 打印仓库地址
|
||||||
|
|
||||||
安装目录:${INSTALL_DIR}
|
安装目录:${INSTALL_DIR}
|
||||||
首次安装后必须编辑:${ENV_FILE}
|
首次安装后必须编辑:${ENV_FILE}
|
||||||
@@ -75,6 +127,33 @@ compose() {
|
|||||||
docker compose -f "${COMPOSE_FILE}" "$@"
|
docker compose -f "${COMPOSE_FILE}" "$@"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
backup_data() {
|
||||||
|
mkdir -p "${BACKUP_DIR}"
|
||||||
|
chmod 700 "${BACKUP_DIR}"
|
||||||
|
local out
|
||||||
|
out="${BACKUP_DIR}/guko-backup-$(date +%Y%m%d-%H%M%S).tar.gz"
|
||||||
|
tar -C "${INSTALL_DIR}" -czf "${out}" \
|
||||||
|
.env servers.json history.json keys media results tmp 2>/dev/null || true
|
||||||
|
chmod 600 "${out}"
|
||||||
|
echo "✅ 备份完成:${out}"
|
||||||
|
}
|
||||||
|
|
||||||
|
doctor() {
|
||||||
|
echo "== GUKO doctor =="
|
||||||
|
echo "install_dir=${INSTALL_DIR}"
|
||||||
|
echo "compose=${COMPOSE_FILE}"
|
||||||
|
command -v docker >/dev/null 2>&1 && docker --version || echo "docker=missing"
|
||||||
|
docker compose version 2>/dev/null || echo "docker compose=missing"
|
||||||
|
[ -f "${ENV_FILE}" ] && echo ".env=ok" || echo ".env=missing"
|
||||||
|
[ -f "${SERVERS_FILE}" ] && echo "servers.json=ok" || echo "servers.json=missing"
|
||||||
|
if [ -f "${ENV_FILE}" ]; then
|
||||||
|
grep -E '^(BOT_TOKEN|ALLOWED_USERS|ADMIN_USERS)=' "${ENV_FILE}" | sed -E 's/(BOT_TOKEN=).*/\1***/'
|
||||||
|
fi
|
||||||
|
if [ -f "${COMPOSE_FILE}" ] && command -v docker >/dev/null 2>&1; then
|
||||||
|
compose ps || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
cmd="${1:-install}"
|
cmd="${1:-install}"
|
||||||
case "$cmd" in
|
case "$cmd" in
|
||||||
install|init)
|
install|init)
|
||||||
@@ -96,7 +175,7 @@ MSG
|
|||||||
up|start)
|
up|start)
|
||||||
need_root
|
need_root
|
||||||
[ -f "${COMPOSE_FILE}" ] || { echo "未安装,先执行 install" >&2; exit 1; }
|
[ -f "${COMPOSE_FILE}" ] || { echo "未安装,先执行 install" >&2; exit 1; }
|
||||||
compose pull
|
compose build --pull
|
||||||
compose up -d
|
compose up -d
|
||||||
compose ps
|
compose ps
|
||||||
;;
|
;;
|
||||||
@@ -111,10 +190,27 @@ MSG
|
|||||||
;;
|
;;
|
||||||
update)
|
update)
|
||||||
need_root
|
need_root
|
||||||
compose pull
|
install_docker
|
||||||
|
init_files
|
||||||
|
compose build --pull
|
||||||
compose up -d
|
compose up -d
|
||||||
compose ps
|
compose ps
|
||||||
;;
|
;;
|
||||||
|
pull)
|
||||||
|
need_root
|
||||||
|
init_files
|
||||||
|
;;
|
||||||
|
backup)
|
||||||
|
need_root
|
||||||
|
backup_data
|
||||||
|
;;
|
||||||
|
doctor)
|
||||||
|
need_root
|
||||||
|
doctor
|
||||||
|
;;
|
||||||
|
repo)
|
||||||
|
echo "https://mjjtop.com/guko-repo"
|
||||||
|
;;
|
||||||
logs)
|
logs)
|
||||||
need_root
|
need_root
|
||||||
compose logs -f --tail=200
|
compose logs -f --tail=200
|
||||||
|
|||||||
@@ -217,6 +217,58 @@ def next_manual_id(servers):
|
|||||||
return n
|
return n
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def backup_bundle_path() -> Path:
|
||||||
|
ts = datetime.now().strftime('%Y%m%d-%H%M%S')
|
||||||
|
out = TMP_DIR / f'guko-backup-{ts}.tar.gz'
|
||||||
|
out.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
files = []
|
||||||
|
for path in (SERVERS_JSON, HISTORY_JSON):
|
||||||
|
if path.exists():
|
||||||
|
files.append(path)
|
||||||
|
for d in (KEYS_DIR, MEDIA_DIR, RESULTS_DIR):
|
||||||
|
if d.exists():
|
||||||
|
files.append(d)
|
||||||
|
import tarfile
|
||||||
|
with tarfile.open(out, 'w:gz') as tar:
|
||||||
|
for item in files:
|
||||||
|
arc = item.name if item.is_file() else item.name
|
||||||
|
tar.add(item, arcname=arc)
|
||||||
|
try:
|
||||||
|
os.chmod(out, 0o600)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def inventory_summary_text() -> str:
|
||||||
|
inv = load_inventory()
|
||||||
|
servers = inv.get('servers') or []
|
||||||
|
total = len(servers)
|
||||||
|
password = key = skipped = 0
|
||||||
|
for item in servers:
|
||||||
|
try:
|
||||||
|
cfg = resolve_ssh(item, inv)
|
||||||
|
if cfg.get('auth') == 'password':
|
||||||
|
password += 1
|
||||||
|
elif cfg.get('key'):
|
||||||
|
key += 1
|
||||||
|
else:
|
||||||
|
skipped += 1
|
||||||
|
except Exception:
|
||||||
|
skipped += 1
|
||||||
|
return (
|
||||||
|
'📦 <b>GUKO 配置概览</b>\n\n'
|
||||||
|
f'服务器:<b>{total}</b> 台\n'
|
||||||
|
f'密钥登录:{key} 台\n'
|
||||||
|
f'密码登录:{password} 台\n'
|
||||||
|
f'未完整配置:{skipped} 台\n'
|
||||||
|
f'清单:<code>{safe(str(SERVERS_JSON))}</code>\n'
|
||||||
|
f'密钥目录:<code>{safe(str(KEYS_DIR))}</code>\n'
|
||||||
|
f'结果目录:<code>{safe(str(RESULTS_DIR))}</code>'
|
||||||
|
)
|
||||||
|
|
||||||
def redact_inventory(inv: dict):
|
def redact_inventory(inv: dict):
|
||||||
def clean_server(s):
|
def clean_server(s):
|
||||||
out = json.loads(json.dumps(s, ensure_ascii=False))
|
out = json.loads(json.dumps(s, ensure_ascii=False))
|
||||||
@@ -3253,6 +3305,18 @@ async def export_config_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|||||||
await update.message.reply_document(document=f, filename='guko-servers-redacted.json', caption='已导出脱敏配置(密码已隐藏)。')
|
await update.message.reply_document(document=f, filename='guko-servers-redacted.json', caption='已导出脱敏配置(密码已隐藏)。')
|
||||||
|
|
||||||
|
|
||||||
|
async def backup_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
|
if not await admin_guard(update): return
|
||||||
|
path = backup_bundle_path()
|
||||||
|
with path.open('rb') as f:
|
||||||
|
await update.message.reply_document(document=f, filename=path.name, caption='GUKO 完整备份:servers/history/keys/media/results。请妥善保存。')
|
||||||
|
|
||||||
|
|
||||||
|
async def summary_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
|
if not await guard(update): return
|
||||||
|
await update.message.reply_text(inventory_summary_text(), parse_mode=ParseMode.HTML)
|
||||||
|
|
||||||
|
|
||||||
async def testall_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
async def testall_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
if not await admin_guard(update): return
|
if not await admin_guard(update): return
|
||||||
servers = [s for s in load_inventory().get('servers', []) if s.get('host')]
|
servers = [s for s in load_inventory().get('servers', []) if s.get('host')]
|
||||||
@@ -3475,6 +3539,8 @@ async def post_init(app: Application):
|
|||||||
BotCommand('testssh', '测试服务器 SSH'),
|
BotCommand('testssh', '测试服务器 SSH'),
|
||||||
BotCommand('testall', '批量测试 SSH'),
|
BotCommand('testall', '批量测试 SSH'),
|
||||||
BotCommand('exportconfig', '导出脱敏配置'),
|
BotCommand('exportconfig', '导出脱敏配置'),
|
||||||
|
BotCommand('backup', '导出完整备份'),
|
||||||
|
BotCommand('summary', '配置概览'),
|
||||||
BotCommand('info', '查看单台操作面板:/info 名字/IP/ID'),
|
BotCommand('info', '查看单台操作面板:/info 名字/IP/ID'),
|
||||||
BotCommand('jobs', '查看后台任务'),
|
BotCommand('jobs', '查看后台任务'),
|
||||||
BotCommand('history', '查看测试历史:/history 服务器'),
|
BotCommand('history', '查看测试历史:/history 服务器'),
|
||||||
@@ -3925,6 +3991,8 @@ def main():
|
|||||||
app.add_handler(CommandHandler('testssh', testssh_cmd))
|
app.add_handler(CommandHandler('testssh', testssh_cmd))
|
||||||
app.add_handler(CommandHandler('testall', testall_cmd))
|
app.add_handler(CommandHandler('testall', testall_cmd))
|
||||||
app.add_handler(CommandHandler('exportconfig', export_config_cmd))
|
app.add_handler(CommandHandler('exportconfig', export_config_cmd))
|
||||||
|
app.add_handler(CommandHandler('backup', backup_cmd))
|
||||||
|
app.add_handler(CommandHandler('summary', summary_cmd))
|
||||||
app.add_handler(CommandHandler('info', info_cmd))
|
app.add_handler(CommandHandler('info', info_cmd))
|
||||||
app.add_handler(CommandHandler('jobs', jobs_cmd))
|
app.add_handler(CommandHandler('jobs', jobs_cmd))
|
||||||
app.add_handler(CommandHandler('history', history_cmd))
|
app.add_handler(CommandHandler('history', history_cmd))
|
||||||
|
|||||||
Reference in New Issue
Block a user