Files
AstraDeploy/install.sh

709 lines
17 KiB
Bash

#!/usr/bin/env bash
set -Eeuo pipefail
DEPLOY_VERSION="0.1.0"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LIB_DIR="$SCRIPT_DIR/lib"
CORE_DIR="$SCRIPT_DIR/cores"
CORE=""
SERVER_DIR=""
SERVER_PORT=""
MC_VERSION="latest"
BUILD="latest"
RAM="2G"
SERVICE_MODE="tmux-systemd"
FIREWALL="yes"
SERVER_USER="minecraft"
ACCEPT_EULA="no"
ACCEPT_LICENSE="no"
LOCALE="${LANGUAGE:-${LANG:-en}}"
ASSUME_YES="no"
BEGINNER_MODE="no"
DRY_RUN="no"
DEBUG="no"
REPORT_URL=""
REPORT_TOKEN=""
VERSION_REQUEST="no"
SELF_UPDATE="no"
ALLOW_EXISTING="no"
BACKUP_EXISTING="no"
FORCE_CLEAN="no"
UPDATE_MODE="no"
STEP_INDEX=0
STEP_TOTAL=30
CORE_DEFAULT_PORT=""
CORE_PROTOCOL=""
CORE_RUNTIME=""
CORE_NAME=""
CORE_SERVICE_NAME=""
OS_ID=""
OS_VERSION=""
ARCHITECTURE=""
PKG_MANAGER=""
INIT_SYSTEM=""
LOG_DIR="/var/log/astra-deploy"
LOG_FILE=""
if shopt -s nullglob 2>/dev/null; then
for lib in "$LIB_DIR"/*.sh; do
# shellcheck source=/dev/null
. "$lib"
done
shopt -u nullglob
else
for lib in "$LIB_DIR"/*.sh; do
. "$lib"
done
fi
usage() {
cat <<EOF
AstraDeploy v${DEPLOY_VERSION}
Usage:
curl -fsSL https://deploy.example.com/install.sh | sudo bash -s -- [options]
Options:
--core <pmmp|paper|purpur|velocity|powernukkitx|nethergamesmc>
--core-version <latest|version> (legacy alias for mc-version)
--dir <path>
--port <number>
--mc-version <latest|version>
--version <latest|version> (legacy alias for mc-version)
--build <latest|number>
--ram <2G>
--service <systemd|tmux-systemd|screen|none>
--firewall <yes|no>
--user <username>
--accept-eula <yes|no>
--eula <yes|no> (legacy alias for accept-eula)
--accept-license <yes|no>
--license <yes|no> (legacy alias for accept-license)
--locale <en|ru>
--lang <en|ru>
--easy, -E
--dry-run
--yes
--non-interactive
--allow-existing
--backup-existing
--force-clean
--report-url <url>
--report-token <token>
--debug
--version (legacy mc-version) without value to print tool version
--self-update
--script-version
-V
--update
--help
Examples:
# Быстрый запуск для нубов (без вопросов):
sudo bash install.sh --easy --core paper
# Полный non-interactive запуск для автоматизации (бота):
sudo bash install.sh --yes --core paper --dir /opt/minecraft/paper --service tmux-systemd --locale ru
EOF
}
emit_step() {
local step_id="$1"
local details="${2:-}"
STEP_INDEX=$((STEP_INDEX + 1))
local key="step_${step_id}"
local message
message="$(t "$key")"
if [[ "$message" == "$key" ]]; then
message="${step_id}"
fi
if [[ -n "$details" ]]; then
message="${message}: ${details}"
fi
info "$(t progress_step "$STEP_INDEX" "$message")"
send_report "step" "$message" "$step_id" "$STEP_INDEX" "$STEP_TOTAL"
}
init_step_counter() {
if is_java_core "$CORE"; then
STEP_TOTAL=80
else
STEP_TOTAL=60
fi
if [[ "$UPDATE_MODE" == "yes" ]]; then
STEP_TOTAL=$((STEP_TOTAL - 6))
if (( STEP_TOTAL < 10 )); then
STEP_TOTAL=10
fi
fi
}
show_license() {
info "$(t license_title)"
info "$(t license_intro)"
info "$(t license_item_1)"
info "$(t license_item_2)"
info "$(t license_item_3)"
info "$(t license_item_4)"
info "$(t license_item_5)"
info "$(t license_item_6)"
}
parse_bool() {
case "${1,,}" in
yes|y|1|true|on) echo yes ;;
no|n|0|false|off) echo no ;;
*) echo "" ;;
esac
}
parse_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
--core)
CORE="${2:-}"; shift 2 ;;
--core=*)
CORE="${1#*=}"; shift ;;
--core-version)
MC_VERSION="${2:-}"; shift 2 ;;
--core-version=*)
MC_VERSION="${1#*=}"; shift ;;
--dir)
SERVER_DIR="${2:-}"; shift 2 ;;
--dir=*)
SERVER_DIR="${1#*=}"; shift ;;
--port)
SERVER_PORT="${2:-}"; shift 2 ;;
--port=*)
SERVER_PORT="${1#*=}"; shift ;;
--mc-version)
MC_VERSION="${2:-}"; shift 2 ;;
--mc-version=*)
MC_VERSION="${1#*=}"; shift ;;
--version-code)
MC_VERSION="${2:-}"; shift 2 ;;
--version-code=*)
MC_VERSION="${1#*=}"; shift ;;
--version)
if [[ "${2:-}" == "" || "${2:-}" == --* ]]; then
VERSION_REQUEST="yes"
shift
else
MC_VERSION="${2:-}"
shift 2
fi
;;
--build)
BUILD="${2:-}"; shift 2 ;;
--build=*)
BUILD="${1#*=}"; shift ;;
--ram)
RAM="${2:-}"; shift 2 ;;
--ram=*)
RAM="${1#*=}"; shift ;;
--service)
SERVICE_MODE="${2:-}"; shift 2 ;;
--service=*)
SERVICE_MODE="${1#*=}"; shift ;;
--firewall)
FIREWALL="$(parse_bool "${2:-}")"; shift 2 ;;
--firewall=*)
FIREWALL="$(parse_bool "${1#*=}")"; shift ;;
--user)
SERVER_USER="${2:-}"; shift 2 ;;
--user=*)
SERVER_USER="${1#*=}"; shift ;;
--accept-eula)
ACCEPT_EULA="$(parse_bool "${2:-}")"; shift 2 ;;
--accept-eula=*)
ACCEPT_EULA="$(parse_bool "${1#*=}")"; shift ;;
--eula)
ACCEPT_EULA="$(parse_bool "${2:-}")"; shift 2 ;;
--eula=*)
ACCEPT_EULA="$(parse_bool "${1#*=}")"; shift ;;
--accept-license)
ACCEPT_LICENSE="$(parse_bool "${2:-}")"; shift 2 ;;
--accept-license=*)
ACCEPT_LICENSE="$(parse_bool "${1#*=}")"; shift ;;
--license)
ACCEPT_LICENSE="$(parse_bool "${2:-}")"; shift 2 ;;
--license=*)
ACCEPT_LICENSE="$(parse_bool "${1#*=}")"; shift ;;
--locale)
LOCALE="${2:-}"; shift 2 ;;
--locale=*)
LOCALE="${1#*=}"; shift ;;
--lang)
LOCALE="${2:-}"; shift 2 ;;
--lang=*)
LOCALE="${1#*=}"; shift ;;
--easy|--beginner|-E)
BEGINNER_MODE="yes"
ASSUME_YES="yes"
shift
;;
--dry-run)
DRY_RUN="yes"; shift ;;
--yes|-y)
ASSUME_YES="yes"; shift ;;
--non-interactive)
ASSUME_YES="yes"; shift ;;
--allow-existing)
ALLOW_EXISTING="yes"; shift ;;
--backup-existing)
BACKUP_EXISTING="yes"; shift ;;
--force-clean)
FORCE_CLEAN="yes"; shift ;;
--report-url)
REPORT_URL="${2:-}"; shift 2 ;;
--report-url=*)
REPORT_URL="${1#*=}"; shift ;;
--report-token)
REPORT_TOKEN="${2:-}"; shift 2 ;;
--report-token=*)
REPORT_TOKEN="${1#*=}"; shift ;;
--debug)
DEBUG="yes"; shift ;;
--script-version|-V)
VERSION_REQUEST="yes"; shift ;;
--self-update)
SELF_UPDATE="yes"; shift ;;
--update)
UPDATE_MODE="yes"; shift ;;
--help|-h)
usage
exit 0
;;
*)
error "$(t unknown_argument "$1")"
usage
exit 1
;;
esac
done
if [[ "$FIREWALL" == "" ]]; then
FIREWALL="yes"
fi
}
init_logger() {
local timestamp
timestamp="$(date +%F-%H-%M-%S)"
mkdir -p "$LOG_DIR"
LOG_FILE="$LOG_DIR/install-${timestamp}.log"
exec > >(tee -a "$LOG_FILE") 2>&1
info "$(t log_title "$DEPLOY_VERSION")"
info "$(t log_file "$LOG_FILE")"
}
resolve_core_defaults() {
if [[ -z "$CORE" ]]; then
return 0
fi
local check_fn="core_default_port_${CORE}"
if [[ "$(type -t "$check_fn" 2>/dev/null)" != "function" ]]; then
fail "$(t unsupported_core "$CORE" "pmmp,paper,purpur,velocity,powernukkitx,nethergamesmc" "${SCRIPT_DIR}/manifests/cores.json")"
fi
CORE_NAME="$(core_name_${CORE})"
CORE_DEFAULT_PORT="$(core_default_port_${CORE})"
CORE_PROTOCOL="$(core_protocol_${CORE})"
CORE_RUNTIME="$(core_runtime_${CORE})"
CORE_SERVICE_NAME="${CORE}_minecraft"
if [[ -z "$SERVER_PORT" ]]; then
SERVER_PORT="$CORE_DEFAULT_PORT"
fi
}
ask_value() {
local prompt_text="$1"
local default_value="$2"
local var_name="$3"
local value=""
if [[ -t 0 ]]; then
read -r -p "$prompt_text [$default_value]: " value < /dev/tty || true
else
read -r -p "$prompt_text [$default_value]: " value || true
fi
if [[ -z "$value" ]]; then
value="$default_value"
fi
printf -v "$var_name" "%s" "$value"
}
ask_yes_no() {
local prompt_text="$1"
local default_value="$2"
local var_name="$3"
local value=""
while true; do
if [[ -t 0 ]]; then
read -r -p "$prompt_text (yes/no) [$default_value]: " value < /dev/tty || true
else
read -r -p "$prompt_text (yes/no) [$default_value]: " value || true
fi
if [[ -z "$value" ]]; then
value="$default_value"
fi
value="$(parse_bool "$value" || true)"
if [[ "$value" == "yes" || "$value" == "no" ]]; then
printf -v "$var_name" "%s" "$value"
return
fi
warn "$(t prompt_invalid_yes_no)"
done
}
apply_noob_defaults() {
if [[ -z "$CORE" ]]; then
CORE="paper"
fi
if [[ -z "$RAM" ]]; then
RAM="2G"
fi
if [[ -z "$SERVICE_MODE" ]]; then
SERVICE_MODE="tmux-systemd"
fi
if [[ -z "$FIREWALL" ]]; then
FIREWALL="yes"
fi
if [[ -z "$SERVER_USER" ]]; then
SERVER_USER="minecraft"
fi
if [[ -z "$MC_VERSION" ]]; then
MC_VERSION="latest"
fi
if [[ -z "$BUILD" ]]; then
BUILD="latest"
fi
resolve_core_defaults
if [[ -z "$SERVER_DIR" ]]; then
SERVER_DIR="/opt/minecraft/$CORE"
fi
if [[ -z "$SERVER_PORT" ]]; then
SERVER_PORT="$CORE_DEFAULT_PORT"
fi
if [[ "$BEGINNER_MODE" == "yes" ]]; then
FIREWALL="$(parse_bool "$FIREWALL" || true)"
[[ "$FIREWALL" == "" ]] && FIREWALL="yes"
ACCEPT_LICENSE="yes"
if is_java_core "$CORE"; then
ACCEPT_EULA="yes"
fi
fi
}
show_noob_plan() {
info "$(t easy_plan_title)"
info "$(t easy_plan_core "$CORE")"
info "$(t easy_plan_dir "$SERVER_DIR")"
info "$(t easy_plan_port "$SERVER_PORT")"
info "$(t easy_plan_ram "$RAM")"
info "$(t easy_plan_service "$SERVICE_MODE")"
info "$(t easy_plan_user "$SERVER_USER")"
info "$(t easy_plan_firewall "$FIREWALL")"
if [[ "$CORE" == "nethergamesmc" ]]; then
info "$(t easy_plan_build "$BUILD")"
info "$(t easy_plan_mc_version "$MC_VERSION")"
fi
if is_java_core "$CORE"; then
info "$(t easy_plan_eula "$ACCEPT_EULA")"
fi
}
interact() {
if [[ "$ASSUME_YES" == "yes" ]]; then
return 0
fi
if [[ -z "$CORE" ]]; then
ask_value "$(t prompt_core)" "paper" CORE
fi
resolve_core_defaults
if [[ -z "$SERVER_DIR" ]]; then
ask_value "$(t prompt_server_directory)" "/opt/minecraft/$CORE" SERVER_DIR
fi
if [[ -z "$SERVER_PORT" ]]; then
ask_value "$(t prompt_server_port)" "$CORE_DEFAULT_PORT" SERVER_PORT
fi
ask_value "$(t prompt_ram)" "$RAM" RAM
if [[ "$CORE" == "nethergamesmc" ]]; then
ask_value "$(t prompt_php_build)" "${BUILD:-latest}" BUILD
ask_value "$(t prompt_pocketmine_version)" "${MC_VERSION:-latest}" MC_VERSION
fi
ask_value "$(t prompt_service_mode)" "$SERVICE_MODE" SERVICE_MODE
ask_value "$(t prompt_open_firewall)" "$FIREWALL" FIREWALL
FIREWALL="$(parse_bool "$FIREWALL" || true)"
[[ "$FIREWALL" == "" ]] && FIREWALL="yes"
ask_value "$(t prompt_install_user)" "$SERVER_USER" SERVER_USER
if is_java_core "$CORE"; then
ask_yes_no "$(t eula_prompt)" "${ACCEPT_EULA}" ACCEPT_EULA
fi
}
configure_plan() {
if [[ -z "$CORE" ]]; then
fail "$(t core_required)"
fi
resolve_core_defaults
local validate_fn="core_validate_${CORE}"
if [[ "$(type -t "$validate_fn" 2>/dev/null)" != "function" ]]; then
fail "$(t core_validator_missing "$CORE")"
fi
"$validate_fn"
if [[ -z "$SERVER_DIR" ]]; then
fail "$(t server_dir_required)"
fi
if [[ -z "$SERVER_PORT" ]]; then
fail "$(t server_port_required)"
fi
if [[ ! "$SERVER_PORT" =~ ^[0-9]+$ ]]; then
fail "$(t port_must_be_numeric)"
fi
if (( SERVER_PORT < 1 || SERVER_PORT > 65535 )); then
fail "$(t port_out_of_range "$SERVER_PORT")"
fi
if [[ "$SERVICE_MODE" != "systemd" && "$SERVICE_MODE" != "tmux-systemd" && "$SERVICE_MODE" != "screen" && "$SERVICE_MODE" != "none" ]]; then
fail "$(t invalid_service_mode "$SERVICE_MODE")"
fi
if [[ "$FIREWALL" != "yes" && "$FIREWALL" != "no" ]]; then
fail "$(t invalid_firewall_value "$FIREWALL")"
fi
}
validate_license() {
if [[ "${ACCEPT_LICENSE,,}" == "yes" ]]; then
return 0
fi
if [[ "$ASSUME_YES" == "yes" ]]; then
fail "$(t license_denied)"
fi
show_license
ask_yes_no "$(t license_prompt)" "no" ACCEPT_LICENSE
if [[ "${ACCEPT_LICENSE,,}" != "yes" ]]; then
fail "$(t license_denied)"
fi
}
check_existing_directory() {
if [[ ! -d "$SERVER_DIR" ]]; then
run_cmd mkdir -p "$SERVER_DIR"
return
fi
if [[ -z "$(ls -A "$SERVER_DIR" 2>/dev/null)" ]]; then
return
fi
if [[ "$FORCE_CLEAN" == "yes" ]]; then
run_cmd find "$SERVER_DIR" -mindepth 1 -maxdepth 1 -exec rm -rf -- {} +
return
fi
if [[ "$BACKUP_EXISTING" == "yes" ]]; then
local backup_dir="${SERVER_DIR}.backup-$(date +%F-%H-%M-%S)"
run_cmd cp -a "$SERVER_DIR" "$backup_dir"
info "$(t dir_backed_up "$backup_dir")"
return
fi
if [[ "$ALLOW_EXISTING" == "yes" ]]; then
return
fi
if [[ "$ASSUME_YES" == "yes" ]]; then
fail "$(t dir_not_empty_auto "$SERVER_DIR")"
fi
warn "$(t dir_not_empty "$SERVER_DIR")"
while true; do
local action
if [[ -t 0 ]]; then
read -r -p "$(t dir_not_empty_actions)" action < /dev/tty || true
else
read -r -p "$(t dir_not_empty_actions)" action || true
fi
case "$action" in
1) fail "$(t action_cancelled_by_user)";;
2) return;;
3)
local backup_dir="${SERVER_DIR}.backup-$(date +%F-%H-%M-%S)"
run_cmd cp -a "$SERVER_DIR" "$backup_dir"
info "$(t dir_backed_up "$backup_dir")"
return
;;
4)
run_cmd find "$SERVER_DIR" -mindepth 1 -maxdepth 1 -exec rm -rf -- {} +
return
;;
*)
warn "$(t dir_invalid_option)"
;;
esac
done
}
backup_file() {
local path="$1"
if [[ ! -e "$path" ]]; then
return 0
fi
local backup_path="${path}.backup-$(date +%F-%H-%M-%S)"
run_cmd cp -a "$path" "$backup_path"
info "$(t file_backed_up "$path" "$backup_path")"
}
preflight() {
emit_step "preflight_begin"
emit_step "preflight_validate_root"
validate_root_usage
emit_step "preflight_detect"
emit_step "preflight_detect_os"
detect_system
emit_step "preflight_validate_support"
validate_core_support
if [[ "$UPDATE_MODE" == "yes" ]]; then
emit_step "preflight_update_mode_check"
if [[ ! -d "$SERVER_DIR" ]]; then
fail "$(t update_requires_existing_dir)"
fi
if [[ -z "$(ls -A "$SERVER_DIR" 2>/dev/null)" ]]; then
fail "$(t update_requires_non_empty_dir)"
fi
fi
emit_step "preflight_validate_plan"
configure_plan
emit_step "preflight_validate_licenses"
validate_license
validate_eula
emit_step "preflight_validate_port"
if is_java_core "$CORE"; then
validate_port "$SERVER_PORT" "tcp"
else
validate_port "$SERVER_PORT" "udp"
fi
emit_step "preflight_directory"
if [[ "$UPDATE_MODE" == "no" ]]; then
check_existing_directory
fi
emit_step "preflight_packages"
install_base_packages
if is_java_core "$CORE"; then
emit_step "preflight_java"
install_java_21
fi
emit_step "preflight_user"
create_minecraft_user "$SERVER_USER"
prepare_server_dir "$SERVER_DIR" "$SERVER_USER"
}
install_core() {
emit_step "install_core" "$CORE"
local install_fn="core_install_${CORE}"
local post_fn="core_post_install_${CORE}"
local update_fn="core_update_${CORE}"
if [[ "$UPDATE_MODE" == "yes" && "$(type -t "$update_fn" 2>/dev/null)" == "function" ]]; then
stop_service_if_running "${CORE_SERVICE_NAME}"
emit_step "update_core" "$CORE"
"$update_fn"
else
emit_step "deploy_core" "$CORE"
"$install_fn"
fi
if [[ ! "$(type -t "$post_fn" 2>/dev/null)" == "function" ]]; then
fail "$(t post_install_callback_missing "$CORE")"
fi
"$post_fn"
}
finalize() {
info "$(t install_completed "$CORE_NAME")"
info "$(t result_server_dir "$SERVER_DIR")"
info "$(t result_service_mode "${SERVICE_MODE_ACTIVE:-none}")"
info "$(t result_service_name "${SERVICE_NAME:-${CORE}_minecraft}")"
info "$(t result_log_file "$LOG_FILE")"
send_report "success" "$(t install_finished)"
}
main() {
for core_file in "$CORE_DIR"/*.sh; do
. "$core_file"
done
load_locale
parse_args "$@"
if [[ "$VERSION_REQUEST" == "yes" ]]; then
echo "AstraDeploy v$DEPLOY_VERSION"
exit 0
fi
load_locale
validate_root_usage
init_logger
trap 'rollback_on_error "$?" "$BASH_COMMAND"' ERR
send_report "started" "Deployment started"
if [[ "$SELF_UPDATE" == "yes" ]]; then
warn "$(t self_update_not_implemented)"
fi
emit_step "parse_complete"
interact
emit_step "prepare_defaults"
apply_noob_defaults
if [[ "$BEGINNER_MODE" == "yes" ]]; then
show_noob_plan
fi
init_step_counter
emit_step "preflight_start"
preflight
emit_step "install_start"
install_core
emit_step "finalize"
finalize
}
main "$@"