diff --git a/misc/alpine-install.func b/misc/alpine-install.func index 909b51043..b7381947a 100644 --- a/misc/alpine-install.func +++ b/misc/alpine-install.func @@ -83,11 +83,6 @@ update_os() { msg_info "Updating Container OS" $STD apk -U upgrade msg_ok "Updated Container OS" - - msg_info "Installing core dependencies" - $STD apk update - $STD apk add newt curl openssh nano mc ncurses gpg - msg_ok "Core dependencies installed" } # This function modifies the message of the day (motd) and SSH settings diff --git a/misc/build.func b/misc/build.func index 863cf50e0..d6ecf70ba 100644 --- a/misc/build.func +++ b/misc/build.func @@ -304,13 +304,12 @@ echo_default() { fi # Output the selected values with icons - echo -e "${OS}${BOLD}${DGN}Operating System: ${BGN}$var_os${CL}" - echo -e "${OSVERSION}${BOLD}${DGN}Version: ${BGN}$var_version${CL}" + echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}${CT_ID}${CL}" + echo -e "${OS}${BOLD}${DGN}Operating System: ${BGN}$var_os ($var_version)${CL}" echo -e "${CONTAINERTYPE}${BOLD}${DGN}Container Type: ${BGN}$CT_TYPE_DESC${CL}" echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE} GB${CL}" echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}${CORE_COUNT}${CL}" echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE} MiB${CL}" - echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}${CT_ID}${CL}" if [ "$VERB" == "yes" ]; then echo -e "${SEARCH}${BOLD}${DGN}Verbose Mode: ${BGN}Enabled${CL}" fi @@ -1095,7 +1094,9 @@ build_container() { # This executes create_lxc.sh and creates the container and .conf file bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/create_lxc.sh)" $? - LXC_CONFIG=/etc/pve/lxc/${CTID}.conf + LXC_CONFIG="/etc/pve/lxc/${CTID}.conf" + + # USB passthrough for privileged LXC (CT_TYPE=0) if [ "$CT_TYPE" == "0" ]; then cat <>"$LXC_CONFIG" # USB passthrough @@ -1111,38 +1112,98 @@ lxc.mount.entry: /dev/ttyACM1 dev/ttyACM1 none bind,optional,create= EOF fi - if [ "$CT_TYPE" == "0" ]; then - if [[ "$APP" == "Channels" || "$APP" == "Emby" || "$APP" == "ErsatzTV" || "$APP" == "Frigate" || "$APP" == "Jellyfin" || "$APP" == "Plex" || "$APP" == "immich" || "$APP" == "Tdarr" || "$APP" == "Open WebUI" || "$APP" == "Unmanic" || "$APP" == "Ollama" || "$APP" == "FileFlows" ]]; then - cat <>"$LXC_CONFIG" -# VAAPI hardware transcoding -lxc.cgroup2.devices.allow: c 226:0 rwm -lxc.cgroup2.devices.allow: c 226:128 rwm -lxc.cgroup2.devices.allow: c 29:0 rwm -lxc.mount.entry: /dev/fb0 dev/fb0 none bind,optional,create=file -lxc.mount.entry: /dev/dri dev/dri none bind,optional,create=dir -lxc.mount.entry: /dev/dri/renderD128 dev/dri/renderD128 none bind,optional,create=file -EOF + # VAAPI passthrough for privileged containers or known apps + VAAPI_APPS=( + "immich" + "Channels" + "Emby" + "ErsatzTV" + "Frigate" + "Jellyfin" + "Plex" + "Scrypted" + "Tdarr" + "Unmanic" + "Ollama" + "FileFlows" + "Open WebUI" + ) + + is_vaapi_app=false + for vaapi_app in "${VAAPI_APPS[@]}"; do + if [[ "$APP" == "$vaapi_app" ]]; then + is_vaapi_app=true + break fi - else - if [[ "$APP" == "Channels" || "$APP" == "Emby" || "$APP" == "ErsatzTV" || "$APP" == "Frigate" || "$APP" == "Jellyfin" || "$APP" == "Plex" || "$APP" == "immich" || "$APP" == "Tdarr" || "$APP" == "Open WebUI" || "$APP" == "Unmanic" || "$APP" == "Ollama" || "$APP" == "FileFlows" ]]; then - if [[ -e "/dev/dri/renderD128" ]]; then - if [[ -e "/dev/dri/card0" ]]; then - cat <>"$LXC_CONFIG" -# VAAPI hardware transcoding -dev0: /dev/dri/card0,gid=44 -dev1: /dev/dri/renderD128,gid=104 -EOF - else - cat <>"$LXC_CONFIG" -# VAAPI hardware transcoding -dev0: /dev/dri/card1,gid=44 -dev1: /dev/dri/renderD128,gid=104 -EOF + done + + if ([ "$CT_TYPE" == "0" ] || [ "$is_vaapi_app" == "true" ]) && + ([[ -e /dev/dri/renderD128 ]] || [[ -e /dev/dri/card0 ]] || [[ -e /dev/fb0 ]]); then + + echo "" + msg_custom "⚙️ " "\e[96m" "Configuring VAAPI passthrough for LXC container" + + if [ "$CT_TYPE" != "0" ]; then + msg_custom "⚠️ " "\e[33m" "Container is unprivileged – VAAPI passthrough may not work without additional host configuration (e.g., idmap)." + fi + + msg_custom "ℹ️ " "\e[96m" "VAAPI enables GPU hardware acceleration (e.g., for video transcoding in Jellyfin or Plex)." + + echo "" + read -rp "➤ Automatically mount all available VAAPI devices? [Y/n]: " VAAPI_ALL + + if [[ "$VAAPI_ALL" =~ ^[Yy]$|^$ ]]; then + # Mount all devices automatically + if [[ -e /dev/dri/renderD128 ]]; then + echo "lxc.cgroup2.devices.allow: c 226:128 rwm" >>"$LXC_CONFIG" + echo "lxc.mount.entry: /dev/dri/renderD128 dev/dri/renderD128 none bind,optional,create=file" >>"$LXC_CONFIG" + fi + if [[ -e /dev/dri/card0 ]]; then + echo "lxc.cgroup2.devices.allow: c 226:0 rwm" >>"$LXC_CONFIG" + + echo "lxc.mount.entry: /dev/dri/card0 dev/dri/card0 none bind,optional,create=file" >>"$LXC_CONFIG" + fi + if [[ -e /dev/fb0 ]]; then + echo "lxc.cgroup2.devices.allow: c 29:0 rwm" >>"$LXC_CONFIG" + echo "lxc.mount.entry: /dev/fb0 dev/fb0 none bind,optional,create=file" >>"$LXC_CONFIG" + fi + if [[ -d /dev/dri ]]; then + echo "lxc.mount.entry: /dev/dri dev/dri none bind,optional,create=dir" >>"$LXC_CONFIG" + fi + else + # Manual selection per device + if [[ -e /dev/dri/renderD128 ]]; then + read -rp "➤ Mount /dev/dri/renderD128 (GPU rendering)? [y/N]: " MOUNT_D128 + if [[ "$MOUNT_D128" =~ ^[Yy]$ ]]; then + echo "lxc.cgroup2.devices.allow: c 226:128 rwm" >>"$LXC_CONFIG" + echo "lxc.mount.entry: /dev/dri/renderD128 dev/dri/renderD128 none bind,optional,create=file" >>"$LXC_CONFIG" fi fi + + if [[ -e /dev/dri/card0 ]]; then + read -rp "➤ Mount /dev/dri/card0 (GPU hardware interface)? [y/N]: " MOUNT_CARD0 + if [[ "$MOUNT_CARD0" =~ ^[Yy]$ ]]; then + echo "lxc.cgroup2.devices.allow: c 226:0 rwm" >>"$LXC_CONFIG" + echo "lxc.mount.entry: /dev/dri/card0 dev/dri/card0 none bind,optional,create=file" >>"$LXC_CONFIG" + + fi + fi + + if [[ -e /dev/fb0 ]]; then + read -rp "➤ Mount /dev/fb0 (Framebuffer, GUI)? [y/N]: " MOUNT_FB0 + if [[ "$MOUNT_FB0" =~ ^[Yy]$ ]]; then + echo "lxc.cgroup2.devices.allow: c 29:0 rwm" >>"$LXC_CONFIG" + echo "lxc.mount.entry: /dev/fb0 dev/fb0 none bind,optional,create=file" >>"$LXC_CONFIG" + fi + fi + + if [[ -d /dev/dri ]]; then + echo "lxc.mount.entry: /dev/dri dev/dri none bind,optional,create=dir" >>"$LXC_CONFIG" + fi fi fi + # TUN device passthrough if [ "$ENABLE_TUN" == "yes" ]; then cat <>"$LXC_CONFIG" lxc.cgroup2.devices.allow: c 10:200 rwm @@ -1172,10 +1233,13 @@ EOF' locale-gen >/dev/null && \ export LANG=\$locale_line" + if [[ -z "${tz:-}" ]]; then + tz=$(timedatectl show --property=Timezone --value 2>/dev/null || echo "Etc/UTC") + fi if pct exec "$CTID" -- test -e "/usr/share/zoneinfo/$tz"; then - pct exec "$CTID" -- bash -c "echo $tz >/etc/timezone && ln -sf /usr/share/zoneinfo/$tz /etc/localtime" + pct exec "$CTID" -- bash -c "tz='$tz'; echo \"\$tz\" >/etc/timezone && ln -sf \"/usr/share/zoneinfo/\$tz\" /etc/localtime" else - msg_info "Skipping timezone setup – zone '$tz' not found in container" + msg_warn "Skipping timezone setup – zone '$tz' not found in container" fi pct exec "$CTID" -- bash -c "apt-get update >/dev/null && apt-get install -y sudo curl mc gnupg2 >/dev/null" @@ -1255,7 +1319,9 @@ api_exit_script() { fi } -trap 'api_exit_script' EXIT +if command -v pveversion >/dev/null 2>&1; then + trap 'api_exit_script' EXIT +fi trap 'post_update_to_api "failed" "$BASH_COMMAND"' ERR trap 'post_update_to_api "failed" "INTERRUPTED"' SIGINT trap 'post_update_to_api "failed" "TERMINATED"' SIGTERM diff --git a/misc/core.func b/misc/core.func index 1a904f9ab..9b0c128af 100644 --- a/misc/core.func +++ b/misc/core.func @@ -1,30 +1,6 @@ # Copyright (c) 2021-2025 community-scripts ORG # License: MIT | https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/LICENSE -# if ! declare -f wait_for >/dev/null; then -# echo "[DEBUG] Undefined function 'wait_for' used from: ${BASH_SOURCE[*]}" >&2 -# wait_for() { -# echo "[DEBUG] Fallback: wait_for called with: $*" >&2 -# true -# } -# fi - -trap 'on_error $? $LINENO' ERR -trap 'on_exit' EXIT -trap 'on_interrupt' INT -trap 'on_terminate' TERM - -if ! declare -f wait_for >/dev/null; then - wait_for() { - true - } -fi - -declare -A MSG_INFO_SHOWN=() -SPINNER_PID="" -SPINNER_ACTIVE=0 -SPINNER_MSG="" - # ------------------------------------------------------------------------------ # Loads core utility groups once (colors, formatting, icons, defaults). # ------------------------------------------------------------------------------ @@ -43,100 +19,51 @@ load_functions() { # add more } -on_error() { - local exit_code="$1" - local lineno="$2" +# ============================================================================ +# Error & Signal Handling – robust, universal, subshell-safe +# ============================================================================ - stop_spinner - - case "$exit_code" in - 1) msg_error "Generic error occurred (line $lineno)" ;; - 2) msg_error "Shell misuse (line $lineno)" ;; - 126) msg_error "Command cannot execute (line $lineno)" ;; - 127) msg_error "Command not found (line $lineno)" ;; - 128) msg_error "Invalid exit argument (line $lineno)" ;; - 130) msg_error "Script aborted by user (CTRL+C)" ;; - 143) msg_error "Script terminated by SIGTERM" ;; - *) msg_error "Script failed at line $lineno with exit code $exit_code" ;; - esac - - exit "$exit_code" -} - -on_exit() { - cleanup_spinner || true - [[ "${VERBOSE:-no}" == "yes" ]] && msg_info "Script exited" -} - -on_interrupt() { - msg_error "Interrupted by user (CTRL+C)" - exit 130 -} - -on_terminate() { - msg_error "Terminated by signal (TERM)" - exit 143 -} - -setup_trap_abort_handling() { - trap '__handle_signal_abort SIGINT' SIGINT - trap '__handle_signal_abort SIGTERM' SIGTERM - trap '__handle_unexpected_error $?' ERR -} - -__handle_signal_abort() { - local signal="$1" - echo - [ -n "${SPINNER_PID:-}" ] && kill "$SPINNER_PID" 2>/dev/null && wait "$SPINNER_PID" 2>/dev/null - - case "$signal" in - SIGINT) - msg_error "Script aborted by user (CTRL+C)" - exit 130 +_tool_error_hint() { + local cmd="$1" + local code="$2" + case "$cmd" in + curl) + case "$code" in + 6) echo "Curl: Could not resolve host (DNS problem)" ;; + 7) echo "Curl: Failed to connect to host (connection refused)" ;; + 22) echo "Curl: HTTP error (404/403 etc)" ;; + 28) echo "Curl: Operation timeout" ;; + *) echo "Curl: Unknown error ($code)" ;; + esac ;; - SIGTERM) - msg_error "Script terminated (SIGTERM)" - exit 143 + wget) + echo "Wget failed – URL unreachable or permission denied" ;; - *) - msg_error "Script interrupted (unknown signal: $signal)" - exit 1 + systemctl) + echo "Systemd unit failure – check service name and permissions" ;; + jq) + echo "jq parse error – malformed JSON or missing key" + ;; + mariadb | mysql) + echo "MySQL/MariaDB command failed – check credentials or DB" + ;; + unzip) + echo "unzip failed – corrupt file or missing permission" + ;; + tar) + echo "tar failed – invalid format or missing binary" + ;; + node | npm | pnpm | yarn) + echo "Node tool failed – check version compatibility or package.json" + ;; + *) echo "" ;; esac } -__handle_unexpected_error() { - local exit_code="$1" - echo - [ -n "${SPINNER_PID:-}" ] && kill "$SPINNER_PID" 2>/dev/null && wait "$SPINNER_PID" 2>/dev/null - - case "$exit_code" in - 1) - msg_error "Generic error occurred (exit code 1)" - ;; - 2) - msg_error "Misuse of shell builtins (exit code 2)" - ;; - 126) - msg_error "Command invoked cannot execute (exit code 126)" - ;; - 127) - msg_error "Command not found (exit code 127)" - ;; - 128) - msg_error "Invalid exit argument (exit code 128)" - ;; - 130) - msg_error "Script aborted by user (CTRL+C)" - ;; - 143) - msg_error "Script terminated by SIGTERM" - ;; - *) - msg_error "Unexpected error occurred (exit code $exit_code)" - ;; - esac - exit "$exit_code" +catch_errors() { + set -Eeuo pipefail + trap 'error_handler $LINENO "$BASH_COMMAND"' ERR } # ------------------------------------------------------------------------------ @@ -153,6 +80,13 @@ color() { CL=$(echo "\033[m") } +# Special for spinner and colorized output via printf +color_spinner() { + CS_YW=$'\033[33m' + CS_YWB=$'\033[93m' + CS_CL=$'\033[m' +} + # ------------------------------------------------------------------------------ # Defines formatting helpers like tab, bold, and line reset sequences. # ------------------------------------------------------------------------------ @@ -196,6 +130,7 @@ icons() { ADVANCED="${TAB}🧩${TAB}${CL}" FUSE="${TAB}🗂️${TAB}${CL}" HOURGLASS="${TAB}⏳${TAB}" + } # ------------------------------------------------------------------------------ @@ -227,7 +162,7 @@ silent() { # Function to download & save header files get_header() { local app_name=$(echo "${APP,,}" | tr -d ' ') - local app_type=${APP_TYPE:-ct} # Default 'ct' + local app_type=${APP_TYPE:-ct} local header_url="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/${app_type}/headers/${app_name}" local local_header_path="/usr/local/community-scripts/headers/${app_type}/${app_name}" @@ -257,77 +192,39 @@ header_info() { fi } -# ------------------------------------------------------------------------------ -# Performs a curl request with retry logic and inline feedback. -# ------------------------------------------------------------------------------ - -run_curl() { - if [ "$VERBOSE" = "no" ]; then - $STD curl "$@" - else - curl "$@" +ensure_tput() { + if ! command -v tput >/dev/null 2>&1; then + if grep -qi 'alpine' /etc/os-release; then + apk add --no-cache ncurses >/dev/null 2>&1 + elif command -v apt-get >/dev/null 2>&1; then + apt-get update -qq >/dev/null + apt-get install -y -qq ncurses-bin >/dev/null 2>&1 + fi fi } -curl_handler() { - set +e - trap 'set -e' RETURN - local args=() - local url="" - local max_retries=3 - local delay=2 - local attempt=1 - local exit_code - local has_output_file=false - local result="" +is_alpine() { + local os_id="${var_os:-${PCT_OSTYPE:-}}" - # Parse arguments - for arg in "$@"; do - if [[ "$arg" != -* && -z "$url" ]]; then - url="$arg" - fi - [[ "$arg" == "-o" || "$arg" == --output ]] && has_output_file=true - args+=("$arg") - done - - if [[ -z "$url" ]]; then - msg_error "No valid URL or option entered for curl_handler" - return 1 + if [[ -z "$os_id" && -f /etc/os-release ]]; then + os_id="$( + . /etc/os-release 2>/dev/null + echo "${ID:-}" + )" fi - $STD msg_info "Fetching: $url" + [[ "$os_id" == "alpine" ]] +} - while [[ $attempt -le $max_retries ]]; do - if $has_output_file; then - $STD run_curl "${args[@]}" - exit_code=$? - else - result=$(run_curl "${args[@]}") - exit_code=$? - fi - - if [[ $exit_code -eq 0 ]]; then - $STD msg_ok "Fetched: $url" - $has_output_file || printf '%s' "$result" - return 0 - fi - - if ((attempt >= max_retries)); then - # Read error log if it exists - if [ -s /tmp/curl_error.log ]; then - local curl_stderr - curl_stderr=$(&2 - sleep "$delay" - ((attempt++)) - done - set -e +is_verbose_mode() { + local verbose="${VERBOSE:-${var_verbose:-no}}" + local tty_status + if [[ -t 2 ]]; then + tty_status="interactive" + else + tty_status="not-a-tty" + fi + [[ "$verbose" != "no" || ! -t 2 ]] } # ------------------------------------------------------------------------------ @@ -372,144 +269,93 @@ fatal() { kill -INT $$ } -# Ensure POSIX compatibility across Alpine and Debian/Ubuntu -# === Spinner Start === -# Trap cleanup on various signals -trap 'cleanup_spinner' EXIT INT TERM HUP - -spinner_frames=('⠋' '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏') - -# === Spinner Start === -start_spinner() { - local msg="$1" - local spin_i=0 - local interval=0.1 - - stop_spinner - SPINNER_MSG="$msg" - SPINNER_ACTIVE=1 - - { - while [[ "$SPINNER_ACTIVE" -eq 1 ]]; do - if [[ -t 2 ]]; then - printf "\r\e[2K%s %b" "${TAB}${spinner_frames[spin_i]}${TAB}" "${YW}${SPINNER_MSG}${CL}" >&2 - else - printf "%s...\n" "$SPINNER_MSG" >&2 - break - fi - spin_i=$(((spin_i + 1) % ${#spinner_frames[@]})) - sleep "$interval" - done - } & - - local pid=$! - if ps -p "$pid" >/dev/null 2>&1; then - SPINNER_PID="$pid" - else - SPINNER_ACTIVE=0 - SPINNER_PID="" - fi +spinner() { + local chars=(⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏) + local i=0 + while true; do + local index=$((i++ % ${#chars[@]})) + printf "\r\033[2K%s %b" "${CS_YWB}${chars[$index]}${CS_CL}" "${CS_YWB}${SPINNER_MSG:-}${CS_CL}" + sleep 0.1 + done +} + +clear_line() { + tput cr 2>/dev/null || echo -en "\r" + tput el 2>/dev/null || echo -en "\033[K" } -# === Spinner Stop === stop_spinner() { - if [[ "$SPINNER_ACTIVE" -eq 1 && -n "$SPINNER_PID" ]]; then - SPINNER_ACTIVE=0 + local pid="${SPINNER_PID:-}" + [[ -z "$pid" && -f /tmp/.spinner.pid ]] && pid=$(/dev/null; then - kill "$SPINNER_PID" 2>/dev/null || true - for _ in $(seq 1 10); do - sleep 0.05 - kill -0 "$SPINNER_PID" 2>/dev/null || break - done + if [[ -n "$pid" && "$pid" =~ ^[0-9]+$ ]]; then + if kill "$pid" 2>/dev/null; then + sleep 0.05 + kill -9 "$pid" 2>/dev/null || true + wait "$pid" 2>/dev/null || true fi - - if [[ "$SPINNER_PID" =~ ^[0-9]+$ ]]; then - ps -p "$SPINNER_PID" -o pid= >/dev/null 2>&1 && wait "$SPINNER_PID" 2>/dev/null || true - fi - - printf "\r\e[2K" >&2 - SPINNER_PID="" + rm -f /tmp/.spinner.pid fi -} -cleanup_spinner() { - stop_spinner + unset SPINNER_PID SPINNER_MSG + stty sane 2>/dev/null || true } msg_info() { local msg="$1" - [[ -z "$msg" || -n "${MSG_INFO_SHOWN["$msg"]+x}" ]] && return + [[ -z "$msg" ]] && return + + if ! declare -p MSG_INFO_SHOWN &>/dev/null || ! declare -A MSG_INFO_SHOWN &>/dev/null; then + declare -gA MSG_INFO_SHOWN=() + fi + [[ -n "${MSG_INFO_SHOWN["$msg"]+x}" ]] && return MSG_INFO_SHOWN["$msg"]=1 stop_spinner + SPINNER_MSG="$msg" - if [[ "${VERBOSE:-no}" == "no" && -t 2 ]]; then - start_spinner "$msg" - else + if is_verbose_mode || is_alpine; then + local HOURGLASS="${TAB}⏳${TAB}" printf "\r\e[2K%s %b" "$HOURGLASS" "${YW}${msg}${CL}" >&2 + return fi + + color_spinner + spinner & + SPINNER_PID=$! + echo "$SPINNER_PID" >/tmp/.spinner.pid + disown "$SPINNER_PID" 2>/dev/null || true } msg_ok() { local msg="$1" [[ -z "$msg" ]] && return stop_spinner - printf "\r\e[2K%s %b\n" "$CM" "${GN}${msg}${CL}" >&2 + clear_line + printf "%s %b\n" "$CM" "${GN}${msg}${CL}" >&2 unset MSG_INFO_SHOWN["$msg"] } msg_error() { - local msg="$1" - [[ -z "$msg" ]] && return stop_spinner - printf "\r\e[2K%s %b\n" "$CROSS" "${RD}${msg}${CL}" >&2 + local msg="$1" + echo -e "${BFR:-} ${CROSS:-✖️} ${RD}${msg}${CL}" } msg_warn() { - local msg="$1" - [[ -z "$msg" ]] && return stop_spinner - printf "\r\e[2K%s %b\n" "$INFO" "${YWB}${msg}${CL}" >&2 - unset MSG_INFO_SHOWN["$msg"] + local msg="$1" + echo -e "${BFR:-} ${INFO:-ℹ️} ${YWB}${msg}${CL}" } msg_custom() { local symbol="${1:-"[*]"}" - local color="${2:-"\e[36m"}" # Default: Cyan + local color="${2:-"\e[36m"}" local msg="${3:-}" - [[ -z "$msg" ]] && return - stop_spinner 2>/dev/null || true - printf "\r\e[2K%s %b\n" "$symbol" "${color}${msg}${CL:-\e[0m}" >&2 -} - -msg_progress() { - local current="$1" - local total="$2" - local label="$3" - local width=40 - local filled percent bar empty - local fill_char="#" - local empty_char="-" - - if ! [[ "$current" =~ ^[0-9]+$ ]] || ! [[ "$total" =~ ^[0-9]+$ ]] || [[ "$total" -eq 0 ]]; then - printf "\r\e[2K%s %b\n" "$CROSS" "${RD}Invalid progress input${CL}" >&2 - return - fi - - percent=$(((current * 100) / total)) - filled=$(((current * width) / total)) - empty=$((width - filled)) - - bar=$(printf "%${filled}s" | tr ' ' "$fill_char") - bar+=$(printf "%${empty}s" | tr ' ' "$empty_char") - - printf "\r\e[2K%s [%s] %3d%% %s" "${TAB}" "$bar" "$percent" "$label" >&2 - - if [[ "$current" -eq "$total" ]]; then - printf "\n" >&2 - fi + stop_spinner + echo -e "${BFR:-} ${symbol} ${color}${msg}${CL:-\e[0m}" + printf "\r\033[K\e[?25h\n" } run_container_safe() { @@ -560,3 +406,5 @@ check_or_create_swap() { return 1 fi } + +trap 'stop_spinner' EXIT INT TERM diff --git a/misc/create_lxc.sh b/misc/create_lxc.sh index 2fce5b826..21f1a5123 100644 --- a/misc/create_lxc.sh +++ b/misc/create_lxc.sh @@ -21,36 +21,67 @@ fi # This sets error handling options and defines the error_handler function to handle errors set -Eeuo pipefail trap 'error_handler $LINENO "$BASH_COMMAND"' ERR +trap on_exit EXIT +trap on_interrupt INT +trap on_terminate TERM + +function on_exit() { + local exit_code="$?" + [[ -n "${lockfile:-}" && -e "$lockfile" ]] && rm -f "$lockfile" + exit "$exit_code" +} -# This function handles errors function error_handler() { - printf "\e[?25h" + local exit_code="$?" local line_number="$1" local command="$2" - local error_message="${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}" - echo -e "\n$error_message\n" - exit 200 + printf "\e[?25h" + echo -e "\n${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}\n" + exit "$exit_code" +} + +function on_interrupt() { + echo -e "\n${RD}Interrupted by user (SIGINT)${CL}" + exit 130 +} + +function on_terminate() { + echo -e "\n${RD}Terminated by signal (SIGTERM)${CL}" + exit 143 +} + +function check_storage_support() { + local CONTENT="$1" + local -a VALID_STORAGES=() + + while IFS= read -r line; do + local STORAGE=$(awk '{print $1}' <<<"$line") + [[ "$STORAGE" == "storage" || -z "$STORAGE" ]] && continue + VALID_STORAGES+=("$STORAGE") + done < <(pvesm status -content "$CONTENT" 2>/dev/null | awk 'NR>1') + + [[ ${#VALID_STORAGES[@]} -gt 0 ]] } # This checks for the presence of valid Container Storage and Template Storage locations msg_info "Validating Storage" -VALIDCT=$(pvesm status -content rootdir | awk 'NR>1') -if [ -z "$VALIDCT" ]; then - msg_error "Unable to detect a valid Container Storage location." +if ! check_storage_support "rootdir"; then + + msg_error "No valid storage found for 'rootdir' (Container)." exit 1 fi -VALIDTMP=$(pvesm status -content vztmpl | awk 'NR>1') -if [ -z "$VALIDTMP" ]; then - msg_error "Unable to detect a valid Template Storage location." +if ! check_storage_support "vztmpl"; then + + msg_error "No valid storage found for 'vztmpl' (Template)." exit 1 fi +msg_ok "Validated Storage (rootdir / vztmpl)." # This function is used to select the storage class and determine the corresponding storage content type and label. function select_storage() { - local CLASS=$1 - local CONTENT - local CONTENT_LABEL + local CLASS=$1 CONTENT CONTENT_LABEL + case $CLASS in container) CONTENT='rootdir' @@ -60,50 +91,74 @@ function select_storage() { CONTENT='vztmpl' CONTENT_LABEL='Container template' ;; - *) false || { - msg_error "Invalid storage class." - exit 201 - } ;; + iso) + CONTENT='iso' + CONTENT_LABEL='ISO image' + ;; + images) + CONTENT='images' + CONTENT_LABEL='VM Disk image' + ;; + backup) + CONTENT='backup' + CONTENT_LABEL='Backup' + ;; + snippets) + CONTENT='snippets' + CONTENT_LABEL='Snippets' + ;; + *) + msg_error "Invalid storage class '$CLASS'" + return 1 + ;; esac - # Collect storage options local -a MENU - local MSG_MAX_LENGTH=0 - - while read -r TAG TYPE _ _ _ FREE _; do - local TYPE_PADDED - local FREE_FMT - - TYPE_PADDED=$(printf "%-10s" "$TYPE") - FREE_FMT=$(numfmt --to=iec --from-unit=K --format %.2f <<<"$FREE")B - local ITEM="Type: $TYPE_PADDED Free: $FREE_FMT" - - ((${#ITEM} + 2 > MSG_MAX_LENGTH)) && MSG_MAX_LENGTH=$((${#ITEM} + 2)) - - MENU+=("$TAG" "$ITEM" "OFF") + local -A STORAGE_MAP + local COL_WIDTH=0 + while read -r TAG TYPE _ TOTAL USED FREE _; do + [[ -n "$TAG" && -n "$TYPE" ]] || continue + local DISPLAY="${TAG} (${TYPE})" + local USED_FMT=$(numfmt --to=iec --from-unit=K --format %.1f <<<"$USED") + local FREE_FMT=$(numfmt --to=iec --from-unit=K --format %.1f <<<"$FREE") + local INFO="Free: ${FREE_FMT}B Used: ${USED_FMT}B" + STORAGE_MAP["$DISPLAY"]="$TAG" + MENU+=("$DISPLAY" "$INFO" "OFF") + ((${#DISPLAY} > COL_WIDTH)) && COL_WIDTH=${#DISPLAY} done < <(pvesm status -content "$CONTENT" | awk 'NR>1') - local OPTION_COUNT=$((${#MENU[@]} / 3)) + if [ ${#MENU[@]} -eq 0 ]; then + msg_error "No storage found for content type '$CONTENT'." + return 2 + fi + + if [ $((${#MENU[@]} / 3)) -eq 1 ]; then + STORAGE_RESULT="${STORAGE_MAP[${MENU[0]}]}" - # Auto-select if only one option available - if [[ "$OPTION_COUNT" -eq 1 ]]; then - echo "${MENU[0]}" return 0 fi - # Display selection menu - local STORAGE - while [[ -z "${STORAGE:+x}" ]]; do - STORAGE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "Storage Pools" --radiolist \ - "Select the storage pool to use for the ${CONTENT_LABEL,,}.\nUse the spacebar to make a selection.\n" \ - 16 $((MSG_MAX_LENGTH + 23)) 6 \ - "${MENU[@]}" 3>&1 1>&2 2>&3) || { - msg_error "Storage selection cancelled." - exit 202 - } - done + local WIDTH=$((COL_WIDTH + 42)) - echo "$STORAGE" + while true; do + local DISPLAY_SELECTED=$( + whiptail --backtitle "Proxmox VE Helper Scripts" \ + --title "Storage Pools" \ + --radiolist "Which storage pool for ${CONTENT_LABEL,,}?\n(Spacebar to select)" + + 16 "$WIDTH" 6 "${MENU[@]}" 3>&1 1>&2 2>&3 + ) + + [[ $? -ne 0 ]] && return 3 + + if [[ -z "$DISPLAY_SELECTED" || -z "${STORAGE_MAP[$DISPLAY_SELECTED]+_}" ]]; then + whiptail --msgbox "No valid storage selected. Please try again." 8 58 + continue + fi + + STORAGE_RESULT="${STORAGE_MAP[$DISPLAY_SELECTED]}" + return 0 + done } # Test if required variables are set [[ "${CTID:-}" ]] || { @@ -137,6 +192,19 @@ msg_ok "Using ${BL}$TEMPLATE_STORAGE${CL} ${GN}for Template Storage." CONTAINER_STORAGE=$(select_storage container) msg_ok "Using ${BL}$CONTAINER_STORAGE${CL} ${GN}for Container Storage." +while true; do + if select_storage template; then + TEMPLATE_STORAGE="$STORAGE_RESULT" + break + fi +done + +while true; do + if select_storage container; then + CONTAINER_STORAGE="$STORAGE_RESULT" + break + fi +done # Check free space on selected container storage STORAGE_FREE=$(pvesm status | awk -v s="$CONTAINER_STORAGE" '$1 == s { print $6 }') REQUIRED_KB=$((${PCT_DISK_SIZE:-8} * 1024 * 1024)) @@ -205,6 +273,8 @@ if ! pveam list "$TEMPLATE_STORAGE" | grep -q "$TEMPLATE" || ! zstdcat "$TEMPLAT fi msg_ok "LXC Template '$TEMPLATE' is ready to use." + +msg_info "Creating LXC Container" # Check and fix subuid/subgid grep -q "root:100000:65536" /etc/subuid || echo "root:100000:65536" >>/etc/subuid grep -q "root:100000:65536" /etc/subgid || echo "root:100000:65536" >>/etc/subgid @@ -215,12 +285,15 @@ PCT_OPTIONS=(${PCT_OPTIONS[@]:-${DEFAULT_PCT_OPTIONS[@]}}) # Secure creation of the LXC container with lock and template check lockfile="/tmp/template.${TEMPLATE}.lock" -exec 9>"$lockfile" +exec 9>"$lockfile" >/dev/null 2>&1 || { + msg_error "Failed to create lock file '$lockfile'." + exit 200 +} flock -w 60 9 || { msg_error "Timeout while waiting for template lock" exit 211 } -msg_info "Creating LXC Container" + if ! pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" "${PCT_OPTIONS[@]}" &>/dev/null; then msg_error "Container creation failed. Checking if template is corrupted or incomplete." @@ -252,16 +325,23 @@ if ! pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" "${PCT_OPTIONS[ sleep 1 # I/O-Sync-Delay msg_ok "Re-downloaded LXC Template" +fi - if ! pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" "${PCT_OPTIONS[@]}" &>/dev/null; then - msg_error "Container creation failed after re-downloading template." - exit 200 +if ! pct list | awk '{print $1}' | grep -qx "$CTID"; then + msg_error "Container ID $CTID not listed in 'pct list' – unexpected failure." + exit 215 +fi + +if ! grep -q '^rootfs:' "/etc/pve/lxc/$CTID.conf"; then + msg_error "RootFS entry missing in container config – storage not correctly assigned." + exit 216 +fi + +if grep -q '^hostname:' "/etc/pve/lxc/$CTID.conf"; then + CT_HOSTNAME=$(grep '^hostname:' "/etc/pve/lxc/$CTID.conf" | awk '{print $2}') + if [[ ! "$CT_HOSTNAME" =~ ^[a-z0-9-]+$ ]]; then + msg_warn "Hostname '$CT_HOSTNAME' contains invalid characters – may cause issues with networking or DNS." fi fi -if ! pct status "$CTID" &>/dev/null; then - msg_error "Container not found after pct create – assuming failure." - exit 210 -fi - msg_ok "LXC Container ${BL}$CTID${CL} ${GN}was successfully created." diff --git a/misc/install.func b/misc/install.func index 6ec040a1c..4dac430a4 100644 --- a/misc/install.func +++ b/misc/install.func @@ -61,8 +61,7 @@ setting_up_container() { fi rm -rf /usr/lib/python3.*/EXTERNALLY-MANAGED systemctl disable -q --now systemd-networkd-wait-online.service - msg_ok "Set up Container OS" - msg_custom "${CM}" "${GN}" "Network Connected: ${BL}$(hostname -I)" + msg_ok "Set up Container OS | Network Connected: ${BL}$(hostname -I" } # This function checks the network connection by pinging a known IP address and prompts the user to continue if the internet is not connected @@ -100,25 +99,26 @@ network_check() { fi # DNS resolution checks for GitHub-related domains (IPv4 and/or IPv6) - GITHUB_HOSTS=("github.com" "raw.githubusercontent.com" "api.github.com") - GITHUB_STATUS="GitHub DNS:" + GIT_HOSTS=("github.com" "raw.githubusercontent.com" "api.github.com" "git.community-scripts.org") + GIT_STATUS="Git DNS:" DNS_FAILED=false - for HOST in "${GITHUB_HOSTS[@]}"; do + for HOST in "${GIT_HOSTS[@]}"; do RESOLVEDIP=$(getent hosts "$HOST" | awk '{ print $1 }' | grep -E '(^([0-9]{1,3}\.){3}[0-9]{1,3}$)|(^[a-fA-F0-9:]+$)' | head -n1) if [[ -z "$RESOLVEDIP" ]]; then - GITHUB_STATUS+="$HOST:($DNSFAIL)" + GIT_STATUS+="$HOST:($DNSFAIL)" DNS_FAILED=true else - GITHUB_STATUS+=" $HOST:($DNSOK)" + GIT_STATUS+=" $HOST:($DNSOK)" fi done if [[ "$DNS_FAILED" == true ]]; then - fatal "$GITHUB_STATUS" + fatal "$GIT_STATUS" else - msg_ok "$GITHUB_STATUS" + msg_ok "$GIT_STATUS" fi + set -e trap 'error_handler $LINENO "$BASH_COMMAND"' ERR } diff --git a/misc/tools.func b/misc/tools.func index 2da38b4d8..d060740bf 100644 --- a/misc/tools.func +++ b/misc/tools.func @@ -239,10 +239,14 @@ setup_mariadb() { DISTRO_CODENAME="$(awk -F= '/^VERSION_CODENAME=/{print $2}' /etc/os-release)" CURRENT_OS="$(awk -F= '/^ID=/{print $2}' /etc/os-release)" + if ! curl -fsI http://mirror.mariadb.org/repo/ >/dev/null; then + msg_error "MariaDB mirror not reachable" + return 1 + fi + msg_info "Setting up MariaDB $MARIADB_VERSION" # grab dynamic latest LTS version if [[ "$MARIADB_VERSION" == "latest" ]]; then - $STD msg_info "Resolving latest GA MariaDB version" MARIADB_VERSION=$(curl -fsSL http://mirror.mariadb.org/repo/ | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+/' | grep -vE 'rc/|rolling/' | @@ -253,7 +257,6 @@ setup_mariadb() { msg_error "Could not determine latest GA MariaDB version" return 1 fi - $STD msg_ok "Latest GA MariaDB version is $MARIADB_VERSION" fi local CURRENT_VERSION="" @@ -278,7 +281,6 @@ setup_mariadb() { $STD msg_info "Setup MariaDB $MARIADB_VERSION" fi - $STD msg_info "Setting up MariaDB Repository" curl -fsSL "https://mariadb.org/mariadb_release_signing_key.asc" | gpg --dearmor -o /etc/apt/trusted.gpg.d/mariadb.gpg