Files
pzmanager/deploy/install.sh
T
mrleo1nidandClaude Opus 5 1e0b0a8d2b Кнопка обновления обновляет всё, а не только бинарник
pzmanager-admin update больше не повторяет логику установки: он забирает
из релиза get.sh (сверив sha256) и отдаёт работу ему. Поэтому обновление
из панели теперь приводит в порядок и скрипты установки, и systemd-юнит —
раньше они застывали на версии первой установки, а вместе с ними
терялась, например, локальная копия установщика для удаления.

Работа уходит в разовый юнит через systemd-run. Иначе она не пережила бы
сама себя: панель вызывает скрипт через sudo, тот живёт в cgroup её юнита,
и остановка службы установщиком убила бы его на середине. Команда restart
по той же причине получила --no-block.

Установщик стал понятнее в двух местах, о которые легко споткнуться:
- повторная установка той же сборки больше не роняет игровой сервер —
  если sha256 бинарника совпал, служба не останавливается вовсе;
- после обновления он явно пишет, что запускает службу, и проверяет, что
  она поднялась. Раньше systemctl start отрабатывал молча, и по выводу
  нельзя было понять, работает панель или нет. Службу, остановленную не
  установщиком, он по-прежнему не поднимает самовольно.

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

373 lines
16 KiB
Bash
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
# Установка, обновление и удаление 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)
# Справка — это шапка файла: печатаем комментарии со второй строки и
# останавливаемся, как только они кончились.
awk 'NR > 1 && /^#/ { sub(/^# ?/, ""); print; next } NR > 1 { exit }' "$0"
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
# Если ставим ровно то же самое, что уже лежит, службу трогать незачем: её
# остановка кладёт и игровой сервер, а повторный запуск установщика — обычное
# дело (например, чтобы обновить скрипты).
SAME_BINARY=0
if [[ "$MODE" == "update" ]] &&
[[ "$(sha256sum "$BINARY" | cut -d' ' -f1)" == "$(sha256sum "$TARGET_BINARY" | cut -d' ' -f1)" ]]; then
SAME_BINARY=1
echo "==> Бинарник не изменился — службу не останавливаю"
fi
# Останавливаем сервис перед заменой бинарника: менеджер должен корректно
# завершить игровой сервер, а не быть подменённым на ходу.
RESTART_AFTER=0
if [[ "$MODE" == "update" && "$SAME_BINARY" == "0" ]] && systemctl is-active --quiet pzmanager; then
echo "==> Останавливаю pzmanager (игровой сервер завершится штатно)"
systemctl stop pzmanager
RESTART_AFTER=1
fi
if [[ "$SAME_BINARY" == "0" ]]; then
echo "==> Копирую бинарник в $TARGET_BINARY"
install -m 0755 "$BINARY" "$TARGET_BINARY"
fi
# Панель работает от непривилегированного пользователя и не может ни заменить
# бинарник, ни дёрнуть 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}|" \
-e "s|@PZ_HOME@|${PZ_HOME}|" \
-e "s|@PZ_USER@|${PZ_USER}|" \
"$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
# wait_active ждёт, пока служба поднимется, и говорит, чем всё кончилось:
# молчаливый systemctl start оставлял пользователя гадать, работает ли панель.
wait_active() {
local i
for i in $(seq 1 15); do
if systemctl is-active --quiet pzmanager; then
return 0
fi
sleep 1
done
return 1
}
if [[ "$MODE" == "update" ]]; then
if [[ "$RESTART_AFTER" == "1" ]]; then
echo "==> Запускаю pzmanager"
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}')"
if [[ "$RESTART_AFTER" == "1" ]]; then
if wait_active; then
echo "Панель работает и слушает ${LISTEN_NOW:-?}."
else
echo "!! Служба не поднялась. Посмотрите, что случилось:" >&2
echo " journalctl -u pzmanager -n 40 --no-pager" >&2
exit 1
fi
elif systemctl is-active --quiet pzmanager; then
echo "Панель работает и слушает ${LISTEN_NOW:-?}."
else
# Службу останавливали не мы — значит и поднимать её самовольно не стоит.
echo "Служба сейчас остановлена. Запустить: systemctl start pzmanager"
fi
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
if wait_active; then
echo "Готово. Панель запущена и слушает ${PZ_LISTEN}."
else
echo "!! Служба не поднялась. Посмотрите, что случилось:" >&2
echo " journalctl -u pzmanager -n 40 --no-pager" >&2
exit 1
fi
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"