В вывод --help попадала первая строка кода: диапазон строк был задан числом, а не концом шапки. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
324 lines
14 KiB
Bash
324 lines
14 KiB
Bash
#!/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
|
||
|
||
# Останавливаем сервис перед заменой бинарника: менеджер должен корректно
|
||
# завершить игровой сервер, а не быть подменённым на ходу.
|
||
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"
|