#!/usr/bin/env bash # Действия, которые панель не может выполнить сама: она работает от # непривилегированного пользователя, а бинарник и systemd принадлежат root. # # Скрипт ставится установщиком в /usr/local/bin/pzmanager-admin (root:root) и # разрешён сервисному пользователю в /etc/sudoers.d/pzmanager — ровно эта # команда и ничего больше. Аргументы проверяются здесь: sudo пропускает любые. # # Значения ниже подставляет install.sh при установке. set -euo pipefail PZ_REPO="@PZ_REPO@" PZ_BINARY="@PZ_BINARY@" PZ_HOME="@PZ_HOME@" PZ_USER="@PZ_USER@" PZ_SERVICE="pzmanager" # Имя разового юнита, в котором идёт обновление. UPDATE_UNIT="pzmanager-update" die() { echo "Ошибка: $*" >&2; exit 1; } api_base() { local url="${PZ_REPO%/}" url="${url%.git}" local host="${url%%://*}://$(printf '%s' "${url#*://}" | cut -d/ -f1)" printf '%s/api/v1/repos/%s' "$host" "${url#"$host"/}" } # asset_url ищет в описании релиза ссылку на файл с точным именем. asset_url() { local release="$1" name="$2" printf '%s' "$release" \ | grep -o '"browser_download_url" *: *"[^"]*"' \ | cut -d'"' -f4 \ | while read -r url; do if [[ "${url##*/}" == "$name" ]]; then printf '%s' "$url" return fi done } # cmd_update только запускает работу и сразу возвращает управление панели. # # Обновление обязано пережить перезапуск службы: установщик остановит # pzmanager, а вместе с ним systemd убьёт и этот скрипт — панель вызывает его # через sudo, так что он живёт в cgroup её юнита. Поэтому саму работу уносим # в отдельный разовый юнит, который к pzmanager отношения не имеет. cmd_update() { local version="${1:-latest}" # Версия приходит из панели: проверяем до запуска юнита, чтобы не плодить # заведомо провальные. [[ "$version" == "latest" || "$version" =~ ^[A-Za-z0-9._-]+$ ]] \ || die "недопустимая версия: $version" command -v systemd-run >/dev/null 2>&1 \ || die "нет systemd-run — обновите панель командой из README" # --collect убирает юнит после завершения, в том числе неуспешного: иначе # следующий запуск упрётся в оставшийся failed-юнит с тем же именем. systemd-run --quiet --collect --unit="$UPDATE_UNIT" \ --description="Обновление pzmanager" \ "$0" do-update "$version" \ || die "не удалось запустить обновление разовым юнитом" echo "Обновление запущено в фоне." echo "Ход работы: journalctl -u ${UPDATE_UNIT} -f" } # cmd_do_update выполняется уже внутри разового юнита. Обновляет всё сразу: # бинарник, скрипты установки и systemd-юнит, — поэтому просто отдаёт работу # get.sh из релиза вместо того, чтобы повторять его логику. cmd_do_update() { local version="${1:-latest}" api release tag url tmp api="$(api_base)" if [[ "$version" == "latest" ]]; then release="$(curl -fsSL "${api}/releases/latest")" || die "не удалось запросить релиз" else # Версия приходит из панели; всё, кроме безопасных символов, отвергаем. [[ "$version" =~ ^[A-Za-z0-9._-]+$ ]] || die "недопустимая версия: $version" release="$(curl -fsSL "${api}/releases/tags/${version}")" || die "релиз ${version} не найден" fi tag="$(printf '%s' "$release" | grep -o '"tag_name" *: *"[^"]*"' | head -1 | cut -d'"' -f4)" [[ -n "$tag" ]] || die "в ответе нет тега" url="$(asset_url "$release" get.sh)" [[ -n "$url" ]] || die "в релизе ${tag} нет get.sh" local sums_url sums_url="$(asset_url "$release" checksums.txt)" [[ -n "$sums_url" ]] || die "в релизе ${tag} нет checksums.txt" tmp="$(mktemp -d)" trap 'rm -rf "$tmp"' RETURN echo "==> Беру установщик из ${tag}" curl -fsSL -o "${tmp}/get.sh" "$url" || die "не удалось скачать get.sh" curl -fsSL -o "${tmp}/checksums.txt" "$sums_url" || die "не удалось скачать checksums.txt" echo "==> Проверяю контрольную сумму" ( cd "$tmp" && awk '$NF == "get.sh"' checksums.txt > expected.txt \ && [[ -s expected.txt ]] && sha256sum -c expected.txt ) \ || die "контрольная сумма get.sh не сошлась" echo "==> Обновляю до ${tag}" # get.sh скачает бинарник нужной архитектуры и запустит install.sh, который # сам остановит службу, заменит файлы и поднимет её обратно. PZ_HOME="$PZ_HOME" PZ_USER="$PZ_USER" PZ_REPO="$PZ_REPO" PZ_VERSION="$tag" \ bash "${tmp}/get.sh" \ || die "обновление не удалось" echo "Обновление до ${tag} завершено" } case "${1:-}" in update) cmd_update "${2:-latest}" ;; do-update) cmd_do_update "${2:-latest}" ;; restart) echo "==> Перезапускаю ${PZ_SERVICE}" # --no-block: команда ставит задание и выходит, не дожидаясь остановки. # Иначе systemd убил бы нас вместе со службой на середине ожидания. systemctl restart --no-block "$PZ_SERVICE" ;; version) "$PZ_BINARY" version ;; *) echo "Использование: pzmanager-admin update [версия] | restart | version" >&2 exit 2 ;; esac