Files
pzmanager/deploy/install.sh
T
mrleo1nidandClaude Opus 5 e25294db16
CI / test (push) Successful in 21s
Release / release (push) Successful in 29s
Код настройки печатает установщик, появилось удаление панели
Менеджер кладёт код первичной настройки в data/setup-token, установщик
дожидается файла и печатает код последним сообщением — за ним больше не
нужно идти в journalctl. Файл удаляется, как только создан администратор,
и не появляется на уже настроенной панели.

Установщик научился снимать панель с машины: --uninstall убирает службу,
бинарники и правило sudo, --purge вдобавок сносит каталог с мирами,
бэкапами и учётками. Данные удаляются только по явному подтверждению, а
каталог сносится, лишь если похож на установку панели: опечатка в PZ_HOME
не должна стоить чужих файлов. Копия установщика теперь лежит в
/opt/pzmanager, так что удалять можно без похода в репозиторий.

get.sh пробрасывает аргументы установщику и в режиме удаления не качает
бинарник. Заодно исправлена подстановка пустого массива аргументов:
"${ARGS[@]:-}" разворачивается в один пустой аргумент, из-за чего обычная
установка спотыкалась бы о него.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 20:03:13 +03:00

322 lines
14 KiB
Bash
Raw Permalink 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
# Установка, обновление и удаление pzmanager на Ubuntu/Debian. От root:
#
# sudo ./deploy/install.sh установить или обновить
# sudo ./deploy/install.sh --uninstall убрать панель, данные оставить
# sudo ./deploy/install.sh --uninstall --purge убрать вместе с мирами
#
# Установка идемпотентна: на уже настроенной машине обновляются бинарник и
# юнит, а конфиг, миры и учётные записи панели остаются нетронутыми.
#
# Переменные окружения:
# PZ_HOME куда ставить всё хозяйство (по умолчанию /opt/pzmanager)
# PZ_USER системный пользователь (по умолчанию pzserver)
# PZ_LISTEN адрес веб-панели (только при первой установке)
# PZ_REPO репозиторий для обновлений (по умолчанию этот)
# BINARY путь к собранному бинарнику (по умолчанию ./pzmanager)
#
# Пример — панель доступна в локальной сети по адресу машины:
# sudo PZ_LISTEN=10.10.1.142:8080 ./deploy/install.sh
set -euo pipefail
PZ_USER="${PZ_USER:-pzserver}"
PZ_HOME="${PZ_HOME:-/opt/pzmanager}"
PZ_LISTEN="${PZ_LISTEN:-127.0.0.1:8080}"
PZ_REPO="${PZ_REPO:-https://gitea.hsrv.site/mrleo1nid/pzmanager}"
BINARY="${BINARY:-./pzmanager}"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
UNIT_SRC="${SCRIPT_DIR}/pzmanager.service"
ADMIN_SRC="${SCRIPT_DIR}/pzmanager-admin.sh"
TARGET_BINARY=/usr/local/bin/pzmanager
ADMIN_BINARY=/usr/local/bin/pzmanager-admin
UNINSTALL=0
PURGE=0
for arg in "$@"; do
case "$arg" in
--uninstall | --remove) UNINSTALL=1 ;;
--purge) UNINSTALL=1; PURGE=1 ;;
--yes | -y) PZ_ASSUME_YES=1 ;;
-h | --help)
sed -n '2,20p' "$0" | sed 's/^# \{0,1\}//'
exit 0
;;
*)
echo "Неизвестный аргумент: $arg" >&2
exit 2
;;
esac
done
if [[ $EUID -ne 0 ]]; then
echo "Запустите скрипт от root: sudo $0" >&2
exit 1
fi
# uninstall снимает панель с машины. Данные (миры, бэкапы, учётки) удаляются
# только по --purge: потерять сохранения из-за неудачно набранной команды
# гораздо обиднее, чем лишний раз убрать каталог руками.
uninstall() {
echo "==> Удаляю pzmanager"
if systemctl list-unit-files pzmanager.service &>/dev/null &&
systemctl is-active --quiet pzmanager; then
echo "==> Останавливаю службу (игровой сервер завершится штатно, это небыстро)"
systemctl stop pzmanager || true
fi
systemctl disable --quiet pzmanager 2>/dev/null || true
rm -f /etc/systemd/system/pzmanager.service
systemctl daemon-reload
systemctl reset-failed pzmanager 2>/dev/null || true
rm -f /usr/local/bin/pzmanager /usr/local/bin/pzmanager-admin /etc/sudoers.d/pzmanager
echo "Служба, бинарники и правило sudo удалены."
if [[ "$PURGE" != "1" ]]; then
echo
echo "Данные остались в ${PZ_HOME} (миры, бэкапы, конфиг, учётные записи)."
echo "Удалить и их: sudo $0 --uninstall --purge"
return
fi
# Защита от опечатки в PZ_HOME: сносим только то, что похоже на нашу
# установку, и никогда — корень или домашние каталоги.
local home_clean="${PZ_HOME%/}"
if [[ -z "$home_clean" || "$home_clean" == "/" || "$home_clean" != /* ]]; then
echo "!! PZ_HOME=${PZ_HOME} не похож на каталог установки — не трогаю" >&2
return
fi
if [[ ! -e "$home_clean/config.yaml" && ! -d "$home_clean/data" ]]; then
echo "!! В ${home_clean} нет ни config.yaml, ни data/ — не похоже на установку pzmanager," >&2
echo " поэтому каталог оставлен нетронутым." >&2
return
fi
if [[ "${PZ_ASSUME_YES:-0}" != "1" ]]; then
echo
echo "Будет безвозвратно удалено: ${home_clean}"
echo "Это миры, бэкапы, конфиг и учётные записи панели."
if [[ -t 0 ]]; then
read -r -p "Введите YES для подтверждения: " answer
[[ "$answer" == "YES" ]] || { echo "Отменено."; return; }
else
echo "Запуск неинтерактивный — повторите с флагом --yes, если уверены." >&2
return
fi
fi
rm -rf "$home_clean"
if id "$PZ_USER" &>/dev/null; then
deluser --system "$PZ_USER" &>/dev/null || userdel "$PZ_USER" &>/dev/null || true
echo "Пользователь ${PZ_USER} удалён."
fi
echo "Удалено полностью: ${home_clean}"
}
if [[ "$UNINSTALL" == "1" ]]; then
uninstall
exit 0
fi
if [[ ! -f "$BINARY" ]]; then
echo "Не найден бинарник $BINARY. Соберите его: make build-linux" >&2
exit 1
fi
# Определяем, что это: первая установка или обновление.
OLD_VERSION=""
if [[ -x "$TARGET_BINARY" ]]; then
OLD_VERSION="$("$TARGET_BINARY" version 2>/dev/null | awk '{print $2}' || true)"
fi
NEW_VERSION="$("$BINARY" version 2>/dev/null | awk '{print $2}' || echo "?")"
if [[ -n "$OLD_VERSION" ]]; then
echo "==> Обновление: установлено ${OLD_VERSION}, ставим ${NEW_VERSION}"
MODE=update
else
echo "==> Первая установка версии ${NEW_VERSION}"
MODE=install
fi
# Зависимости ставим только при первой установке: на обновлении это лишние
# минуты ожидания apt.
if [[ "$MODE" == "install" || "${FORCE_DEPS:-0}" == "1" ]]; then
echo "==> Устанавливаю системные зависимости"
apt-get update -qq
# lib32gcc-s1 нужен SteamCMD, остальное — самому серверу PZ. Свою JRE сервер
# приносит с собой, системная нужна как запасной вариант.
apt-get install -y --no-install-recommends \
ca-certificates curl sudo lib32gcc-s1 libsdl2-2.0-0 openjdk-17-jre-headless
fi
if ! id "$PZ_USER" &>/dev/null; then
echo "==> Создаю пользователя $PZ_USER"
adduser --system --group --home "$PZ_HOME" --shell /usr/sbin/nologin "$PZ_USER"
fi
echo "==> Готовлю директории в $PZ_HOME"
install -d -o "$PZ_USER" -g "$PZ_USER" -m 0755 \
"$PZ_HOME" \
"$PZ_HOME/data" \
"$PZ_HOME/server" \
"$PZ_HOME/Zomboid" \
"$PZ_HOME/backups"
# Конфиг создаётся только при первой установке: обновление не должно затирать
# настройки, которые уже поправили руками или через панель.
CONFIG="$PZ_HOME/config.yaml"
if [[ -f "$CONFIG" ]]; then
echo "==> Конфиг $CONFIG уже есть — оставляю как есть"
else
echo "==> Создаю конфиг $CONFIG (панель будет слушать $PZ_LISTEN)"
cat > "$CONFIG" <<EOF
# Адрес веб-панели. 127.0.0.1 — только через SSH-туннель,
# 0.0.0.0 — на всех интерфейсах, конкретный IP — только на нём.
listen: ${PZ_LISTEN}
data_dir: ${PZ_HOME}/data # учётки панели и скачанный SteamCMD
server_dir: ${PZ_HOME}/server # серверные файлы Project Zomboid
zomboid_dir: ${PZ_HOME}/Zomboid # миры, конфиги и логи игры
profiles:
- id: servertest
title: Основной сервер
admin_password: "" # пароль админа игры, можно задать в панели
java_memory: 4g # -Xmx для JVM
extra_args: []
active_profile: servertest
autostart: false # поднимать игровой сервер вместе с панелью
autorestart: true # поднимать сервер после падения
stop_timeout: 1m30s # ждать после quit до SIGTERM
log_buffer_lines: 5000
backup:
dir: ${PZ_HOME}/backups
schedule: 6h # 0 — отключить автобэкапы
keep: 20 # архивов на профиль
stop_server: false # останавливать сервер на время бэкапа
EOF
chown "$PZ_USER:$PZ_USER" "$CONFIG"
# В конфиге лежит пароль админа игрового сервера.
chmod 0640 "$CONFIG"
fi
# Останавливаем сервис перед заменой бинарника: менеджер должен корректно
# завершить игровой сервер, а не быть подменённым на ходу.
RESTART_AFTER=0
if [[ "$MODE" == "update" ]] && systemctl is-active --quiet pzmanager; then
echo "==> Останавливаю pzmanager (игровой сервер завершится штатно)"
systemctl stop pzmanager
RESTART_AFTER=1
fi
echo "==> Копирую бинарник в $TARGET_BINARY"
install -m 0755 "$BINARY" "$TARGET_BINARY"
# Панель работает от непривилегированного пользователя и не может ни заменить
# бинарник, ни дёрнуть systemctl. Даём ей ровно одну команду через sudo.
if [[ -f "$ADMIN_SRC" ]] && command -v sudo >/dev/null 2>&1; then
echo "==> Ставлю $ADMIN_BINARY и правило sudo"
sed -e "s|@PZ_REPO@|${PZ_REPO}|" \
-e "s|@PZ_BINARY@|${TARGET_BINARY}|" \
"$ADMIN_SRC" > "$ADMIN_BINARY"
chown root:root "$ADMIN_BINARY"
chmod 0755 "$ADMIN_BINARY"
printf '%s ALL=(root) NOPASSWD: %s\n' "$PZ_USER" "$ADMIN_BINARY" \
> /etc/sudoers.d/pzmanager
chmod 0440 /etc/sudoers.d/pzmanager
# Битый файл в sudoers.d ломает sudo целиком, поэтому проверяем и откатываем.
if ! visudo -cf /etc/sudoers.d/pzmanager >/dev/null; then
rm -f /etc/sudoers.d/pzmanager
echo "!! Правило sudo отвергнуто, кнопки обновления в панели не будет" >&2
fi
else
echo "!! $ADMIN_SRC не найден или нет sudo — обновление из панели будет недоступно" >&2
fi
# Кладём сам установщик рядом с данными: удалить или переустановить панель
# можно будет одной локальной командой, без похода в репозиторий.
install -m 0755 "$0" "$PZ_HOME/install.sh"
if [[ -f "$ADMIN_SRC" ]]; then
install -m 0755 "$ADMIN_SRC" "$PZ_HOME/pzmanager-admin.sh"
fi
install -m 0644 "$UNIT_SRC" "$PZ_HOME/pzmanager.service"
echo "==> Ставлю systemd-юнит"
sed -e "s|/opt/pzmanager|${PZ_HOME}|g" \
-e "s|User=pzserver|User=${PZ_USER}|" \
-e "s|Group=pzserver|Group=${PZ_USER}|" \
"$UNIT_SRC" > /etc/systemd/system/pzmanager.service
systemctl daemon-reload
systemctl enable --quiet pzmanager
if [[ "$MODE" == "update" ]]; then
if [[ "$RESTART_AFTER" == "1" ]]; then
systemctl start pzmanager
fi
echo
if [[ "$OLD_VERSION" == "$NEW_VERSION" ]]; then
echo "Переустановлена версия ${NEW_VERSION}."
else
echo "Обновлено: ${OLD_VERSION} -> ${NEW_VERSION}."
fi
echo "Конфиг, миры и учётные записи не тронуты."
LISTEN_NOW="$(grep -E '^listen:' "$CONFIG" | head -1 | awk '{print $2}')"
echo "Панель слушает ${LISTEN_NOW:-?}."
exit 0
fi
systemctl start pzmanager
# Панель кладёт код первичной настройки в файл, как только поднимется. Ждём
# его, чтобы показать код прямо здесь: иначе за ним пришлось бы идти в журнал.
SETUP_TOKEN=""
TOKEN_FILE="$PZ_HOME/data/setup-token"
for _ in $(seq 1 20); do
if [[ -s "$TOKEN_FILE" ]]; then
SETUP_TOKEN="$(head -1 "$TOKEN_FILE")"
break
fi
if ! systemctl is-active --quiet pzmanager; then
break
fi
sleep 1
done
echo
echo "Готово. Панель запущена и слушает ${PZ_LISTEN}."
echo
if [[ "$PZ_LISTEN" == 127.0.0.1:* || "$PZ_LISTEN" == localhost:* ]]; then
echo "Панель доступна только с самой машины. Пробросьте порт:"
echo
echo " ssh -L 8080:${PZ_LISTEN} <пользователь>@<сервер>"
echo
else
PORT="${PZ_LISTEN##*:}"
echo "Откройте в браузере: http://${PZ_LISTEN}"
echo
echo "Если включён firewall, разрешите порт панели:"
echo
echo " sudo ufw allow ${PORT}/tcp"
echo
fi
if [[ -n "$SETUP_TOKEN" ]]; then
echo "Код первичной настройки (введите его в браузере):"
echo
echo " ${SETUP_TOKEN}"
echo
echo "Код одноразовый: после создания администратора он перестаёт работать."
else
echo "Код первичной настройки не удалось прочитать. Посмотрите журнал:"
echo
echo " journalctl -u pzmanager -n 30 --no-pager"
fi
echo
echo "Игровые порты сервера: udp/16261 и udp/16262."
echo
echo "Удалить панель целиком: sudo ${PZ_HOME}/install.sh --uninstall --purge"