diff --git a/scripts/core/alpine-install.func b/scripts/core/alpine-install.func index 291708a..1a682ae 100644 --- a/scripts/core/alpine-install.func +++ b/scripts/core/alpine-install.func @@ -11,6 +11,9 @@ source "$(dirname "${BASH_SOURCE[0]}")/error-handler.func" load_functions catch_errors +# Get LXC IP address (must be called INSIDE container, after network is up) +get_lxc_ip + # This function enables IPv6 if it's not disabled and sets verbose mode verb_ip6() { set_std_mode # Set STD mode based on VERBOSE @@ -125,22 +128,13 @@ update_os() { # This function modifies the message of the day (motd) and SSH settings motd_ssh() { echo "export TERM='xterm-256color'" >>/root/.bashrc - IP=$(ip -4 addr show eth0 | awk '/inet / {print $2}' | cut -d/ -f1 | head -n 1) - - if [ -f "/etc/os-release" ]; then - OS_NAME=$(grep ^NAME /etc/os-release | cut -d= -f2 | tr -d '"') - OS_VERSION=$(grep ^VERSION_ID /etc/os-release | cut -d= -f2 | tr -d '"') - else - OS_NAME="Alpine Linux" - OS_VERSION="Unknown" - fi PROFILE_FILE="/etc/profile.d/00_lxc-details.sh" echo "echo -e \"\"" >"$PROFILE_FILE" echo -e "echo -e \"${BOLD}${APPLICATION} LXC Container${CL}"\" >>"$PROFILE_FILE" echo -e "echo -e \"${TAB}${GATEWAY}${YW} Provided by: ${GN}community-scripts ORG ${YW}| GitHub: ${GN}https://github.com/community-scripts/ProxmoxVE${CL}\"" >>"$PROFILE_FILE" echo "echo \"\"" >>"$PROFILE_FILE" - echo -e "echo -e \"${TAB}${OS}${YW} OS: ${GN}${OS_NAME} - Version: ${OS_VERSION}${CL}\"" >>"$PROFILE_FILE" + echo -e "echo -e \"${TAB}${OS}${YW} OS: ${GN}\$(grep ^NAME /etc/os-release | cut -d= -f2 | tr -d '\"') - Version: \$(grep ^VERSION_ID /etc/os-release | cut -d= -f2 | tr -d '\"')${CL}\"" >>"$PROFILE_FILE" echo -e "echo -e \"${TAB}${HOSTNAME}${YW} Hostname: ${GN}\$(hostname)${CL}\"" >>"$PROFILE_FILE" echo -e "echo -e \"${TAB}${INFO}${YW} IP Address: ${GN}\$(ip -4 addr show eth0 | awk '/inet / {print \$2}' | cut -d/ -f1 | head -n 1)${CL}\"" >>"$PROFILE_FILE" diff --git a/scripts/core/alpine-tools.func b/scripts/core/alpine-tools.func index 0acb6ab..34729e0 100644 --- a/scripts/core/alpine-tools.func +++ b/scripts/core/alpine-tools.func @@ -1,507 +1,188 @@ #!/bin/ash # shellcheck shell=ash +# Copyright (c) 2021-2026 community-scripts ORG +# Author: MickLesk +# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE -# Expects existing msg_* functions and optional $STD from the framework. +if ! command -v curl >/dev/null 2>&1; then + apk update && apk add curl >/dev/null 2>&1 +fi +source "$(dirname "${BASH_SOURCE[0]}")/core.func" +source "$(dirname "${BASH_SOURCE[0]}")/error-handler.func" +load_functions +catch_errors -# ------------------------------ -# helpers -# ------------------------------ -lower() { printf '%s' "$1" | tr '[:upper:]' '[:lower:]'; } -has() { command -v "$1" >/dev/null 2>&1; } +# Get LXC IP address (must be called INSIDE container, after network is up) +get_lxc_ip -need_tool() { - # usage: need_tool curl jq unzip ... - # setup missing tools via apk - local missing=0 t - for t in "$@"; do - if ! has "$t"; then missing=1; fi +# This function enables IPv6 if it's not disabled and sets verbose mode +verb_ip6() { + set_std_mode # Set STD mode based on VERBOSE + + if [ "${IPV6_METHOD:-}" = "disable" ]; then + msg_info "Disabling IPv6 (this may affect some services)" + $STD sysctl -w net.ipv6.conf.all.disable_ipv6=1 + $STD sysctl -w net.ipv6.conf.default.disable_ipv6=1 + $STD sysctl -w net.ipv6.conf.lo.disable_ipv6=1 + mkdir -p /etc/sysctl.d + $STD tee /etc/sysctl.d/99-disable-ipv6.conf >/dev/null <&2 -en "${CROSS}${RD} No Network! " + sleep $RETRY_EVERY + i=$((i - 1)) done - if [ "$missing" -eq 1 ]; then - msg_info "Installing tools: $*" - apk add --no-cache "$@" >/dev/null 2>&1 || { - msg_error "apk add failed for: $*" - return 1 - } - msg_ok "Tools ready: $*" + + if [ "$(ip addr show | grep 'inet ' | grep -v '127.0.0.1' | awk '{print $2}' | cut -d'/' -f1)" = "" ]; then + echo 1>&2 -e "\n${CROSS}${RD} No Network After $RETRY_NUM Tries${CL}" + echo -e "${NETWORK}Check Network Settings" + exit 1 fi + msg_ok "Set up Container OS" + msg_ok "Network Connected: ${BL}$(ip addr show | grep 'inet ' | awk '{print $2}' | cut -d'/' -f1 | tail -n1)${CL}" } -net_resolves() { - # better handling for missing getent on Alpine - # usage: net_resolves api.github.com - local host="$1" - ping -c1 -W1 "$host" >/dev/null 2>&1 || nslookup "$host" >/dev/null 2>&1 -} - -ensure_usr_local_bin_persist() { - local PROFILE_FILE="/etc/profile.d/10-localbin.sh" - if [ ! -f "$PROFILE_FILE" ]; then - echo 'case ":$PATH:" in *:/usr/local/bin:*) ;; *) export PATH="/usr/local/bin:$PATH";; esac' >"$PROFILE_FILE" - chmod +x "$PROFILE_FILE" - fi -} - -download_with_progress() { - # $1 url, $2 dest - local url="$1" out="$2" cl - need_tool curl pv || return 1 - cl=$(curl -fsSLI "$url" 2>/dev/null | awk 'tolower($0) ~ /^content-length:/ {print $2}' | tr -d '\r') - if [ -n "$cl" ]; then - curl -fsSL "$url" | pv -s "$cl" >"$out" || { - msg_error "Download failed: $url" - return 1 - } +# This function checks the network connection by pinging a known IP address and prompts the user to continue if the internet is not connected +network_check() { + set +e + trap - ERR + if ping -c 1 -W 1 1.1.1.1 &>/dev/null || ping -c 1 -W 1 8.8.8.8 &>/dev/null || ping -c 1 -W 1 9.9.9.9 &>/dev/null; then + ipv4_status="${GN}✔${CL} IPv4" else - curl -fL# -o "$out" "$url" || { - msg_error "Download failed: $url" - return 1 - } - fi -} - -# ------------------------------ -# GitHub: check Release -# ------------------------------ -check_for_gh_release() { - # app, repo, [pinned] - local app="$1" source="$2" pinned="${3:-}" - local app_lc - app_lc="$(lower "$app" | tr -d ' ')" - local current_file="$HOME/.${app_lc}" - local current="" release tag - - msg_info "Check for update: $app" - - net_resolves api.github.com || { - msg_error "DNS/network error: api.github.com" - return 1 - } - need_tool curl jq || return 1 - - tag=$(curl -fsSL "https://api.github.com/repos/${source}/releases/latest" | jq -r '.tag_name // empty') - [ -z "$tag" ] && { - msg_error "Unable to fetch latest tag for $app" - return 1 - } - release="${tag#v}" - - [ -f "$current_file" ] && current="$(cat "$current_file")" - - if [ -n "$pinned" ]; then - if [ "$pinned" = "$release" ]; then - msg_ok "$app pinned to v$pinned (no update)" - return 1 - fi - if [ "$current" = "$pinned" ]; then - msg_ok "$app pinned v$pinned installed (upstream v$release)" - return 1 - fi - msg_info "$app pinned v$pinned (upstream v$release) → update/downgrade" - CHECK_UPDATE_RELEASE="$pinned" - return 0 - fi - - if [ "$release" != "$current" ] || [ ! -f "$current_file" ]; then - CHECK_UPDATE_RELEASE="$release" - msg_info "New release available: v$release (current: v${current:-none})" - return 0 - fi - - msg_ok "$app is up to date (v$release)" - return 1 -} - -# ------------------------------ -# GitHub: get Release & deploy (Alpine) -# modes: tarball | prebuild | singlefile -# ------------------------------ -fetch_and_deploy_gh() { - # $1 app, $2 repo, [$3 mode], [$4 version], [$5 target], [$6 asset_pattern - local app="$1" repo="$2" mode="${3:-tarball}" version="${4:-latest}" target="${5:-/opt/$1}" pattern="${6:-}" - local app_lc - app_lc="$(lower "$app" | tr -d ' ')" - local vfile="$HOME/.${app_lc}" - local json url filename tmpd unpack - - net_resolves api.github.com || { - msg_error "DNS/network error" - return 1 - } - need_tool curl jq tar || return 1 - [ "$mode" = "prebuild" ] || [ "$mode" = "singlefile" ] && need_tool unzip >/dev/null 2>&1 || true - - tmpd="$(mktemp -d)" || return 1 - mkdir -p "$target" - - # Release JSON - if [ "$version" = "latest" ]; then - json="$(curl -fsSL "https://api.github.com/repos/$repo/releases/latest")" || { - msg_error "GitHub API failed" - rm -rf "$tmpd" - return 1 - } - else - json="$(curl -fsSL "https://api.github.com/repos/$repo/releases/tags/$version")" || { - msg_error "GitHub API failed" - rm -rf "$tmpd" - return 1 - } - fi - - # correct Version - version="$(printf '%s' "$json" | jq -r '.tag_name // empty')" - version="${version#v}" - - [ -z "$version" ] && { - msg_error "No tag in release json" - rm -rf "$tmpd" - return 1 - } - - case "$mode" in - tarball | source) - url="$(printf '%s' "$json" | jq -r '.tarball_url // empty')" - [ -z "$url" ] && url="https://github.com/$repo/archive/refs/tags/v$version.tar.gz" - filename="${app_lc}-${version}.tar.gz" - download_with_progress "$url" "$tmpd/$filename" || { - rm -rf "$tmpd" - return 1 - } - tar -xzf "$tmpd/$filename" -C "$tmpd" || { - msg_error "tar extract failed" - rm -rf "$tmpd" - return 1 - } - unpack="$(find "$tmpd" -mindepth 1 -maxdepth 1 -type d | head -n1)" - # copy content of unpack to target - (cd "$unpack" && tar -cf - .) | (cd "$target" && tar -xf -) || { - msg_error "copy failed" - rm -rf "$tmpd" - return 1 - } - ;; - prebuild) - [ -n "$pattern" ] || { - msg_error "prebuild requires asset pattern" - rm -rf "$tmpd" - return 1 - } - url="$(printf '%s' "$json" | jq -r '.assets[].browser_download_url' | awk -v p="$pattern" ' - BEGIN{IGNORECASE=1} - $0 ~ p {print; exit} - ')" - [ -z "$url" ] && { - msg_error "asset not found for pattern: $pattern" - rm -rf "$tmpd" - return 1 - } - filename="${url##*/}" - download_with_progress "$url" "$tmpd/$filename" || { - rm -rf "$tmpd" - return 1 - } - # unpack archive (Zip or tarball) - case "$filename" in - *.zip) - need_tool unzip || { - rm -rf "$tmpd" - return 1 - } - mkdir -p "$tmpd/unp" - unzip -q "$tmpd/$filename" -d "$tmpd/unp" - ;; - *.tar.gz | *.tgz | *.tar.xz | *.tar.zst | *.tar.bz2) - mkdir -p "$tmpd/unp" - tar -xf "$tmpd/$filename" -C "$tmpd/unp" - ;; - *) - msg_error "unsupported archive: $filename" - rm -rf "$tmpd" - return 1 - ;; - esac - # top-level folder strippen - if [ "$(find "$tmpd/unp" -mindepth 1 -maxdepth 1 -type d | wc -l)" -eq 1 ] && [ -z "$(find "$tmpd/unp" -mindepth 1 -maxdepth 1 -type f | head -n1)" ]; then - unpack="$(find "$tmpd/unp" -mindepth 1 -maxdepth 1 -type d)" - (cd "$unpack" && tar -cf - .) | (cd "$target" && tar -xf -) || { - msg_error "copy failed" - rm -rf "$tmpd" - return 1 - } + ipv4_status="${RD}✖${CL} IPv4" + read -r -p "Internet NOT connected. Continue anyway? " prompt + if [[ "${prompt,,}" =~ ^(y|yes)$ ]]; then + echo -e "${INFO}${RD}Expect Issues Without Internet${CL}" else - (cd "$tmpd/unp" && tar -cf - .) | (cd "$target" && tar -xf -) || { - msg_error "copy failed" - rm -rf "$tmpd" - return 1 - } - fi - ;; - singlefile) - [ -n "$pattern" ] || { - msg_error "singlefile requires asset pattern" - rm -rf "$tmpd" - return 1 - } - url="$(printf '%s' "$json" | jq -r '.assets[].browser_download_url' | awk -v p="$pattern" ' - BEGIN{IGNORECASE=1} - $0 ~ p {print; exit} - ')" - [ -z "$url" ] && { - msg_error "asset not found for pattern: $pattern" - rm -rf "$tmpd" - return 1 - } - filename="${url##*/}" - download_with_progress "$url" "$target/$app" || { - rm -rf "$tmpd" - return 1 - } - chmod +x "$target/$app" - ;; - *) - msg_error "Unknown mode: $mode" - rm -rf "$tmpd" - return 1 - ;; - esac - - echo "$version" >"$vfile" - ensure_usr_local_bin_persist - rm -rf "$tmpd" - msg_ok "Deployed $app ($version) → $target" -} - -# ------------------------------ -# yq (mikefarah) – Alpine -# ------------------------------ -setup_yq() { - # prefer apk, unless FORCE_GH=1 - if [ "${FORCE_GH:-0}" != "1" ] && apk info -e yq >/dev/null 2>&1; then - msg_info "Updating yq via apk" - apk add --no-cache --upgrade yq >/dev/null 2>&1 || true - msg_ok "yq ready ($(yq --version 2>/dev/null))" - return 0 - fi - - need_tool curl || return 1 - local arch bin url tmp - case "$(uname -m)" in - x86_64) arch="amd64" ;; - aarch64) arch="arm64" ;; - *) - msg_error "Unsupported arch for yq: $(uname -m)" - return 1 - ;; - esac - url="https://github.com/mikefarah/yq/releases/latest/download/yq_linux_${arch}" - tmp="$(mktemp)" - download_with_progress "$url" "$tmp" || return 1 - install -m 0755 "$tmp" /usr/local/bin/yq - rm -f "$tmp" - msg_ok "Setup yq ($(yq --version 2>/dev/null))" -} - -# ------------------------------ -# Adminer – Alpine -# ------------------------------ -setup_adminer() { - need_tool curl || return 1 - msg_info "Setup Adminer (Alpine)" - mkdir -p /var/www/localhost/htdocs/adminer - curl -fsSL https://github.com/vrana/adminer/releases/latest/download/adminer.php \ - -o /var/www/localhost/htdocs/adminer/index.php || { - msg_error "Adminer download failed" - return 1 - } - msg_ok "Adminer at /adminer (served by your webserver)" -} - -# ------------------------------ -# uv – Alpine (musl tarball) -# optional: PYTHON_VERSION="3.12" -# ------------------------------ -setup_uv() { - need_tool curl tar || return 1 - local UV_BIN="/usr/local/bin/uv" - local arch tarball url tmpd ver installed - - case "$(uname -m)" in - x86_64) arch="x86_64-unknown-linux-musl" ;; - aarch64) arch="aarch64-unknown-linux-musl" ;; - *) - msg_error "Unsupported arch for uv: $(uname -m)" - return 1 - ;; - esac - - ver="$(curl -fsSL https://api.github.com/repos/astral-sh/uv/releases/latest | jq -r '.tag_name' 2>/dev/null)" - ver="${ver#v}" - [ -z "$ver" ] && { - msg_error "uv: cannot determine latest version" - return 1 - } - - if has "$UV_BIN"; then - installed="$($UV_BIN -V 2>/dev/null | awk '{print $2}')" - [ "$installed" = "$ver" ] && { - msg_ok "uv $ver already installed" - return 0 - } - msg_info "Updating uv $installed → $ver" - else - msg_info "Setup uv $ver" - fi - - tmpd="$(mktemp -d)" || return 1 - tarball="uv-${arch}.tar.gz" - url="https://github.com/astral-sh/uv/releases/download/v${ver}/${tarball}" - - download_with_progress "$url" "$tmpd/uv.tar.gz" || { - rm -rf "$tmpd" - return 1 - } - tar -xzf "$tmpd/uv.tar.gz" -C "$tmpd" || { - msg_error "uv: extract failed" - rm -rf "$tmpd" - return 1 - } - - # tar contains ./uv - if [ -x "$tmpd/uv" ]; then - install -m 0755 "$tmpd/uv" "$UV_BIN" - else - # fallback: in subfolder - install -m 0755 "$tmpd"/*/uv "$UV_BIN" 2>/dev/null || { - msg_error "uv binary not found in tar" - rm -rf "$tmpd" - return 1 - } - fi - rm -rf "$tmpd" - ensure_usr_local_bin_persist - msg_ok "Setup uv $ver" - - if [ -n "${PYTHON_VERSION:-}" ]; then - local match - match="$(uv python list --only-downloads 2>/dev/null | awk -v maj="$PYTHON_VERSION" ' - $0 ~ "^cpython-"maj"\\." { print $0 }' | awk -F- '{print $2}' | sort -V | tail -n1)" - [ -z "$match" ] && { - msg_error "No matching Python for $PYTHON_VERSION" - return 1 - } - if ! uv python list | grep -q "cpython-${match}-linux"; then - msg_info "Installing Python $match via uv" - uv python install "$match" || { - msg_error "uv python install failed" - return 1 - } - msg_ok "Python $match installed (uv)" + echo -e "${NETWORK}Check Network Settings" + exit 1 fi fi -} - -# ------------------------------ -# Java – Alpine (OpenJDK) -# JAVA_VERSION: 17|21 (Default 21) -# ------------------------------ -setup_java() { - local JAVA_VERSION="${JAVA_VERSION:-21}" pkg - case "$JAVA_VERSION" in - 17) pkg="openjdk17-jdk" ;; - 21 | *) pkg="openjdk21-jdk" ;; - esac - msg_info "Setup Java (OpenJDK $JAVA_VERSION)" - apk add --no-cache "$pkg" >/dev/null 2>&1 || { - msg_error "apk add $pkg failed" - return 1 - } - # set JAVA_HOME - local prof="/etc/profile.d/20-java.sh" - if [ ! -f "$prof" ]; then - echo 'export JAVA_HOME=$(dirname $(dirname $(readlink -f $(command -v java))))' >"$prof" - echo 'case ":$PATH:" in *:$JAVA_HOME/bin:*) ;; *) export PATH="$JAVA_HOME/bin:$PATH";; esac' >>"$prof" - chmod +x "$prof" - fi - msg_ok "Java ready: $(java -version 2>&1 | head -n1)" -} - -# ------------------------------ -# Go – Alpine (apk prefers, else tarball) -# ------------------------------ -setup_go() { - if [ -z "${GO_VERSION:-}" ]; then - msg_info "Setup Go (apk)" - apk add --no-cache go >/dev/null 2>&1 || { - msg_error "apk add go failed" - return 1 - } - msg_ok "Go ready: $(go version 2>/dev/null)" - return 0 - fi - - need_tool curl tar || return 1 - local ARCH TARBALL URL TMP - case "$(uname -m)" in - x86_64) ARCH="amd64" ;; - aarch64) ARCH="arm64" ;; - *) - msg_error "Unsupported arch for Go: $(uname -m)" - return 1 - ;; - esac - TARBALL="go${GO_VERSION}.linux-${ARCH}.tar.gz" - URL="https://go.dev/dl/${TARBALL}" - msg_info "Setup Go $GO_VERSION (tarball)" - TMP="$(mktemp)" - download_with_progress "$URL" "$TMP" || return 1 - rm -rf /usr/local/go - tar -C /usr/local -xzf "$TMP" || { - msg_error "extract go failed" - rm -f "$TMP" - return 1 - } - rm -f "$TMP" - ln -sf /usr/local/go/bin/go /usr/local/bin/go - ln -sf /usr/local/go/bin/gofmt /usr/local/bin/gofmt - ensure_usr_local_bin_persist - msg_ok "Go ready: $(go version 2>/dev/null)" -} - -# ------------------------------ -# Composer – Alpine -# uses php83-cli + openssl + phar -# ------------------------------ -setup_composer() { - local COMPOSER_BIN="/usr/local/bin/composer" - if ! has php; then - # prefers php83 - msg_info "Installing PHP CLI for Composer" - apk add --no-cache php83-cli php83-openssl php83-phar php83-iconv >/dev/null 2>&1 || { - # Fallback to generic php if 83 not available - apk add --no-cache php-cli php-openssl php-phar php-iconv >/dev/null 2>&1 || { - msg_error "Failed to install php-cli for composer" - return 1 - } - } - msg_ok "PHP CLI ready: $(php -v | head -n1)" - fi - - if [ -x "$COMPOSER_BIN" ]; then - msg_info "Updating Composer" + RESOLVEDIP=$(getent hosts github.com | awk '{ print $1 }') + if [[ -z "$RESOLVEDIP" ]]; then + msg_error "Internet: ${ipv4_status} DNS Failed" else - msg_info "Setup Composer" + msg_ok "Internet: ${ipv4_status} DNS: ${BL}${RESOLVEDIP}${CL}" + fi + set -e + trap 'error_handler $LINENO "$BASH_COMMAND"' ERR +} + +# This function updates the Container OS by running apt-get update and upgrade +update_os() { + msg_info "Updating Container OS" + $STD apk -U upgrade + source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/tools.func) + msg_ok "Updated Container OS" +} + +# This function modifies the message of the day (motd) and SSH settings +motd_ssh() { + echo "export TERM='xterm-256color'" >>/root/.bashrc + + PROFILE_FILE="/etc/profile.d/00_lxc-details.sh" + echo "echo -e \"\"" >"$PROFILE_FILE" + echo -e "echo -e \"${BOLD}${APPLICATION} LXC Container${CL}"\" >>"$PROFILE_FILE" + echo -e "echo -e \"${TAB}${GATEWAY}${YW} Provided by: ${GN}community-scripts ORG ${YW}| GitHub: ${GN}https://github.com/community-scripts/ProxmoxVE${CL}\"" >>"$PROFILE_FILE" + echo "echo \"\"" >>"$PROFILE_FILE" + echo -e "echo -e \"${TAB}${OS}${YW} OS: ${GN}\$(grep ^NAME /etc/os-release | cut -d= -f2 | tr -d '\"') - Version: \$(grep ^VERSION_ID /etc/os-release | cut -d= -f2 | tr -d '\"')${CL}\"" >>"$PROFILE_FILE" + echo -e "echo -e \"${TAB}${HOSTNAME}${YW} Hostname: ${GN}\$(hostname)${CL}\"" >>"$PROFILE_FILE" + echo -e "echo -e \"${TAB}${INFO}${YW} IP Address: ${GN}\$(ip -4 addr show eth0 | awk '/inet / {print \$2}' | cut -d/ -f1 | head -n 1)${CL}\"" >>"$PROFILE_FILE" + + # Configure SSH if enabled + if [[ "${SSH_ROOT}" == "yes" ]]; then + # Enable sshd service + $STD rc-update add sshd + # Allow root login via SSH + sed -i "s/#PermitRootLogin prohibit-password/PermitRootLogin yes/g" /etc/ssh/sshd_config + # Start the sshd service + $STD /etc/init.d/sshd start + fi +} + +# Validate Timezone for some LXC's +validate_tz() { + [[ -f "/usr/share/zoneinfo/$1" ]] +} + +# This function customizes the container and enables passwordless login for the root user +customize() { + if [[ "$PASSWORD" == "" ]]; then + msg_info "Customizing Container" + passwd -d root >/dev/null 2>&1 + + # Ensure agetty is available + apk add --no-cache --force-broken-world util-linux >/dev/null 2>&1 + + # Create persistent autologin boot script + mkdir -p /etc/local.d + cat <<'EOF' >/etc/local.d/autologin.start +#!/bin/sh +sed -i 's|^tty1::respawn:.*|tty1::respawn:/sbin/agetty --autologin root --noclear tty1 38400 linux|' /etc/inittab +kill -HUP 1 +EOF + touch /root/.hushlogin + + chmod +x /etc/local.d/autologin.start + rc-update add local >/dev/null 2>&1 + + # Apply autologin immediately for current session + /etc/local.d/autologin.start + + msg_ok "Customized Container" fi - need_tool curl || return 1 - curl -fsSL https://getcomposer.org/installer -o /tmp/composer-setup.php || { - msg_error "composer installer download failed" - return 1 - } - php /tmp/composer-setup.php --install-dir=/usr/local/bin --filename=composer >/dev/null 2>&1 || { - msg_error "composer install failed" - return 1 - } - rm -f /tmp/composer-setup.php - ensure_usr_local_bin_persist - msg_ok "Composer ready: $(composer --version 2>/dev/null)" + echo "bash -c \"\$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/${app}.sh)\"" >/usr/bin/update + chmod +x /usr/bin/update + } \ No newline at end of file diff --git a/scripts/core/build.func b/scripts/core/build.func index 356b19a..abfc722 100755 --- a/scripts/core/build.func +++ b/scripts/core/build.func @@ -160,17 +160,29 @@ maxkeys_check() { # # - Returns current container IP depending on OS type # - Debian/Ubuntu: uses `hostname -I` -# - Alpine: parses eth0 via `ip -4 addr` +# - Alpine: parses eth0 via `ip -4 addr` or `ip -6 addr` +# - Supports IPv6-only environments as fallback # - Returns "Unknown" if OS type cannot be determined # ------------------------------------------------------------------------------ get_current_ip() { + CURRENT_IP="" if [ -f /etc/os-release ]; then # Check for Debian/Ubuntu (uses hostname -I) if grep -qE 'ID=debian|ID=ubuntu' /etc/os-release; then - CURRENT_IP=$(hostname -I | awk '{print $1}') + # Try IPv4 first + CURRENT_IP=$(hostname -I 2>/dev/null | tr ' ' '\n' | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' | head -n1) + # Fallback to IPv6 if no IPv4 + if [[ -z "$CURRENT_IP" ]]; then + CURRENT_IP=$(hostname -I 2>/dev/null | tr ' ' '\n' | grep -E ':' | head -n1) + fi # Check for Alpine (uses ip command) elif grep -q 'ID=alpine' /etc/os-release; then - CURRENT_IP=$(ip -4 addr show eth0 | awk '/inet / {print $2}' | cut -d/ -f1 | head -n 1) + # Try IPv4 first + CURRENT_IP=$(ip -4 addr show eth0 2>/dev/null | awk '/inet / {print $2}' | cut -d/ -f1 | head -n 1) + # Fallback to IPv6 if no IPv4 + if [[ -z "$CURRENT_IP" ]]; then + CURRENT_IP=$(ip -6 addr show eth0 scope global 2>/dev/null | awk '/inet6 / {print $2}' | cut -d/ -f1 | head -n 1) + fi else CURRENT_IP="Unknown" fi @@ -183,9 +195,11 @@ get_current_ip() { # # - Updates /etc/motd with current container IP # - Removes old IP entries to avoid duplicates +# - Regenerates /etc/profile.d/00_lxc-details.sh with dynamic OS/IP info # ------------------------------------------------------------------------------ update_motd_ip() { MOTD_FILE="/etc/motd" + PROFILE_FILE="/etc/profile.d/00_lxc-details.sh" if [ -f "$MOTD_FILE" ]; then # Remove existing IP Address lines to prevent duplication @@ -195,6 +209,26 @@ update_motd_ip() { # Add the new IP address echo -e "${TAB}${NETWORK}${YW} IP Address: ${GN}${IP}${CL}" >>"$MOTD_FILE" fi + + # Update dynamic LXC details profile if values changed (e.g., after OS upgrade) + # Only update if file exists and is from community-scripts + if [ -f "$PROFILE_FILE" ] && grep -q "community-scripts" "$PROFILE_FILE" 2>/dev/null; then + # Get current values + local current_os="$(grep ^NAME /etc/os-release | cut -d= -f2 | tr -d '"') - Version: $(grep ^VERSION_ID /etc/os-release | cut -d= -f2 | tr -d '"')" + local current_hostname="$(hostname)" + local current_ip="$(hostname -I | awk '{print $1}')" + + # Update only if values actually changed + if ! grep -q "OS:.*$current_os" "$PROFILE_FILE" 2>/dev/null; then + sed -i "s|OS:.*|OS: \${GN}$current_os\${CL}\\\"|" "$PROFILE_FILE" + fi + if ! grep -q "Hostname:.*$current_hostname" "$PROFILE_FILE" 2>/dev/null; then + sed -i "s|Hostname:.*|Hostname: \${GN}$current_hostname\${CL}\\\"|" "$PROFILE_FILE" + fi + if ! grep -q "IP Address:.*$current_ip" "$PROFILE_FILE" 2>/dev/null; then + sed -i "s|IP Address:.*|IP Address: \${GN}$current_ip\${CL}\\\"|" "$PROFILE_FILE" + fi + fi } # ------------------------------------------------------------------------------ @@ -202,6 +236,7 @@ update_motd_ip() { # # - Installs SSH keys into container root account if SSH is enabled # - Uses pct push or direct input to authorized_keys +# - Supports both SSH_KEYS_FILE (from advanced settings) and SSH_AUTHORIZED_KEY (from user defaults) # - Falls back to warning if no keys provided # ------------------------------------------------------------------------------ install_ssh_keys_into_ct() { @@ -210,6 +245,13 @@ install_ssh_keys_into_ct() { # Ensure SSH_KEYS_FILE is defined (may not be set if advanced_settings was skipped) : "${SSH_KEYS_FILE:=}" + # If SSH_KEYS_FILE doesn't exist but SSH_AUTHORIZED_KEY is set (from user defaults), + # create a temporary SSH_KEYS_FILE with the key + if [[ -z "$SSH_KEYS_FILE" || ! -s "$SSH_KEYS_FILE" ]] && [[ -n "${SSH_AUTHORIZED_KEY:-}" ]]; then + SSH_KEYS_FILE="$(mktemp)" + printf '%s\n' "$SSH_AUTHORIZED_KEY" >"$SSH_KEYS_FILE" + fi + if [[ -n "$SSH_KEYS_FILE" && -s "$SSH_KEYS_FILE" ]]; then msg_info "Installing selected SSH keys into CT ${CTID}" pct exec "$CTID" -- sh -c 'mkdir -p /root/.ssh && chmod 700 /root/.ssh' || { @@ -231,6 +273,394 @@ install_ssh_keys_into_ct() { return 0 } +# ------------------------------------------------------------------------------ +# validate_container_id() +# +# - Validates if a container ID is available for use +# - Checks if ID is already used by VM or LXC container +# - Checks if ID is used in LVM logical volumes +# - Returns 0 if ID is available, 1 if already in use +# ------------------------------------------------------------------------------ +validate_container_id() { + local ctid="$1" + + # Check if ID is numeric + if ! [[ "$ctid" =~ ^[0-9]+$ ]]; then + return 1 + fi + + # Check if config file exists for VM or LXC + if [[ -f "/etc/pve/qemu-server/${ctid}.conf" ]] || [[ -f "/etc/pve/lxc/${ctid}.conf" ]]; then + return 1 + fi + + # Check if ID is used in LVM logical volumes + if lvs --noheadings -o lv_name 2>/dev/null | grep -qE "(^|[-_])${ctid}($|[-_])"; then + return 1 + fi + + return 0 +} + +# ------------------------------------------------------------------------------ +# get_valid_container_id() +# +# - Returns a valid, unused container ID +# - If provided ID is valid, returns it +# - Otherwise increments from suggested ID until a free one is found +# - Calls validate_container_id() to check availability +# ------------------------------------------------------------------------------ +get_valid_container_id() { + local suggested_id="${1:-$(pvesh get /cluster/nextid)}" + + while ! validate_container_id "$suggested_id"; do + suggested_id=$((suggested_id + 1)) + done + + echo "$suggested_id" +} + +# ------------------------------------------------------------------------------ +# validate_container_id() +# +# - Validates if a container ID is available for use +# - Checks if ID is already used by VM or LXC container +# - Checks if ID is used in LVM logical volumes +# - Returns 0 if ID is available, 1 if already in use +# ------------------------------------------------------------------------------ +validate_container_id() { + local ctid="$1" + + # Check if ID is numeric + if ! [[ "$ctid" =~ ^[0-9]+$ ]]; then + return 1 + fi + + # Check if config file exists for VM or LXC + if [[ -f "/etc/pve/qemu-server/${ctid}.conf" ]] || [[ -f "/etc/pve/lxc/${ctid}.conf" ]]; then + return 1 + fi + + # Check if ID is used in LVM logical volumes + if lvs --noheadings -o lv_name 2>/dev/null | grep -qE "(^|[-_])${ctid}($|[-_])"; then + return 1 + fi + + return 0 +} + +# ------------------------------------------------------------------------------ +# get_valid_container_id() +# +# - Returns a valid, unused container ID +# - If provided ID is valid, returns it +# - Otherwise increments from suggested ID until a free one is found +# - Calls validate_container_id() to check availability +# ------------------------------------------------------------------------------ +get_valid_container_id() { + local suggested_id="${1:-$(pvesh get /cluster/nextid)}" + + while ! validate_container_id "$suggested_id"; do + suggested_id=$((suggested_id + 1)) + done + + echo "$suggested_id" +} + +# ------------------------------------------------------------------------------ +# validate_hostname() +# +# - Validates hostname/FQDN according to RFC 1123/952 +# - Checks total length (max 253 characters for FQDN) +# - Validates each label (max 63 chars, alphanumeric + hyphens) +# - Returns 0 if valid, 1 if invalid +# ------------------------------------------------------------------------------ +validate_hostname() { + local hostname="$1" + + # Check total length (max 253 for FQDN) + if [[ ${#hostname} -gt 253 ]] || [[ -z "$hostname" ]]; then + return 1 + fi + + # Split by dots and validate each label + local IFS='.' + read -ra labels <<< "$hostname" + for label in "${labels[@]}"; do + # Each label: 1-63 chars, alphanumeric, hyphens allowed (not at start/end) + if [[ -z "$label" ]] || [[ ${#label} -gt 63 ]]; then + return 1 + fi + if [[ ! "$label" =~ ^[a-z0-9]([a-z0-9-]*[a-z0-9])?$ ]] && [[ ! "$label" =~ ^[a-z0-9]$ ]]; then + return 1 + fi + done + + return 0 +} + +# ------------------------------------------------------------------------------ +# validate_mac_address() +# +# - Validates MAC address format (XX:XX:XX:XX:XX:XX) +# - Empty value is allowed (auto-generated) +# - Returns 0 if valid, 1 if invalid +# ------------------------------------------------------------------------------ +validate_mac_address() { + local mac="$1" + [[ -z "$mac" ]] && return 0 + if [[ ! "$mac" =~ ^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$ ]]; then + return 1 + fi + return 0 +} + +# ------------------------------------------------------------------------------ +# validate_vlan_tag() +# +# - Validates VLAN tag (1-4094) +# - Empty value is allowed (no VLAN) +# - Returns 0 if valid, 1 if invalid +# ------------------------------------------------------------------------------ +validate_vlan_tag() { + local vlan="$1" + [[ -z "$vlan" ]] && return 0 + if ! [[ "$vlan" =~ ^[0-9]+$ ]] || ((vlan < 1 || vlan > 4094)); then + return 1 + fi + return 0 +} + +# ------------------------------------------------------------------------------ +# validate_mtu() +# +# - Validates MTU size (576-65535, common values: 1500, 9000) +# - Empty value is allowed (default 1500) +# - Returns 0 if valid, 1 if invalid +# ------------------------------------------------------------------------------ +validate_mtu() { + local mtu="$1" + [[ -z "$mtu" ]] && return 0 + if ! [[ "$mtu" =~ ^[0-9]+$ ]] || ((mtu < 576 || mtu > 65535)); then + return 1 + fi + return 0 +} + +# ------------------------------------------------------------------------------ +# validate_ipv6_address() +# +# - Validates IPv6 address with optional CIDR notation +# - Supports compressed (::) and full notation +# - Empty value is allowed +# - Returns 0 if valid, 1 if invalid +# ------------------------------------------------------------------------------ +validate_ipv6_address() { + local ipv6="$1" + [[ -z "$ipv6" ]] && return 0 + + # Extract address and CIDR + local addr="${ipv6%%/*}" + local cidr="${ipv6##*/}" + + # Validate CIDR if present (1-128) + if [[ "$ipv6" == */* ]]; then + if ! [[ "$cidr" =~ ^[0-9]+$ ]] || ((cidr < 1 || cidr > 128)); then + return 1 + fi + fi + + # Basic IPv6 validation - check for valid characters and structure + # Must contain only hex digits and colons + if [[ ! "$addr" =~ ^[0-9a-fA-F:]+$ ]]; then + return 1 + fi + + # Must contain at least one colon + if [[ ! "$addr" == *:* ]]; then + return 1 + fi + + # Check for valid double-colon usage (only one :: allowed) + if [[ "$addr" == *::*::* ]]; then + return 1 + fi + + # Check that no segment exceeds 4 hex chars + local IFS=':' + local -a segments + read -ra segments <<< "$addr" + for seg in "${segments[@]}"; do + if [[ ${#seg} -gt 4 ]]; then + return 1 + fi + done + + return 0 +} + +# ------------------------------------------------------------------------------ +# validate_bridge() +# +# - Validates that network bridge exists and is active +# - Returns 0 if valid, 1 if invalid +# ------------------------------------------------------------------------------ +validate_bridge() { + local bridge="$1" + [[ -z "$bridge" ]] && return 1 + + # Check if bridge interface exists + if ! ip link show "$bridge" &>/dev/null; then + return 1 + fi + + return 0 +} + +# ------------------------------------------------------------------------------ +# validate_gateway_in_subnet() +# +# - Validates that gateway IP is in the same subnet as static IP +# - Arguments: static_ip (with CIDR), gateway_ip +# - Returns 0 if valid, 1 if invalid +# ------------------------------------------------------------------------------ +validate_gateway_in_subnet() { + local static_ip="$1" + local gateway="$2" + + [[ -z "$static_ip" || -z "$gateway" ]] && return 0 + + # Extract IP and CIDR + local ip="${static_ip%%/*}" + local cidr="${static_ip##*/}" + + # Convert CIDR to netmask bits + local mask=$((0xFFFFFFFF << (32 - cidr) & 0xFFFFFFFF)) + + # Convert IPs to integers + local IFS='.' + read -r i1 i2 i3 i4 <<< "$ip" + read -r g1 g2 g3 g4 <<< "$gateway" + + local ip_int=$(( (i1 << 24) + (i2 << 16) + (i3 << 8) + i4 )) + local gw_int=$(( (g1 << 24) + (g2 << 16) + (g3 << 8) + g4 )) + + # Check if both are in same network + if (( (ip_int & mask) != (gw_int & mask) )); then + return 1 + fi + + return 0 +} + +# ------------------------------------------------------------------------------ +# validate_ip_address() +# +# - Validates IPv4 address with CIDR notation +# - Checks each octet is 0-255 +# - Checks CIDR is 1-32 +# - Returns 0 if valid, 1 if invalid +# ------------------------------------------------------------------------------ +validate_ip_address() { + local ip="$1" + [[ -z "$ip" ]] && return 1 + + # Check format with CIDR + if [[ ! "$ip" =~ ^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})/([0-9]{1,2})$ ]]; then + return 1 + fi + + local o1="${BASH_REMATCH[1]}" + local o2="${BASH_REMATCH[2]}" + local o3="${BASH_REMATCH[3]}" + local o4="${BASH_REMATCH[4]}" + local cidr="${BASH_REMATCH[5]}" + + # Validate octets (0-255) + for octet in "$o1" "$o2" "$o3" "$o4"; do + if ((octet > 255)); then + return 1 + fi + done + + # Validate CIDR (1-32) + if ((cidr < 1 || cidr > 32)); then + return 1 + fi + + return 0 +} + +# ------------------------------------------------------------------------------ +# validate_gateway_ip() +# +# - Validates gateway IPv4 address (without CIDR) +# - Checks each octet is 0-255 +# - Returns 0 if valid, 1 if invalid +# ------------------------------------------------------------------------------ +validate_gateway_ip() { + local ip="$1" + [[ -z "$ip" ]] && return 0 + + # Check format without CIDR + if [[ ! "$ip" =~ ^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$ ]]; then + return 1 + fi + + local o1="${BASH_REMATCH[1]}" + local o2="${BASH_REMATCH[2]}" + local o3="${BASH_REMATCH[3]}" + local o4="${BASH_REMATCH[4]}" + + # Validate octets (0-255) + for octet in "$o1" "$o2" "$o3" "$o4"; do + if ((octet > 255)); then + return 1 + fi + done + + return 0 +} + +# ------------------------------------------------------------------------------ +# validate_timezone() +# +# - Validates timezone string against system zoneinfo +# - Empty value or "host" is allowed +# - Returns 0 if valid, 1 if invalid +# ------------------------------------------------------------------------------ +validate_timezone() { + local tz="$1" + [[ -z "$tz" || "$tz" == "host" ]] && return 0 + + # Check if timezone file exists + if [[ ! -f "/usr/share/zoneinfo/$tz" ]]; then + return 1 + fi + + return 0 +} + +# ------------------------------------------------------------------------------ +# validate_tags() +# +# - Validates Proxmox tags format +# - Only alphanumeric, hyphens, underscores, and semicolons allowed +# - Empty value is allowed +# - Returns 0 if valid, 1 if invalid +# ------------------------------------------------------------------------------ +validate_tags() { + local tags="$1" + [[ -z "$tags" ]] && return 0 + + # Tags can only contain alphanumeric, -, _, and ; (separator) + if [[ ! "$tags" =~ ^[a-zA-Z0-9_\;-]+$ ]]; then + return 1 + fi + + return 0 +} + # ------------------------------------------------------------------------------ # find_host_ssh_keys() # @@ -417,6 +847,12 @@ choose_and_set_storage_for_file() { if [[ "$count" -eq 1 ]]; then STORAGE_RESULT=$(pvesm status -content "$content" | awk 'NR>1{print $1; exit}') STORAGE_INFO="" + + # Validate storage space for auto-picked container storage + if [[ "$class" == "container" && -n "${DISK_SIZE:-}" ]]; then + validate_storage_space "$STORAGE_RESULT" "$DISK_SIZE" "yes" + # Continue even if validation fails - user was warned + fi else # If the current value is preselectable, we could show it, but per your requirement we always offer selection select_storage "$class" || return 1 @@ -482,9 +918,45 @@ base_settings() { CORE_COUNT="${final_cpu}" RAM_SIZE="${final_ram}" VERBOSE=${var_verbose:-"${1:-no}"} - PW=${var_pw:-""} - CT_ID=${var_ctid:-$NEXTID} - HN=${var_hostname:-$NSAPP} + PW="" + if [[ -n "${var_pw:-}" ]]; then + local _pw_raw="${var_pw}" + case "$_pw_raw" in + --password\ *) _pw_raw="${_pw_raw#--password }" ;; + -password\ *) _pw_raw="${_pw_raw#-password }" ;; + esac + while [[ "$_pw_raw" == -* ]]; do + _pw_raw="${_pw_raw#-}" + done + if [[ -z "$_pw_raw" ]]; then + msg_warn "Password was only dashes after cleanup; leaving empty." + else + PW="--password $_pw_raw" + fi + fi + + # Validate and set Container ID + local requested_id="${var_ctid:-$NEXTID}" + if ! validate_container_id "$requested_id"; then + # Only show warning if user manually specified an ID (not auto-assigned) + if [[ -n "${var_ctid:-}" ]]; then + msg_warn "Container ID $requested_id is already in use. Using next available ID: $(get_valid_container_id "$requested_id")" + fi + requested_id=$(get_valid_container_id "$requested_id") + fi + CT_ID="$requested_id" + + # Validate and set Hostname/FQDN + local requested_hostname="${var_hostname:-$NSAPP}" + requested_hostname=$(echo "${requested_hostname,,}" | tr -d ' ') + if ! validate_hostname "$requested_hostname"; then + if [[ -n "${var_hostname:-}" ]]; then + msg_warn "Invalid hostname '$requested_hostname'. Using default: $NSAPP" + fi + requested_hostname="$NSAPP" + fi + HN="$requested_hostname" + BRG=${var_brg:-"vmbr0"} NET=${var_net:-"dhcp"} @@ -517,16 +989,11 @@ base_settings() { fi fi - # Format optional network variables with proper prefixes for pct create - # Also strip any spaces from nameserver values (multiple IPs must be comma-separated without spaces) - local _ns_clean="${var_ns:-}" - _ns_clean="${_ns_clean// /}" # Remove all spaces from nameserver value - - [[ -n "${var_mtu:-}" ]] && MTU=",mtu=${var_mtu}" || MTU="" - [[ -n "${var_searchdomain:-}" ]] && SD="-searchdomain=${var_searchdomain}" || SD="" - [[ -n "$_ns_clean" ]] && NS="-nameserver=${_ns_clean}" || NS="" - [[ -n "${var_mac:-}" ]] && MAC=",hwaddr=${var_mac}" || MAC="" - [[ -n "${var_vlan:-}" ]] && VLAN=",tag=${var_vlan}" || VLAN="" + MTU=${var_mtu:-""} + SD=${var_searchdomain:-""} + NS=${var_ns:-""} + MAC=${var_mac:-""} + VLAN=${var_vlan:-""} SSH=${var_ssh:-"no"} SSH_AUTHORIZED_KEY=${var_ssh_authorized_key:-""} UDHCPC_FIX=${var_udhcpc_fix:-""} @@ -609,6 +1076,123 @@ load_vars_file() { # Trim trailing whitespace var_val="${var_val%"${var_val##*[![:space:]]}"}" + # Validate values before setting (skip empty values - they use defaults) + if [[ -n "$var_val" ]]; then + case "$var_key" in + var_mac) + if ! validate_mac_address "$var_val"; then + msg_warn "Invalid MAC address '$var_val' in $file, ignoring" + continue + fi + ;; + var_vlan) + if ! validate_vlan_tag "$var_val"; then + msg_warn "Invalid VLAN tag '$var_val' in $file (must be 1-4094), ignoring" + continue + fi + ;; + var_mtu) + if ! validate_mtu "$var_val"; then + msg_warn "Invalid MTU '$var_val' in $file (must be 576-65535), ignoring" + continue + fi + ;; + var_tags) + if ! validate_tags "$var_val"; then + msg_warn "Invalid tags '$var_val' in $file (alphanumeric, -, _, ; only), ignoring" + continue + fi + ;; + var_timezone) + if ! validate_timezone "$var_val"; then + msg_warn "Invalid timezone '$var_val' in $file, ignoring" + continue + fi + ;; + var_brg) + if ! validate_bridge "$var_val"; then + msg_warn "Bridge '$var_val' not found in $file, ignoring" + continue + fi + ;; + var_gateway) + if ! validate_gateway_ip "$var_val"; then + msg_warn "Invalid gateway IP '$var_val' in $file, ignoring" + continue + fi + ;; + var_hostname) + if ! validate_hostname "$var_val"; then + msg_warn "Invalid hostname '$var_val' in $file, ignoring" + continue + fi + ;; + var_cpu) + if ! [[ "$var_val" =~ ^[0-9]+$ ]] || ((var_val < 1 || var_val > 128)); then + msg_warn "Invalid CPU count '$var_val' in $file (must be 1-128), ignoring" + continue + fi + ;; + var_ram) + if ! [[ "$var_val" =~ ^[0-9]+$ ]] || ((var_val < 256)); then + msg_warn "Invalid RAM '$var_val' in $file (must be >= 256 MiB), ignoring" + continue + fi + ;; + var_disk) + if ! [[ "$var_val" =~ ^[0-9]+$ ]] || ((var_val < 1)); then + msg_warn "Invalid disk size '$var_val' in $file (must be >= 1 GB), ignoring" + continue + fi + ;; + var_unprivileged) + if [[ "$var_val" != "0" && "$var_val" != "1" ]]; then + msg_warn "Invalid unprivileged value '$var_val' in $file (must be 0 or 1), ignoring" + continue + fi + ;; + var_nesting) + if [[ "$var_val" != "0" && "$var_val" != "1" ]]; then + msg_warn "Invalid nesting value '$var_val' in $file (must be 0 or 1), ignoring" + continue + fi + # Warn about potential issues with systemd-based OS when nesting is disabled via vars file + if [[ "$var_val" == "0" && "${var_os:-debian}" != "alpine" ]]; then + msg_warn "Nesting disabled in $file - modern systemd-based distributions may require nesting for proper operation" + fi + ;; + var_keyctl) + if [[ "$var_val" != "0" && "$var_val" != "1" ]]; then + msg_warn "Invalid keyctl value '$var_val' in $file (must be 0 or 1), ignoring" + continue + fi + ;; + var_net) + # var_net can be: dhcp, static IP/CIDR, or IP range + if [[ "$var_val" != "dhcp" ]]; then + if is_ip_range "$var_val"; then + : # IP range is valid, will be resolved at runtime + elif ! validate_ip_address "$var_val"; then + msg_warn "Invalid network '$var_val' in $file (must be dhcp or IP/CIDR), ignoring" + continue + fi + fi + ;; + var_fuse|var_tun|var_gpu|var_ssh|var_verbose|var_protection) + if [[ "$var_val" != "yes" && "$var_val" != "no" ]]; then + msg_warn "Invalid boolean '$var_val' for $var_key in $file (must be yes/no), ignoring" + continue + fi + ;; + var_ipv6_method) + if [[ "$var_val" != "auto" && "$var_val" != "dhcp" && "$var_val" != "static" && "$var_val" != "none" ]]; then + msg_warn "Invalid IPv6 method '$var_val' in $file (must be auto/dhcp/static/none), ignoring" + continue + fi + ;; + esac + fi + # Set variable: force mode overrides existing, otherwise only set if empty if [[ "$force" == "yes" ]]; then export "${var_key}=${var_val}" @@ -1103,6 +1687,13 @@ ensure_storage_selection_for_vars_file() { if [[ -n "$tpl" && -n "$ct" ]]; then TEMPLATE_STORAGE="$tpl" CONTAINER_STORAGE="$ct" + + # Validate storage space for loaded container storage + if [[ -n "${DISK_SIZE:-}" ]]; then + validate_storage_space "$ct" "$DISK_SIZE" "yes" + # Continue even if validation fails - user was warned + fi + return 0 fi @@ -1226,7 +1817,7 @@ advanced_settings() { if [[ -n "$BRIDGES" ]]; then while IFS= read -r bridge; do if [[ -n "$bridge" ]]; then - local description=$(grep -A 10 "iface $bridge" /etc/network/interfaces 2>/dev/null | grep '^#' | head -n1 | sed 's/^#\s*//') + local description=$(grep -A 10 "iface $bridge" /etc/network/interfaces 2>/dev/null | grep '^#' | head -n1 | sed 's/^#\s*//;s/^[- ]*//') BRIDGE_MENU_OPTIONS+=("$bridge" "${description:- }") fi done <<<"$BRIDGES" @@ -1279,17 +1870,30 @@ advanced_settings() { ((STEP++)) elif [[ "$PW1" == *" "* ]]; then whiptail --msgbox "Password cannot contain spaces." 8 58 - elif ((${#PW1} < 5)); then - whiptail --msgbox "Password must be at least 5 characters." 8 58 else + local _pw1_clean="$PW1" + while [[ "$_pw1_clean" == -* ]]; do + _pw1_clean="${_pw1_clean#-}" + done + if [[ -z "$_pw1_clean" ]]; then + whiptail --msgbox "Password cannot be only '-' characters." 8 58 + continue + elif ((${#_pw1_clean} < 5)); then + whiptail --msgbox "Password must be at least 5 characters (after removing leading '-')." 8 70 + continue + fi # Verify password if PW2=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ --title "PASSWORD VERIFICATION" \ --ok-button "Confirm" --cancel-button "Back" \ --passwordbox "\nVerify Root Password" 10 58 \ 3>&1 1>&2 2>&3); then - if [[ "$PW1" == "$PW2" ]]; then - _pw="-password $PW1" + local _pw2_clean="$PW2" + while [[ "$_pw2_clean" == -* ]]; do + _pw2_clean="${_pw2_clean#-}" + done + if [[ "$_pw1_clean" == "$_pw2_clean" ]]; then + _pw="--password $_pw1_clean" _pw_display="********" ((STEP++)) else @@ -1313,7 +1917,25 @@ advanced_settings() { --ok-button "Next" --cancel-button "Back" \ --inputbox "\nSet Container ID" 10 58 "$_ct_id" \ 3>&1 1>&2 2>&3); then - _ct_id="${result:-$NEXTID}" + local input_id="${result:-$NEXTID}" + + # Validate that ID is numeric + if ! [[ "$input_id" =~ ^[0-9]+$ ]]; then + whiptail --backtitle "Proxmox VE Helper Scripts" --title "Invalid ID" --msgbox "Container ID must be numeric." 8 58 + continue + fi + + # Check if ID is already in use + if ! validate_container_id "$input_id"; then + if whiptail --backtitle "Proxmox VE Helper Scripts" --title "ID Already In Use" \ + --yesno "Container/VM ID $input_id is already in use.\n\nWould you like to use the next available ID ($(get_valid_container_id "$input_id"))?" 10 58; then + _ct_id=$(get_valid_container_id "$input_id") + else + continue + fi + else + _ct_id="$input_id" + fi ((STEP++)) else ((STEP--)) @@ -1327,15 +1949,16 @@ advanced_settings() { if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ --title "HOSTNAME" \ --ok-button "Next" --cancel-button "Back" \ - --inputbox "\nSet Hostname (lowercase, alphanumeric, hyphens only)" 10 58 "$_hostname" \ + --inputbox "\nSet Hostname (or FQDN, e.g. host.example.com)" 10 58 "$_hostname" \ 3>&1 1>&2 2>&3); then local hn_test="${result:-$NSAPP}" hn_test=$(echo "${hn_test,,}" | tr -d ' ') - if [[ "$hn_test" =~ ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ ]]; then + + if validate_hostname "$hn_test"; then _hostname="$hn_test" ((STEP++)) else - whiptail --msgbox "Invalid hostname: '$hn_test'\n\nOnly lowercase letters, digits and hyphens are allowed." 10 58 + whiptail --msgbox "Invalid hostname: '$hn_test'\n\nRules:\n- Only lowercase letters, digits, dots and hyphens\n- Labels separated by dots (max 63 chars each)\n- No leading/trailing hyphens or dots\n- No consecutive dots\n- Total max 253 characters" 14 60 fi else ((STEP--)) @@ -1410,8 +2033,14 @@ advanced_settings() { # ═══════════════════════════════════════════════════════════════════════════ 8) if [[ ${#BRIDGE_MENU_OPTIONS[@]} -eq 0 ]]; then - _bridge="vmbr0" - ((STEP++)) + # Validate default bridge exists + if validate_bridge "vmbr0"; then + _bridge="vmbr0" + ((STEP++)) + else + whiptail --msgbox "Default bridge 'vmbr0' not found!\n\nPlease configure a network bridge in Proxmox first." 10 58 + exit 1 + fi else if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ --title "NETWORK BRIDGE" \ @@ -1419,8 +2048,17 @@ advanced_settings() { --menu "\nSelect network bridge:" 16 58 6 \ "${BRIDGE_MENU_OPTIONS[@]}" \ 3>&1 1>&2 2>&3); then - _bridge="${result:-vmbr0}" - ((STEP++)) + local bridge_test="${result:-vmbr0}" + # Skip separator entries (e.g., __other__) - re-display menu + if [[ "$bridge_test" == "__other__" || "$bridge_test" == -* ]]; then + continue + fi + if validate_bridge "$bridge_test"; then + _bridge="$bridge_test" + ((STEP++)) + else + whiptail --msgbox "Bridge '$bridge_test' is not available or not active." 8 58 + fi else ((STEP--)) fi @@ -1448,7 +2086,7 @@ advanced_settings() { --ok-button "Next" --cancel-button "Back" \ --inputbox "\nEnter Static IPv4 CIDR Address\n(e.g. 192.168.1.100/24)" 12 58 "" \ 3>&1 1>&2 2>&3); then - if [[ "$static_ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}/([0-9]|[1-2][0-9]|3[0-2])$ ]]; then + if validate_ip_address "$static_ip"; then # Get gateway local gateway_ip if gateway_ip=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ @@ -1456,16 +2094,21 @@ advanced_settings() { --ok-button "Next" --cancel-button "Back" \ --inputbox "\nEnter Gateway IP address" 10 58 "" \ 3>&1 1>&2 2>&3); then - if [[ "$gateway_ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then - _net="$static_ip" - _gate=",gw=$gateway_ip" - ((STEP++)) + if validate_gateway_ip "$gateway_ip"; then + # Validate gateway is in same subnet + if validate_gateway_in_subnet "$static_ip" "$gateway_ip"; then + _net="$static_ip" + _gate=",gw=$gateway_ip" + ((STEP++)) + else + whiptail --msgbox "Gateway is not in the same subnet as the static IP.\n\nStatic IP: $static_ip\nGateway: $gateway_ip" 10 58 + fi else - whiptail --msgbox "Invalid Gateway IP format." 8 58 + whiptail --msgbox "Invalid Gateway IP format.\n\nEach octet must be 0-255.\nExample: 192.168.1.1" 10 58 fi fi else - whiptail --msgbox "Invalid IPv4 CIDR format.\nExample: 192.168.1.100/24" 8 58 + whiptail --msgbox "Invalid IPv4 CIDR format.\n\nEach octet must be 0-255.\nCIDR must be 1-32.\nExample: 192.168.1.100/24" 12 58 fi fi elif [[ "$result" == "range" ]]; then @@ -1489,12 +2132,17 @@ advanced_settings() { --ok-button "Next" --cancel-button "Back" \ --inputbox "\nFound free IP: $NET_RESOLVED\n\nEnter Gateway IP address" 12 58 "" \ 3>&1 1>&2 2>&3); then - if [[ "$gateway_ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then - _net="$NET_RESOLVED" - _gate=",gw=$gateway_ip" - ((STEP++)) + if validate_gateway_ip "$gateway_ip"; then + # Validate gateway is in same subnet + if validate_gateway_in_subnet "$NET_RESOLVED" "$gateway_ip"; then + _net="$NET_RESOLVED" + _gate=",gw=$gateway_ip" + ((STEP++)) + else + whiptail --msgbox "Gateway is not in the same subnet as the IP.\n\nIP: $NET_RESOLVED\nGateway: $gateway_ip" 10 58 + fi else - whiptail --msgbox "Invalid Gateway IP format." 8 58 + whiptail --msgbox "Invalid Gateway IP format.\n\nEach octet must be 0-255.\nExample: 192.168.1.1" 10 58 fi fi else @@ -1537,16 +2185,33 @@ advanced_settings() { --title "STATIC IPv6 ADDRESS" \ --inputbox "\nEnter IPv6 CIDR address\n(e.g. 2001:db8::1/64)" 12 58 "" \ 3>&1 1>&2 2>&3); then - if [[ "$ipv6_addr" =~ ^([0-9a-fA-F:]+:+)+[0-9a-fA-F]+(/[0-9]{1,3})$ ]]; then + if validate_ipv6_address "$ipv6_addr"; then _ipv6_addr="$ipv6_addr" - # Optional gateway - _ipv6_gate=$(whiptail --backtitle "Proxmox VE Helper Scripts" \ - --title "IPv6 GATEWAY" \ - --inputbox "\nEnter IPv6 gateway (optional, leave blank for none)" 10 58 "" \ - 3>&1 1>&2 2>&3) || true - ((STEP++)) + # Optional gateway - loop until valid or empty + local ipv6_gw_valid=false + while [[ "$ipv6_gw_valid" == "false" ]]; do + local ipv6_gw + ipv6_gw=$(whiptail --backtitle "Proxmox VE Helper Scripts" \ + --title "IPv6 GATEWAY" \ + --inputbox "\nEnter IPv6 gateway (optional, leave blank for none)" 10 58 "" \ + 3>&1 1>&2 2>&3) || true + # Validate gateway if provided + if [[ -n "$ipv6_gw" ]]; then + if validate_ipv6_address "$ipv6_gw"; then + _ipv6_gate="$ipv6_gw" + ipv6_gw_valid=true + ((STEP++)) + else + whiptail --msgbox "Invalid IPv6 gateway format.\n\nExample: 2001:db8::1" 8 58 + fi + else + _ipv6_gate="" + ipv6_gw_valid=true + ((STEP++)) + fi + done else - whiptail --msgbox "Invalid IPv6 CIDR format." 8 58 + whiptail --msgbox "Invalid IPv6 CIDR format.\n\nExample: 2001:db8::1/64\nCIDR must be 1-128." 10 58 fi fi ;; @@ -1579,10 +2244,14 @@ advanced_settings() { if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ --title "MTU SIZE" \ --ok-button "Next" --cancel-button "Back" \ - --inputbox "\nSet Interface MTU Size\n(leave blank for default 1500)" 12 58 "" \ + --inputbox "\nSet Interface MTU Size\n(leave blank for default 1500, common values: 1500, 9000)" 12 62 "" \ 3>&1 1>&2 2>&3); then - _mtu="$result" - ((STEP++)) + if validate_mtu "$result"; then + _mtu="$result" + ((STEP++)) + else + whiptail --msgbox "Invalid MTU size.\n\nMTU must be between 576 and 65535.\nCommon values: 1500 (default), 9000 (jumbo frames)" 10 58 + fi else ((STEP--)) fi @@ -1627,10 +2296,14 @@ advanced_settings() { if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ --title "MAC ADDRESS" \ --ok-button "Next" --cancel-button "Back" \ - --inputbox "\nSet MAC Address\n(leave blank for auto-generated)" 12 58 "" \ + --inputbox "\nSet MAC Address\n(leave blank for auto-generated, format: XX:XX:XX:XX:XX:XX)" 12 62 "" \ 3>&1 1>&2 2>&3); then - _mac="$result" - ((STEP++)) + if validate_mac_address "$result"; then + _mac="$result" + ((STEP++)) + else + whiptail --msgbox "Invalid MAC address format.\n\nRequired format: XX:XX:XX:XX:XX:XX\nExample: 02:00:00:00:00:01" 10 58 + fi else ((STEP--)) fi @@ -1643,10 +2316,14 @@ advanced_settings() { if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ --title "VLAN TAG" \ --ok-button "Next" --cancel-button "Back" \ - --inputbox "\nSet VLAN Tag\n(leave blank for no VLAN)" 12 58 "" \ + --inputbox "\nSet VLAN Tag (1-4094)\n(leave blank for no VLAN)" 12 58 "" \ 3>&1 1>&2 2>&3); then - _vlan="$result" - ((STEP++)) + if validate_vlan_tag "$result"; then + _vlan="$result" + ((STEP++)) + else + whiptail --msgbox "Invalid VLAN tag.\n\nVLAN must be a number between 1 and 4094." 8 58 + fi else ((STEP--)) fi @@ -1659,11 +2336,16 @@ advanced_settings() { if result=$(whiptail --backtitle "Proxmox VE Helper Scripts [Step $STEP/$MAX_STEP]" \ --title "CONTAINER TAGS" \ --ok-button "Next" --cancel-button "Back" \ - --inputbox "\nSet Custom Tags (semicolon-separated)\n(remove all for no tags)" 12 58 "$_tags" \ + --inputbox "\nSet Custom Tags (semicolon-separated)\n(alphanumeric, hyphens, underscores only)" 12 58 "$_tags" \ 3>&1 1>&2 2>&3); then - _tags="${result:-;}" - _tags=$(echo "$_tags" | tr -d '[:space:]') - ((STEP++)) + local tags_test="${result:-}" + tags_test=$(echo "$tags_test" | tr -d '[:space:]') + if validate_tags "$tags_test"; then + _tags="$tags_test" + ((STEP++)) + else + whiptail --msgbox "Invalid tag format.\n\nTags can only contain:\n- Letters (a-z, A-Z)\n- Numbers (0-9)\n- Hyphens (-)\n- Underscores (_)\n- Semicolons (;) as separator" 14 58 + fi else ((STEP--)) fi @@ -1742,6 +2424,12 @@ advanced_settings() { else if [ $? -eq 1 ]; then _enable_nesting="0" + # Warn about potential issues with systemd-based OS when nesting is disabled + if [[ "$var_os" != "alpine" ]]; then + whiptail --backtitle "Proxmox VE Helper Scripts" \ + --title "⚠️ NESTING WARNING" \ + --msgbox "Modern systemd-based distributions (Debian 13+, Ubuntu 24.04+, etc.) may require nesting to be enabled for proper operation.\n\nWithout nesting, the container may start in a degraded state with failing services (error 243/CREDENTIALS).\n\nIf you experience issues, enable nesting in the container options." 14 68 + fi else ((STEP--)) continue @@ -1842,9 +2530,14 @@ advanced_settings() { --ok-button "Next" --cancel-button "Back" \ --inputbox "\nSet container timezone.\n\nExamples: Europe/Berlin, America/New_York, Asia/Tokyo\n\nHost timezone: ${_host_timezone:-unknown}\n\nLeave empty to inherit from host." 16 62 "$_ct_timezone" \ 3>&1 1>&2 2>&3); then - _ct_timezone="$result" - [[ "${_ct_timezone:-}" == Etc/* ]] && _ct_timezone="host" # pct doesn't accept Etc/* zones - ((STEP++)) + local tz_test="$result" + [[ "${tz_test:-}" == Etc/* ]] && tz_test="host" # pct doesn't accept Etc/* zones + if validate_timezone "$tz_test"; then + _ct_timezone="$tz_test" + ((STEP++)) + else + whiptail --msgbox "Invalid timezone: '$result'\n\nTimezone must exist in /usr/share/zoneinfo/\n\nExamples:\n- Europe/Berlin\n- America/New_York\n- Asia/Tokyo\n- UTC" 14 58 + fi else ((STEP--)) fi @@ -2028,11 +2721,10 @@ Advanced: var_apt_cacher="$_apt_cacher" var_apt_cacher_ip="$_apt_cacher_ip" - # Format optional values (strip spaces from nameserver - multiple IPs must be comma-separated without spaces) - local _ns_clean="${_ns// /}" + # Format optional values [[ -n "$_mtu" ]] && MTU=",mtu=$_mtu" || MTU="" [[ -n "$_sd" ]] && SD="-searchdomain=$_sd" || SD="" - [[ -n "$_ns_clean" ]] && NS="-nameserver=$_ns_clean" || NS="" + [[ -n "$_ns" ]] && NS="-nameserver=$_ns" || NS="" [[ -n "$_mac" ]] && MAC=",hwaddr=$_mac" || MAC="" [[ -n "$_vlan" ]] && VLAN=",tag=$_vlan" || VLAN="" @@ -2298,7 +2990,6 @@ install_script() { 2 | advanced | ADVANCED) header_info echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Install on node $PVEHOST_NAME${CL}" - echo -e "${INFO}${BOLD}${DGN}PVE Version ${PVEVERSION} (Kernel: ${KERNEL_VERSION})${CL}" METHOD="advanced" base_settings advanced_settings @@ -2643,7 +3334,9 @@ start() { elif [ ! -z ${PHS_SILENT+x} ] && [[ "${PHS_SILENT}" == "1" ]]; then VERBOSE="no" set_std_mode + ensure_profile_loaded update_script + update_motd_ip cleanup_lxc else CHOICE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "${APP} LXC Update/Setting" --menu \ @@ -2668,7 +3361,9 @@ start() { exit ;; esac + ensure_profile_loaded update_script + update_motd_ip cleanup_lxc fi } @@ -2805,8 +3500,13 @@ build_container() { export DEV_MODE_DRYRUN="${DEV_MODE_DRYRUN:-false}" # Build PCT_OPTIONS as multi-line string - PCT_OPTIONS_STRING=" -hostname $HN + PCT_OPTIONS_STRING=" -hostname $HN" + + # Only add -tags if TAGS is not empty + if [ -n "$TAGS" ]; then + PCT_OPTIONS_STRING="$PCT_OPTIONS_STRING -tags $TAGS" + fi # Only add -features if FEATURES is not empty if [ -n "$FEATURES" ]; then @@ -2859,6 +3559,21 @@ $PCT_OPTIONS_STRING" export TEMPLATE_STORAGE="${var_template_storage:-}" export CONTAINER_STORAGE="${var_container_storage:-}" + # Validate storage space only if CONTAINER_STORAGE is already set + # (Storage selection happens in create_lxc_container for some modes) + if [[ -n "$CONTAINER_STORAGE" ]]; then + msg_info "Validating storage space" + if ! validate_storage_space "$CONTAINER_STORAGE" "$DISK_SIZE" "no"; then + local free_space + free_space=$(pvesm status 2>/dev/null | awk -v s="$CONTAINER_STORAGE" '$1 == s { print $6 }') + local free_fmt + free_fmt=$(numfmt --to=iec --from-unit=1024 --suffix=B --format %.1f "$free_space" 2>/dev/null || echo "${free_space}KB") + msg_error "Not enough space on '$CONTAINER_STORAGE'. Required: ${DISK_SIZE}GB, Available: ${free_fmt}" + exit 214 + fi + msg_ok "Storage space validated" + fi + create_lxc_container || exit $? LXC_CONFIG="/etc/pve/lxc/${CTID}.conf" @@ -3167,6 +3882,17 @@ EOF fix_gpu_gids + # Fix Debian 13 LXC template bug where / is owned by nobody:nogroup + # This must be done from the host as unprivileged containers cannot chown / + local rootfs + rootfs=$(pct config "$CTID" | grep -E '^rootfs:' | sed 's/rootfs: //' | cut -d',' -f1) + if [[ -n "$rootfs" ]]; then + local mount_point="/var/lib/lxc/${CTID}/rootfs" + if [[ -d "$mount_point" ]] && [[ "$(stat -c '%U' "$mount_point")" != "root" ]]; then + chown root:root "$mount_point" 2>/dev/null || true + fi + fi + # Continue with standard container setup msg_info "Customizing LXC Container" @@ -3390,9 +4116,9 @@ resolve_storage_preselect() { free="$(awk '{print $6}' <<<"$line")" local total_h used_h free_h if command -v numfmt >/dev/null 2>&1; then - total_h="$(numfmt --to=iec --suffix=B --format %.1f "$total" 2>/dev/null || echo "$total")" - used_h="$(numfmt --to=iec --suffix=B --format %.1f "$used" 2>/dev/null || echo "$used")" - free_h="$(numfmt --to=iec --suffix=B --format %.1f "$free" 2>/dev/null || echo "$free")" + total_h="$(numfmt --to=iec --from-unit=1024 --suffix=B --format %.1f "$total" 2>/dev/null || echo "$total")" + used_h="$(numfmt --to=iec --from-unit=1024 --suffix=B --format %.1f "$used" 2>/dev/null || echo "$used")" + free_h="$(numfmt --to=iec --from-unit=1024 --suffix=B --format %.1f "$free" 2>/dev/null || echo "$free")" STORAGE_INFO="Free: ${free_h} Used: ${used_h}" else STORAGE_INFO="Free: ${free} Used: ${used}" @@ -3508,8 +4234,8 @@ select_storage() { 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 USED_FMT=$(numfmt --to=iec --from-unit=1024 --format %.1f <<<"$USED") + local FREE_FMT=$(numfmt --to=iec --from-unit=1024 --format %.1f <<<"$FREE") local INFO="Free: ${FREE_FMT}B Used: ${USED_FMT}B" STORAGE_MAP["$DISPLAY"]="$TAG" MENU+=("$DISPLAY" "$INFO" "OFF") @@ -3547,10 +4273,87 @@ select_storage() { break fi done + + # Validate storage space for container storage + if [[ "$CLASS" == "container" && -n "${DISK_SIZE:-}" ]]; then + validate_storage_space "$STORAGE_RESULT" "$DISK_SIZE" "yes" + # Continue even if validation fails - user was warned + fi + return 0 done } +# ------------------------------------------------------------------------------ +# validate_storage_space() +# +# - Validates if storage has enough free space for container +# - Takes storage name and required size in GB +# - Returns 0 if enough space, 1 if not enough, 2 if storage unavailable +# - Can optionally show whiptail warning +# - Handles all storage types: dir, lvm, lvmthin, zfs, nfs, cifs, etc. +# ------------------------------------------------------------------------------ +validate_storage_space() { + local storage="$1" + local required_gb="${2:-8}" + local show_dialog="${3:-no}" + + # Get full storage line from pvesm status + local storage_line + storage_line=$(pvesm status 2>/dev/null | awk -v s="$storage" '$1 == s {print $0}') + + # Check if storage exists and is active + if [[ -z "$storage_line" ]]; then + [[ "$show_dialog" == "yes" ]] && whiptail --msgbox "⚠️ Warning: Storage '$storage' not found!\n\nThe storage may be unavailable or disabled." 10 60 + return 2 + fi + + # Check storage status (column 3) + local status + status=$(awk '{print $3}' <<<"$storage_line") + if [[ "$status" == "disabled" ]]; then + [[ "$show_dialog" == "yes" ]] && whiptail --msgbox "⚠️ Warning: Storage '$storage' is disabled!\n\nPlease enable the storage first." 10 60 + return 2 + fi + + # Get storage type and free space (column 6) + local storage_type storage_free + storage_type=$(awk '{print $2}' <<<"$storage_line") + storage_free=$(awk '{print $6}' <<<"$storage_line") + + # Some storage types (like PBS, iSCSI) don't report size info + # In these cases, skip space validation + if [[ -z "$storage_free" || "$storage_free" == "0" ]]; then + # Silent pass for storages without size info + return 0 + fi + + local required_kb=$((required_gb * 1024 * 1024)) + local free_gb_fmt + free_gb_fmt=$(numfmt --to=iec --from-unit=1024 --suffix=B --format %.1f "$storage_free" 2>/dev/null || echo "${storage_free}KB") + + if [[ "$storage_free" -lt "$required_kb" ]]; then + if [[ "$show_dialog" == "yes" ]]; then + whiptail --msgbox "⚠️ Warning: Storage '$storage' may not have enough space!\n\nStorage Type: ${storage_type}\nRequired: ${required_gb}GB\nAvailable: ${free_gb_fmt}\n\nYou can continue, but creation might fail." 14 70 + fi + return 1 + fi + + return 0 +} + +# ============================================================================== +# SECTION 8: CONTAINER CREATION +# ============================================================================== + +# ------------------------------------------------------------------------------ +# create_lxc_container() +# +# - Main function for creating LXC containers +# - Handles all phases: validation, template discovery, container creation, +# network config, storage, etc. +# - Extensive error checking with detailed exit codes +# ------------------------------------------------------------------------------ create_lxc_container() { # ------------------------------------------------------------------------------ # Optional verbose mode (debug tracing) @@ -3722,14 +4525,6 @@ create_lxc_container() { fi msg_ok "Template storage '$TEMPLATE_STORAGE' validated" - # Free space check - STORAGE_FREE=$(pvesm status | awk -v s="$CONTAINER_STORAGE" '$1 == s { print $6 }') - REQUIRED_KB=$((${PCT_DISK_SIZE:-8} * 1024 * 1024)) - [[ "$STORAGE_FREE" -ge "$REQUIRED_KB" ]] || { - msg_error "Not enough space on '$CONTAINER_STORAGE'. Needed: ${PCT_DISK_SIZE:-8}G." - exit 214 - } - # Cluster quorum (if cluster) if [[ -f /etc/pve/corosync.conf ]]; then msg_info "Checking cluster quorum" @@ -3752,37 +4547,42 @@ create_lxc_container() { msg_info "Searching for template '$TEMPLATE_SEARCH'" + # Initialize variables + ONLINE_TEMPLATE="" + ONLINE_TEMPLATES=() + + # Step 1: Check local templates first (instant) mapfile -t LOCAL_TEMPLATES < <( pveam list "$TEMPLATE_STORAGE" 2>/dev/null | awk -v search="${TEMPLATE_SEARCH}" -v pattern="${TEMPLATE_PATTERN}" '$1 ~ search && $1 ~ pattern {print $1}' | sed 's|.*/||' | sort -t - -k 2 -V ) - pveam update >/dev/null 2>&1 || msg_warn "Could not update template catalog (pveam update failed)." - - msg_ok "Template search completed" - - set +u - mapfile -t ONLINE_TEMPLATES < <(pveam available -section system 2>/dev/null | grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | awk '{print $2}' | grep -E "^${TEMPLATE_SEARCH}.*${TEMPLATE_PATTERN}" | sort -t - -k 2 -V 2>/dev/null || true) - set -u - - ONLINE_TEMPLATE="" - [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]] && ONLINE_TEMPLATE="${ONLINE_TEMPLATES[-1]}" - - if [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]]; then - count=0 - for idx in "${!ONLINE_TEMPLATES[@]}"; do - ((count++)) - [[ $count -ge 3 ]] && break - done - fi - + # Step 2: If local template found, use it immediately (skip pveam update) if [[ ${#LOCAL_TEMPLATES[@]} -gt 0 ]]; then TEMPLATE="${LOCAL_TEMPLATES[-1]}" TEMPLATE_SOURCE="local" + msg_ok "Template search completed" else + # Step 3: No local template - need to check online (this may be slow) + msg_info "No local template found, checking online catalog..." + + # Update catalog with timeout to prevent long hangs + if command -v timeout &>/dev/null; then + if ! timeout 30 pveam update >/dev/null 2>&1; then + msg_warn "Template catalog update timed out (possible network/DNS issue). Run 'pveam update' manually to diagnose." + fi + else + pveam update >/dev/null 2>&1 || msg_warn "Could not update template catalog (pveam update failed)" + fi + + ONLINE_TEMPLATES=() + mapfile -t ONLINE_TEMPLATES < <(pveam available -section system 2>/dev/null | grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | awk '{print $2}' | grep -E "^${TEMPLATE_SEARCH}.*${TEMPLATE_PATTERN}" | sort -t - -k 2 -V 2>/dev/null || true) + [[ ${#ONLINE_TEMPLATES[@]} -gt 0 ]] && ONLINE_TEMPLATE="${ONLINE_TEMPLATES[-1]}" + TEMPLATE="$ONLINE_TEMPLATE" TEMPLATE_SOURCE="online" + msg_ok "Template search completed" fi # If still no template, try to find alternatives @@ -3791,6 +4591,7 @@ create_lxc_container() { echo "[DEBUG] No template found for ${PCT_OSTYPE} ${PCT_OSVERSION}, searching for alternatives..." # Get all available versions for this OS type + AVAILABLE_VERSIONS=() mapfile -t AVAILABLE_VERSIONS < <( pveam available -section system 2>/dev/null | grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | @@ -3813,6 +4614,7 @@ create_lxc_container() { PCT_OSVERSION="${AVAILABLE_VERSIONS[$((choice - 1))]}" TEMPLATE_SEARCH="${PCT_OSTYPE}-${PCT_OSVERSION}" + ONLINE_TEMPLATES=() mapfile -t ONLINE_TEMPLATES < <( pveam available -section system 2>/dev/null | grep -E '\.(tar\.zst|tar\.xz|tar\.gz)$' | @@ -4022,50 +4824,88 @@ create_lxc_container() { -rootfs $CONTAINER_STORAGE:${PCT_DISK_SIZE:-8}" fi - # Lock by template file (avoid concurrent downloads/creates) + # Lock by template file (avoid concurrent template downloads/validation) lockfile="/tmp/template.${TEMPLATE}.lock" + + # Cleanup stale lock files (older than 1 hour - likely from crashed processes) + if [[ -f "$lockfile" ]]; then + local lock_age=$(($(date +%s) - $(stat -c %Y "$lockfile" 2>/dev/null || echo 0))) + if [[ $lock_age -gt 3600 ]]; then + msg_warn "Removing stale template lock file (age: ${lock_age}s)" + rm -f "$lockfile" + fi + fi + exec 9>"$lockfile" || { msg_error "Failed to create lock file '$lockfile'." exit 200 } - flock -w 60 9 || { - msg_error "Timeout while waiting for template lock." - exit 211 - } + + # Retry logic for template lock (another container creation may be running) + local lock_attempts=0 + local max_lock_attempts=10 + local lock_wait_time=30 + + while ! flock -w "$lock_wait_time" 9; do + lock_attempts=$((lock_attempts + 1)) + if [[ $lock_attempts -ge $max_lock_attempts ]]; then + msg_error "Timeout while waiting for template lock after ${max_lock_attempts} attempts." + msg_custom "💡" "${YW}" "Another container creation may be stuck. Check running processes or remove: $lockfile" + exit 211 + fi + msg_custom "⏳" "${YW}" "Another container is being created with this template. Waiting... (attempt ${lock_attempts}/${max_lock_attempts})" + done LOGFILE="/tmp/pct_create_${CTID}_$(date +%Y%m%d_%H%M%S)_${SESSION_ID}.log" + # Validate template before pct create (while holding lock) + if [[ ! -s "$TEMPLATE_PATH" || "$(stat -c%s "$TEMPLATE_PATH" 2>/dev/null || echo 0)" -lt 1000000 ]]; then + msg_info "Template file missing or too small – downloading" + rm -f "$TEMPLATE_PATH" + pveam download "$TEMPLATE_STORAGE" "$TEMPLATE" >/dev/null 2>&1 + msg_ok "Template downloaded" + elif ! tar -tf "$TEMPLATE_PATH" &>/dev/null; then + if [[ -n "$ONLINE_TEMPLATE" ]]; then + msg_info "Template appears corrupted – re-downloading" + rm -f "$TEMPLATE_PATH" + pveam download "$TEMPLATE_STORAGE" "$TEMPLATE" >/dev/null 2>&1 + msg_ok "Template re-downloaded" + else + msg_warn "Template appears corrupted, but no online version exists. Skipping re-download." + fi + fi + + # Release lock after template validation - pct create has its own internal locking + exec 9>&- + msg_debug "pct create command: pct create $CTID ${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE} $PCT_OPTIONS" msg_debug "Logfile: $LOGFILE" # First attempt (PCT_OPTIONS is a multi-line string, use it directly) if ! pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" $PCT_OPTIONS >"$LOGFILE" 2>&1; then - msg_debug "Container creation failed on ${TEMPLATE_STORAGE}. Validating template..." + msg_debug "Container creation failed on ${TEMPLATE_STORAGE}. Checking error..." - # Validate template file - if [[ ! -s "$TEMPLATE_PATH" || "$(stat -c%s "$TEMPLATE_PATH")" -lt 1000000 ]]; then - msg_warn "Template file too small or missing – re-downloading." + # Check if template issue - retry with fresh download + if grep -qiE 'unable to open|corrupt|invalid' "$LOGFILE"; then + msg_info "Template may be corrupted – re-downloading" rm -f "$TEMPLATE_PATH" - pveam download "$TEMPLATE_STORAGE" "$TEMPLATE" - elif ! tar -tf "$TEMPLATE_PATH" &>/dev/null; then - if [[ -n "$ONLINE_TEMPLATE" ]]; then - msg_warn "Template appears corrupted – re-downloading." - rm -f "$TEMPLATE_PATH" - pveam download "$TEMPLATE_STORAGE" "$TEMPLATE" - else - msg_warn "Template appears corrupted, but no online version exists. Skipping re-download." - fi + pveam download "$TEMPLATE_STORAGE" "$TEMPLATE" >/dev/null 2>&1 + msg_ok "Template re-downloaded" fi # Retry after repair if ! pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" $PCT_OPTIONS >>"$LOGFILE" 2>&1; then # Fallback to local storage if not already on local if [[ "$TEMPLATE_STORAGE" != "local" ]]; then - msg_info "Retrying container creation with fallback to local storage..." + msg_info "Retrying container creation with fallback to local storage" LOCAL_TEMPLATE_PATH="/var/lib/vz/template/cache/$TEMPLATE" if [[ ! -f "$LOCAL_TEMPLATE_PATH" ]]; then - msg_info "Downloading template to local..." + msg_ok "Trying local storage fallback" + msg_info "Downloading template to local" pveam download local "$TEMPLATE" >/dev/null 2>&1 + msg_ok "Template downloaded to local" + else + msg_ok "Trying local storage fallback" fi if ! pct create "$CTID" "local:vztmpl/${TEMPLATE}" $PCT_OPTIONS >>"$LOGFILE" 2>&1; then # Local fallback also failed - check for LXC stack version issue @@ -4236,4 +5076,4 @@ if command -v pveversion >/dev/null 2>&1; then 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 +trap 'post_update_to_api "failed" "TERMINATED"' SIGTERM \ No newline at end of file diff --git a/scripts/core/core.func b/scripts/core/core.func index a241c1a..c9d8d31 100644 --- a/scripts/core/core.func +++ b/scripts/core/core.func @@ -127,6 +127,34 @@ icons() { HOURGLASS="${TAB}⏳${TAB}" } +# ------------------------------------------------------------------------------ +# ensure_profile_loaded() +# +# - Sources /etc/profile.d/*.sh scripts if not already loaded +# - Fixes PATH issues when running via pct enter/exec (non-login shells) +# - Safe to call multiple times (uses guard variable) +# - Should be called in update_script() or any script running inside LXC +# ------------------------------------------------------------------------------ +ensure_profile_loaded() { + # Skip if already loaded or running on Proxmox host + [[ -n "${_PROFILE_LOADED:-}" ]] && return + command -v pveversion &>/dev/null && return + + # Source all profile.d scripts to ensure PATH is complete + if [[ -d /etc/profile.d ]]; then + for script in /etc/profile.d/*.sh; do + [[ -r "$script" ]] && source "$script" + done + fi + + # Also ensure /usr/local/bin is in PATH (common install location) + if [[ ":$PATH:" != *":/usr/local/bin:"* ]]; then + export PATH="/usr/local/bin:$PATH" + fi + + export _PROFILE_LOADED=1 +} + # ------------------------------------------------------------------------------ # default_vars() # @@ -787,11 +815,9 @@ is_verbose_mode() { # ------------------------------------------------------------------------------ # cleanup_lxc() # -# - Comprehensive cleanup of package managers, caches, and logs -# - Supports Alpine (apk), Debian/Ubuntu (apt), and language package managers -# - Cleans: Python (pip/uv), Node.js (npm/yarn/pnpm), Go, Rust, Ruby, PHP -# - Truncates log files and vacuums systemd journal -# - Run at end of container creation to minimize disk usage +# - Cleans package manager and language caches (safe for installs AND updates) +# - Supports Alpine (apk), Debian/Ubuntu (apt), Python, Node.js, Go, Rust, Ruby, PHP +# - Uses fallback error handling to prevent cleanup failures from breaking installs # ------------------------------------------------------------------------------ cleanup_lxc() { msg_info "Cleaning up" @@ -800,32 +826,52 @@ cleanup_lxc() { $STD apk cache clean || true rm -rf /var/cache/apk/* else - $STD apt -y autoremove || true - $STD apt -y autoclean || true - $STD apt -y clean || true + $STD apt -y autoremove 2>/dev/null || msg_warn "apt autoremove failed (non-critical)" + $STD apt -y autoclean 2>/dev/null || msg_warn "apt autoclean failed (non-critical)" + $STD apt -y clean 2>/dev/null || msg_warn "apt clean failed (non-critical)" fi - # Clear temp artifacts (keep sockets/FIFOs; ignore errors) find /tmp /var/tmp -type f -name 'tmp*' -delete 2>/dev/null || true find /tmp /var/tmp -type f -name 'tempfile*' -delete 2>/dev/null || true - # Node.js npm - directly remove cache directory - # npm cache clean/verify can fail with ENOTEMPTY errors, so we skip them + # Python + if command -v pip &>/dev/null; then + rm -rf /root/.cache/pip 2>/dev/null || true + fi + if command -v uv &>/dev/null; then + rm -rf /root/.cache/uv 2>/dev/null || true + fi + + # Node.js if command -v npm &>/dev/null; then rm -rf /root/.npm/_cacache /root/.npm/_logs 2>/dev/null || true fi - # Node.js yarn - if command -v yarn &>/dev/null; then yarn cache clean &>/dev/null || true; fi - # Node.js pnpm - if command -v pnpm &>/dev/null; then pnpm store prune &>/dev/null || true; fi - # Go - if command -v go &>/dev/null; then $STD go clean -cache -modcache || true; fi - # Rust cargo - if command -v cargo &>/dev/null; then $STD cargo clean || true; fi - # Ruby gem - if command -v gem &>/dev/null; then $STD gem cleanup || true; fi - # Composer (PHP) - if command -v composer &>/dev/null; then COMPOSER_ALLOW_SUPERUSER=1 $STD composer clear-cache || true; fi + if command -v yarn &>/dev/null; then + rm -rf /root/.cache/yarn /root/.yarn/cache 2>/dev/null || true + fi + if command -v pnpm &>/dev/null; then + pnpm store prune &>/dev/null || true + fi + + # Go (only build cache, not modules) + if command -v go &>/dev/null; then + $STD go clean -cache 2>/dev/null || true + fi + + # Rust (only registry cache, not build artifacts) + if command -v cargo &>/dev/null; then + rm -rf /root/.cargo/registry/cache /root/.cargo/.package-cache 2>/dev/null || true + fi + + # Ruby + if command -v gem &>/dev/null; then + rm -rf /root/.gem/cache 2>/dev/null || true + fi + + # PHP + if command -v composer &>/dev/null; then + rm -rf /root/.composer/cache 2>/dev/null || true + fi msg_ok "Cleaned" } @@ -878,8 +924,95 @@ check_or_create_swap() { fi } +# ------------------------------------------------------------------------------ +# Loads LOCAL_IP from persistent store or detects if missing. +# +# Description: +# - Loads from /run/local-ip.env or performs runtime lookup +# ------------------------------------------------------------------------------ + +function get_lxc_ip() { + local IP_FILE="/run/local-ip.env" + if [[ -f "$IP_FILE" ]]; then + # shellcheck disable=SC1090 + source "$IP_FILE" + fi + + if [[ -z "${LOCAL_IP:-}" ]]; then + get_current_ip() { + local ip + + # Try direct interface lookup for eth0 FIRST (most reliable for LXC) - IPv4 + ip=$(ip -4 addr show eth0 2>/dev/null | awk '/inet / {print $2}' | cut -d/ -f1 | head -n1) + if [[ -n "$ip" && "$ip" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "$ip" + return 0 + fi + + # Fallback: Try hostname -I (returns IPv4 first if available) + if command -v hostname >/dev/null 2>&1; then + ip=$(hostname -I 2>/dev/null | awk '{print $1}') + if [[ -n "$ip" && "$ip" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "$ip" + return 0 + fi + fi + + # Try routing table with IPv4 targets + local ipv4_targets=("8.8.8.8" "1.1.1.1" "default") + for target in "${ipv4_targets[@]}"; do + if [[ "$target" == "default" ]]; then + ip=$(ip route get 1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if ($i=="src") print $(i+1)}') + else + ip=$(ip route get "$target" 2>/dev/null | awk '{for(i=1;i<=NF;i++) if ($i=="src") print $(i+1)}') + fi + if [[ -n "$ip" ]]; then + echo "$ip" + return 0 + fi + done + + # IPv6 fallback: Try direct interface lookup for eth0 + ip=$(ip -6 addr show eth0 scope global 2>/dev/null | awk '/inet6 / {print $2}' | cut -d/ -f1 | head -n1) + if [[ -n "$ip" && "$ip" =~ : ]]; then + echo "$ip" + return 0 + fi + + # IPv6 fallback: Try hostname -I for IPv6 + if command -v hostname >/dev/null 2>&1; then + ip=$(hostname -I 2>/dev/null | tr ' ' '\n' | grep -E ':' | head -n1) + if [[ -n "$ip" && "$ip" =~ : ]]; then + echo "$ip" + return 0 + fi + fi + + # IPv6 fallback: Use routing table with IPv6 targets + local ipv6_targets=("2001:4860:4860::8888" "2606:4700:4700::1111") + for target in "${ipv6_targets[@]}"; do + ip=$(ip -6 route get "$target" 2>/dev/null | awk '{for(i=1;i<=NF;i++) if ($i=="src") print $(i+1)}') + if [[ -n "$ip" && "$ip" =~ : ]]; then + echo "$ip" + return 0 + fi + done + + return 1 + } + + LOCAL_IP="$(get_current_ip || true)" + if [[ -z "$LOCAL_IP" ]]; then + msg_error "Could not determine LOCAL_IP" + return 1 + fi + fi + + export LOCAL_IP +} + # ============================================================================== # SIGNAL TRAPS # ============================================================================== -trap 'stop_spinner' EXIT INT TERM +trap 'stop_spinner' EXIT INT TERM \ No newline at end of file diff --git a/scripts/core/install.func b/scripts/core/install.func index 5392605..dce940f 100755 --- a/scripts/core/install.func +++ b/scripts/core/install.func @@ -37,6 +37,9 @@ source "$(dirname "${BASH_SOURCE[0]}")/error-handler.func" load_functions catch_errors +# Get LXC IP address (must be called INSIDE container, after network is up) +get_lxc_ip + # ============================================================================== # SECTION 2: NETWORK & CONNECTIVITY # ============================================================================== @@ -76,6 +79,13 @@ EOF # ------------------------------------------------------------------------------ setting_up_container() { msg_info "Setting up Container OS" + + # Fix Debian 13 LXC template bug where / is owned by nobody + # Only attempt in privileged containers (unprivileged cannot chown /) + if [[ "$(stat -c '%U' /)" != "root" ]]; then + (chown root:root / 2>/dev/null) || true + fi + for ((i = RETRY_NUM; i > 0; i--)); do if [ "$(hostname -I)" != "" ]; then break diff --git a/scripts/core/tools.func b/scripts/core/tools.func index 5440eee..b81d809 100644 --- a/scripts/core/tools.func +++ b/scripts/core/tools.func @@ -184,7 +184,10 @@ install_packages_with_retry() { local retry=0 while [[ $retry -le $max_retries ]]; do - if $STD apt install -y "${packages[@]}" 2>/dev/null; then + if DEBIAN_FRONTEND=noninteractive $STD apt install -y \ + -o Dpkg::Options::="--force-confdef" \ + -o Dpkg::Options::="--force-confold" \ + "${packages[@]}" 2>/dev/null; then return 0 fi @@ -211,7 +214,10 @@ upgrade_packages_with_retry() { local retry=0 while [[ $retry -le $max_retries ]]; do - if $STD apt install --only-upgrade -y "${packages[@]}" 2>/dev/null; then + if DEBIAN_FRONTEND=noninteractive $STD apt install --only-upgrade -y \ + -o Dpkg::Options::="--force-confdef" \ + -o Dpkg::Options::="--force-confold" \ + "${packages[@]}" 2>/dev/null; then return 0 fi @@ -568,7 +574,8 @@ EOF msg_error "Failed to download PHP keyring" return 1 } - dpkg -i /tmp/debsuryorg-archive-keyring.deb >/dev/null 2>&1 || { + # Don't use /dev/null redirection for dpkg as it may use background processes + dpkg -i /tmp/debsuryorg-archive-keyring.deb >>"$(get_active_logfile)" 2>&1 || { msg_error "Failed to install PHP keyring" rm -f /tmp/debsuryorg-archive-keyring.deb return 1 @@ -1838,8 +1845,9 @@ function fetch_and_deploy_gh_release() { } chmod 644 "$tmpdir/$filename" - $STD apt install -y "$tmpdir/$filename" || { - $STD dpkg -i "$tmpdir/$filename" || { + # SYSTEMD_OFFLINE=1 prevents systemd-tmpfiles failures in unprivileged LXC (Debian 13+/systemd 257+) + SYSTEMD_OFFLINE=1 $STD apt install -y "$tmpdir/$filename" || { + SYSTEMD_OFFLINE=1 $STD dpkg -i "$tmpdir/$filename" || { msg_error "Both apt and dpkg installation failed" rm -rf "$tmpdir" return 1 @@ -1894,7 +1902,7 @@ function fetch_and_deploy_gh_release() { rm -rf "$tmpdir" "$unpack_tmp" return 1 } - elif [[ "$filename" == *.tar.* || "$filename" == *.tgz ]]; then + elif [[ "$filename" == *.tar.* || "$filename" == *.tgz || "$filename" == *.txz ]]; then tar --no-same-owner -xf "$tmpdir/$filename" -C "$unpack_tmp" || { msg_error "Failed to extract TAR archive" rm -rf "$tmpdir" "$unpack_tmp" @@ -1998,50 +2006,6 @@ function fetch_and_deploy_gh_release() { rm -rf "$tmpdir" } -# ------------------------------------------------------------------------------ -# Loads LOCAL_IP from persistent store or detects if missing. -# -# Description: -# - Loads from /run/local-ip.env or performs runtime lookup -# ------------------------------------------------------------------------------ - -function import_local_ip() { - local IP_FILE="/run/local-ip.env" - if [[ -f "$IP_FILE" ]]; then - # shellcheck disable=SC1090 - source "$IP_FILE" - fi - - if [[ -z "${LOCAL_IP:-}" ]]; then - get_current_ip() { - local targets=("8.8.8.8" "1.1.1.1" "192.168.1.1" "10.0.0.1" "172.16.0.1" "default") - local ip - - for target in "${targets[@]}"; do - if [[ "$target" == "default" ]]; then - ip=$(ip route get 1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if ($i=="src") print $(i+1)}') - else - ip=$(ip route get "$target" 2>/dev/null | awk '{for(i=1;i<=NF;i++) if ($i=="src") print $(i+1)}') - fi - if [[ -n "$ip" ]]; then - echo "$ip" - return 0 - fi - done - - return 1 - } - - LOCAL_IP="$(get_current_ip || true)" - if [[ -z "$LOCAL_IP" ]]; then - msg_error "Could not determine LOCAL_IP" - return 1 - fi - fi - - export LOCAL_IP -} - # ------------------------------------------------------------------------------ # Installs Adminer (Debian/Ubuntu via APT, Alpine via direct download). # @@ -2669,6 +2633,7 @@ function setup_hwaccel() { # GPU Selection - Let user choose which GPU(s) to configure # ═══════════════════════════════════════════════════════════════════════════ local -a SELECTED_INDICES=() + local install_nvidia_drivers="yes" if [[ $gpu_count -eq 1 ]]; then # Single GPU - auto-select @@ -2677,7 +2642,7 @@ function setup_hwaccel() { else # Multiple GPUs - show selection menu echo "" - msg_info "Multiple GPUs detected:" + msg_custom "⚠" "${YW}" "Multiple GPUs detected:" echo "" for i in "${!GPU_LIST[@]}"; do local type_display="${GPU_TYPES[$i]}" @@ -2730,6 +2695,30 @@ function setup_hwaccel() { fi fi + # Ask whether to install NVIDIA drivers in the container + local nvidia_selected="no" + for idx in "${SELECTED_INDICES[@]}"; do + if [[ "${GPU_TYPES[$idx]}" == "NVIDIA" ]]; then + nvidia_selected="yes" + break + fi + done + + if [[ "$nvidia_selected" == "yes" ]]; then + if [[ -n "${INSTALL_NVIDIA_DRIVERS:-}" ]]; then + install_nvidia_drivers="${INSTALL_NVIDIA_DRIVERS}" + else + echo "" + msg_custom "🎮" "${GN}" "NVIDIA GPU passthrough detected" + local nvidia_reply="" + read -r -t 60 -p "${TAB3}⚙️ Install NVIDIA driver libraries in the container? [Y/n] (auto-yes in 60s): " nvidia_reply || nvidia_reply="" + case "${nvidia_reply,,}" in + n | no) install_nvidia_drivers="no" ;; + *) install_nvidia_drivers="yes" ;; + esac + fi + fi + # ═══════════════════════════════════════════════════════════════════════════ # OS Detection # ═══════════════════════════════════════════════════════════════════════════ @@ -2790,7 +2779,11 @@ function setup_hwaccel() { # NVIDIA GPUs # ───────────────────────────────────────────────────────────────────────── NVIDIA) - _setup_nvidia_gpu "$os_id" "$os_codename" "$os_version" + if [[ "$install_nvidia_drivers" == "yes" ]]; then + _setup_nvidia_gpu "$os_id" "$os_codename" "$os_version" + else + msg_warn "Skipping NVIDIA driver installation (user opted to install manually)" + fi ;; esac done @@ -2920,8 +2913,15 @@ _setup_intel_legacy() { vainfo \ intel-gpu-tools 2>/dev/null || msg_warn "Some Intel legacy packages failed" - # beignet provides OpenCL for older Intel GPUs (if available) - $STD apt -y install beignet-opencl-icd 2>/dev/null || true + # beignet provides OpenCL for older Intel GPUs (Sandy Bridge to Broadwell) + # Note: beignet-opencl-icd was removed in Debian 12+ and Ubuntu 22.04+ + # Check if package is available before attempting installation + if apt-cache show beignet-opencl-icd &>/dev/null; then + $STD apt -y install beignet-opencl-icd 2>/dev/null || msg_warn "beignet-opencl-icd installation failed (optional)" + else + msg_warn "beignet-opencl-icd not available - OpenCL support for legacy Intel GPU limited" + msg_warn "Note: Hardware video encoding/decoding (VA-API) still works without OpenCL" + fi msg_ok "Intel Legacy GPU configured" } @@ -2989,16 +2989,24 @@ _setup_nvidia_gpu() { msg_info "Installing NVIDIA GPU drivers" + # Prevent interactive dialogs (e.g., "Mismatching nvidia kernel module" whiptail) + export DEBIAN_FRONTEND=noninteractive + export NEEDRESTART_MODE=a + # Detect host driver version (passed through via /proc) + # Format varies by driver type: + # Proprietary: "NVRM version: NVIDIA UNIX x86_64 Kernel Module 550.54.14 Thu..." + # Open: "NVRM version: NVIDIA UNIX Open Kernel Module for x86_64 590.48.01 Release..." + # Use regex to extract version number (###.##.## pattern) local nvidia_host_version="" if [[ -f /proc/driver/nvidia/version ]]; then - nvidia_host_version=$(grep "NVRM version:" /proc/driver/nvidia/version 2>/dev/null | awk '{print $8}') + nvidia_host_version=$(grep -oP '\d{3,}\.\d+\.\d+' /proc/driver/nvidia/version 2>/dev/null | head -1) fi if [[ -z "$nvidia_host_version" ]]; then msg_warn "NVIDIA host driver version not found in /proc/driver/nvidia/version" msg_warn "Ensure NVIDIA drivers are installed on host and GPU passthrough is enabled" - $STD apt -y install va-driver-all vainfo 2>/dev/null || true + $STD apt-get -y install va-driver-all vainfo 2>/dev/null || true return 0 fi @@ -3011,53 +3019,115 @@ _setup_nvidia_gpu() { sed -i -E 's/Components: (.*)$/Components: \1 contrib non-free non-free-firmware/g' /etc/apt/sources.list.d/debian.sources 2>/dev/null || true fi fi + $STD apt-get -y update 2>/dev/null || msg_warn "apt update failed - continuing anyway" - # Determine CUDA repository - local cuda_repo="debian12" - case "$os_codename" in - bullseye) cuda_repo="debian11" ;; - bookworm) cuda_repo="debian12" ;; - trixie | sid) cuda_repo="debian12" ;; # Forward compatible - esac + # For Debian 13 Trixie/Sid: Use Debian's own nvidia packages first (better compatibility) + # NVIDIA's CUDA repo targets Debian 12 and may not have amd64 packages for Trixie + if [[ "$os_codename" == "trixie" || "$os_codename" == "sid" ]]; then + msg_info "Debian ${os_codename}: Using Debian's NVIDIA packages" - # Add NVIDIA CUDA repository - if [[ ! -f /usr/share/keyrings/cuda-archive-keyring.gpg ]]; then - msg_info "Adding NVIDIA CUDA repository (${cuda_repo})" - local cuda_keyring - cuda_keyring="$(mktemp)" - if curl -fsSL -o "$cuda_keyring" "https://developer.download.nvidia.com/compute/cuda/repos/${cuda_repo}/x86_64/cuda-keyring_1.1-1_all.deb" 2>/dev/null; then - $STD dpkg -i "$cuda_keyring" 2>/dev/null || true + # Extract major version for flexible matching (580.126.09 -> 580) + local nvidia_major_version="${nvidia_host_version%%.*}" + + # Check what versions are actually available + local available_version="" + available_version=$(apt-cache madison libcuda1 2>/dev/null | awk '{print $3}' | grep -E "^${nvidia_major_version}\." | head -1 || true) + + if [[ -n "$available_version" ]]; then + msg_info "Found available NVIDIA version: ${available_version}" + local nvidia_pkgs="libcuda1=${available_version} libnvcuvid1=${available_version} libnvidia-encode1=${available_version} libnvidia-ml1=${available_version}" + if $STD apt-get -y -o Dpkg::Options::="--force-confold" install --no-install-recommends $nvidia_pkgs 2>/dev/null; then + msg_ok "Installed NVIDIA libraries (${available_version})" + else + msg_warn "Failed to install NVIDIA ${available_version} - trying unversioned" + $STD apt-get -y -o Dpkg::Options::="--force-confold" install --no-install-recommends libcuda1 libnvcuvid1 libnvidia-encode1 libnvidia-ml1 2>/dev/null || true + fi else - msg_warn "Failed to download NVIDIA CUDA keyring" + # No matching major version - try latest available or unversioned + msg_warn "No NVIDIA packages for version ${nvidia_major_version}.x found in repos" + available_version=$(apt-cache madison libcuda1 2>/dev/null | awk '{print $3}' | head -1 || true) + if [[ -n "$available_version" ]]; then + msg_info "Trying latest available: ${available_version} (may cause version mismatch)" + $STD apt-get -y -o Dpkg::Options::="--force-confold" install --no-install-recommends \ + libcuda1="${available_version}" libnvcuvid1="${available_version}" \ + libnvidia-encode1="${available_version}" libnvidia-ml1="${available_version}" 2>/dev/null || + $STD apt-get -y -o Dpkg::Options::="--force-confold" install --no-install-recommends \ + libcuda1 libnvcuvid1 libnvidia-encode1 libnvidia-ml1 2>/dev/null || + msg_warn "NVIDIA library installation failed - GPU compute may not work" + else + msg_warn "No NVIDIA packages available in Debian repos - GPU support disabled" + fi fi - rm -f "$cuda_keyring" - fi + $STD apt-get -y -o Dpkg::Options::="--force-confold" install --no-install-recommends nvidia-smi 2>/dev/null || true - # Pin NVIDIA repo for version matching - cat <<'NVIDIA_PIN' >/etc/apt/preferences.d/nvidia-cuda-pin + else + # Debian 11/12: Use NVIDIA CUDA repository for version matching + local cuda_repo="debian12" + case "$os_codename" in + bullseye) cuda_repo="debian11" ;; + bookworm) cuda_repo="debian12" ;; + esac + + # Add NVIDIA CUDA repository + if [[ ! -f /usr/share/keyrings/cuda-archive-keyring.gpg ]]; then + msg_info "Adding NVIDIA CUDA repository (${cuda_repo})" + local cuda_keyring + cuda_keyring="$(mktemp)" + if curl -fsSL -o "$cuda_keyring" "https://developer.download.nvidia.com/compute/cuda/repos/${cuda_repo}/x86_64/cuda-keyring_1.1-1_all.deb" 2>/dev/null; then + $STD dpkg -i "$cuda_keyring" 2>/dev/null || true + else + msg_warn "Failed to download NVIDIA CUDA keyring" + fi + rm -f "$cuda_keyring" + fi + + # Pin NVIDIA repo for version matching + cat <<'NVIDIA_PIN' >/etc/apt/preferences.d/nvidia-cuda-pin Package: * Pin: origin developer.download.nvidia.com Pin-Priority: 1001 NVIDIA_PIN - $STD apt -y update + $STD apt-get -y update 2>/dev/null || msg_warn "apt update failed - continuing anyway" - # Install version-matched NVIDIA libraries - local nvidia_pkgs="libcuda1=${nvidia_host_version}* libnvcuvid1=${nvidia_host_version}* libnvidia-encode1=${nvidia_host_version}* libnvidia-ml1=${nvidia_host_version}*" + # Extract major version for flexible matching (580.126.09 -> 580) + local nvidia_major_version="${nvidia_host_version%%.*}" - msg_info "Installing NVIDIA libraries (version ${nvidia_host_version})" - if $STD apt -y install --no-install-recommends $nvidia_pkgs 2>/dev/null; then - msg_ok "Installed version-matched NVIDIA libraries" - else - msg_warn "Version-pinned install failed - trying unpinned" - if $STD apt -y install --no-install-recommends libcuda1 libnvcuvid1 libnvidia-encode1 libnvidia-ml1 2>/dev/null; then - msg_warn "Installed NVIDIA libraries (unpinned) - version mismatch may occur" + # Check what versions are actually available in CUDA repo + local available_version="" + available_version=$(apt-cache madison libcuda1 2>/dev/null | awk '{print $3}' | grep -E "^${nvidia_major_version}\." | head -1 || true) + + if [[ -n "$available_version" ]]; then + msg_info "Installing NVIDIA libraries (version ${available_version})" + local nvidia_pkgs="libcuda1=${available_version} libnvcuvid1=${available_version} libnvidia-encode1=${available_version} libnvidia-ml1=${available_version}" + if $STD apt-get -y -o Dpkg::Options::="--force-confold" install --no-install-recommends $nvidia_pkgs 2>/dev/null; then + msg_ok "Installed version-matched NVIDIA libraries" + else + msg_warn "Version-pinned install failed - trying unpinned" + $STD apt-get -y -o Dpkg::Options::="--force-confold" install --no-install-recommends libcuda1 libnvcuvid1 libnvidia-encode1 libnvidia-ml1 2>/dev/null || + msg_warn "NVIDIA library installation failed" + fi else - msg_warn "NVIDIA library installation failed" + msg_warn "No NVIDIA packages for version ${nvidia_major_version}.x in CUDA repo (host: ${nvidia_host_version})" + # Try latest available version + available_version=$(apt-cache madison libcuda1 2>/dev/null | awk '{print $3}' | head -1 || true) + if [[ -n "$available_version" ]]; then + msg_info "Trying latest available: ${available_version} (version mismatch warning)" + if $STD apt-get -y -o Dpkg::Options::="--force-confold" install --no-install-recommends \ + libcuda1="${available_version}" libnvcuvid1="${available_version}" \ + libnvidia-encode1="${available_version}" libnvidia-ml1="${available_version}" 2>/dev/null; then + msg_ok "Installed NVIDIA libraries (${available_version}) - version differs from host" + else + $STD apt-get -y -o Dpkg::Options::="--force-confold" install --no-install-recommends libcuda1 libnvcuvid1 libnvidia-encode1 libnvidia-ml1 2>/dev/null || + msg_warn "NVIDIA library installation failed" + fi + else + msg_warn "No NVIDIA packages available in CUDA repo - GPU support disabled" + fi fi - fi - $STD apt -y install --no-install-recommends nvidia-smi 2>/dev/null || true + $STD apt-get -y -o Dpkg::Options::="--force-confold" install --no-install-recommends nvidia-smi 2>/dev/null || true + fi elif [[ "$os_id" == "ubuntu" ]]; then # Ubuntu versioning @@ -3081,20 +3151,45 @@ NVIDIA_PIN rm -f "$cuda_keyring" fi - $STD apt -y update + $STD apt-get -y update 2>/dev/null || msg_warn "apt update failed - continuing anyway" - # Try version-matched install - local nvidia_pkgs="libcuda1=${nvidia_host_version}* libnvcuvid1=${nvidia_host_version}* libnvidia-encode1=${nvidia_host_version}* libnvidia-ml1=${nvidia_host_version}*" - if $STD apt -y install --no-install-recommends $nvidia_pkgs 2>/dev/null; then - msg_ok "Installed version-matched NVIDIA libraries" + # Extract major version for flexible matching + local nvidia_major_version="${nvidia_host_version%%.*}" + + # Check what versions are available + local available_version="" + available_version=$(apt-cache madison libcuda1 2>/dev/null | awk '{print $3}' | grep -E "^${nvidia_major_version}\." | head -1 || true) + + if [[ -n "$available_version" ]]; then + msg_info "Installing NVIDIA libraries (version ${available_version})" + local nvidia_pkgs="libcuda1=${available_version} libnvcuvid1=${available_version} libnvidia-encode1=${available_version} libnvidia-ml1=${available_version}" + if $STD apt-get -y -o Dpkg::Options::="--force-confold" install --no-install-recommends $nvidia_pkgs 2>/dev/null; then + msg_ok "Installed version-matched NVIDIA libraries" + else + # Fallback to Ubuntu repo packages with versioned nvidia-utils + msg_warn "CUDA repo install failed - trying Ubuntu native packages (nvidia-utils-${nvidia_major_version})" + if $STD apt-get -y -o Dpkg::Options::="--force-confold" install --no-install-recommends \ + libnvidia-decode-${nvidia_major_version} libnvidia-encode-${nvidia_major_version} nvidia-utils-${nvidia_major_version} 2>/dev/null; then + msg_ok "Installed Ubuntu NVIDIA packages (${nvidia_major_version})" + else + msg_warn "NVIDIA driver installation failed - please install manually: apt install nvidia-utils-${nvidia_major_version}" + fi + fi else - # Fallback to Ubuntu repo packages - $STD apt -y install --no-install-recommends libnvidia-decode libnvidia-encode nvidia-utils 2>/dev/null || msg_warn "NVIDIA installation failed" + msg_warn "No NVIDIA packages for version ${nvidia_major_version}.x in CUDA repo" + # Fallback to Ubuntu repo packages with versioned nvidia-utils + msg_info "Trying Ubuntu native packages (nvidia-utils-${nvidia_major_version})" + if $STD apt-get -y -o Dpkg::Options::="--force-confold" install --no-install-recommends \ + libnvidia-decode-${nvidia_major_version} libnvidia-encode-${nvidia_major_version} nvidia-utils-${nvidia_major_version} 2>/dev/null; then + msg_ok "Installed Ubuntu NVIDIA packages (${nvidia_major_version})" + else + msg_warn "NVIDIA driver installation failed - please install manually: apt install nvidia-utils-${nvidia_major_version}" + fi fi fi # VA-API for hybrid setups (Intel + NVIDIA) - $STD apt -y install va-driver-all vainfo 2>/dev/null || true + $STD apt-get -y install va-driver-all vainfo 2>/dev/null || true msg_ok "NVIDIA GPU configured" } @@ -3496,10 +3591,11 @@ IP_FILE="/run/local-ip.env" mkdir -p "$(dirname "$IP_FILE")" get_current_ip() { - local targets=("8.8.8.8" "1.1.1.1" "192.168.1.1" "10.0.0.1" "172.16.0.1" "default") local ip - for target in "${targets[@]}"; do + # Try IPv4 targets first + local ipv4_targets=("8.8.8.8" "1.1.1.1" "192.168.1.1" "10.0.0.1" "172.16.0.1" "default") + for target in "${ipv4_targets[@]}"; do if [[ "$target" == "default" ]]; then ip=$(ip route get 1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if ($i=="src") print $(i+1)}') else @@ -3511,6 +3607,23 @@ get_current_ip() { fi done + # IPv6 fallback: Try direct interface lookup for eth0 + ip=$(ip -6 addr show eth0 scope global 2>/dev/null | awk '/inet6 / {print $2}' | cut -d/ -f1 | head -n1) + if [[ -n "$ip" && "$ip" =~ : ]]; then + echo "$ip" + return 0 + fi + + # IPv6 fallback: Use routing table with IPv6 targets (Google DNS, Cloudflare DNS) + local ipv6_targets=("2001:4860:4860::8888" "2606:4700:4700::1111") + for target in "${ipv6_targets[@]}"; do + ip=$(ip -6 route get "$target" 2>/dev/null | awk '{for(i=1;i<=NF;i++) if ($i=="src") print $(i+1)}') + if [[ -n "$ip" && "$ip" =~ : ]]; then + echo "$ip" + return 0 + fi + done + return 1 } @@ -3549,58 +3662,145 @@ EOF } # ------------------------------------------------------------------------------ -# Installs or updates MariaDB from official repo. +# Installs or updates MariaDB. # # Description: +# - Uses Debian/Ubuntu distribution packages by default (most reliable) +# - Only uses official MariaDB repository when a specific version is requested # - Detects current MariaDB version and replaces it if necessary # - Preserves existing database data -# - Dynamically determines latest GA version if "latest" is given # # Variables: -# MARIADB_VERSION - MariaDB version to install (e.g. 10.11, latest) (default: latest) +# MARIADB_VERSION - MariaDB version to install (optional) +# - Not set or "latest": Uses distribution packages (recommended) +# - Specific version (e.g. "11.4", "12.2"): Uses MariaDB official repo # ------------------------------------------------------------------------------ setup_mariadb() { local MARIADB_VERSION="${MARIADB_VERSION:-latest}" + local USE_DISTRO_PACKAGES=false - # Resolve "latest" to actual version - if [[ "$MARIADB_VERSION" == "latest" ]]; then - if ! curl -fsI --max-time 10 http://mirror.mariadb.org/repo/ >/dev/null 2>&1; then - msg_warn "MariaDB mirror not reachable - trying mariadb_repo_setup fallback" - # Try using official mariadb_repo_setup script as fallback - if curl -fsSL --max-time 15 https://r.mariadb.com/downloads/mariadb_repo_setup 2>/dev/null | bash -s -- --skip-verify >/dev/null 2>&1; then - msg_ok "MariaDB repository configured via mariadb_repo_setup" - # Extract version from configured repo - MARIADB_VERSION=$(grep -oP 'repo/\K[0-9]+\.[0-9]+\.[0-9]+' /etc/apt/sources.list.d/mariadb.list 2>/dev/null | head -n1 || echo "12.2") - else - msg_warn "mariadb_repo_setup failed - using hardcoded fallback version" - MARIADB_VERSION="12.2" - fi - else - MARIADB_VERSION=$(curl -fsSL --max-time 15 http://mirror.mariadb.org/repo/ 2>/dev/null | - grep -Eo '[0-9]+\.[0-9]+\.[0-9]+/' | - grep -vE 'rc/|rolling/' | - sed 's|/||' | - sort -Vr | - head -n1 || echo "") + # Ensure non-interactive mode for all apt operations + export DEBIAN_FRONTEND=noninteractive + export NEEDRESTART_MODE=a + export NEEDRESTART_SUSPEND=1 - if [[ -z "$MARIADB_VERSION" ]]; then - msg_warn "Could not parse latest GA MariaDB version from mirror - trying mariadb_repo_setup" - if curl -fsSL --max-time 15 https://r.mariadb.com/downloads/mariadb_repo_setup 2>/dev/null | bash -s -- --skip-verify >/dev/null 2>&1; then - msg_ok "MariaDB repository configured via mariadb_repo_setup" - MARIADB_VERSION=$(grep -oP 'repo/\K[0-9]+\.[0-9]+\.[0-9]+' /etc/apt/sources.list.d/mariadb.list 2>/dev/null | head -n1 || echo "12.2") - else - msg_warn "mariadb_repo_setup failed - using hardcoded fallback version" - MARIADB_VERSION="12.2" - fi - fi - fi + # Determine installation method: + # - "latest" or empty: Use distribution packages (avoids mirror issues) + # - Specific version: Use MariaDB official repository + if [[ "$MARIADB_VERSION" == "latest" || -z "$MARIADB_VERSION" ]]; then + USE_DISTRO_PACKAGES=true + msg_info "Setup MariaDB (distribution packages)" + else + msg_info "Setup MariaDB $MARIADB_VERSION (official repository)" fi # Get currently installed version local CURRENT_VERSION="" CURRENT_VERSION=$(is_tool_installed "mariadb" 2>/dev/null) || true + # Pre-configure debconf to prevent any interactive prompts during install/upgrade + debconf-set-selections </dev/null | grep -E "Candidate:" | awk '{print $2}' | grep -oP '^\d+:\K\d+\.\d+\.\d+' || echo "") + + if [[ -n "$DISTRO_VERSION" ]]; then + # Compare versions - if current is higher, keep it + local CURRENT_MAJOR DISTRO_MAJOR + CURRENT_MAJOR=$(echo "$CURRENT_VERSION" | awk -F. '{print $1}') + DISTRO_MAJOR=$(echo "$DISTRO_VERSION" | awk -F. '{print $1}') + + if [[ "$CURRENT_MAJOR" -gt "$DISTRO_MAJOR" ]]; then + msg_warn "MariaDB $CURRENT_VERSION is already installed (higher than distro $DISTRO_VERSION)" + msg_warn "Keeping existing installation to preserve data integrity" + msg_warn "To use distribution packages, manually remove MariaDB first" + _setup_mariadb_runtime_dir + cache_installed_version "mariadb" "$CURRENT_VERSION" + msg_ok "Setup MariaDB $CURRENT_VERSION (existing installation kept)" + return 0 + fi + fi + fi + + # Install or upgrade MariaDB from distribution packages + if ! install_packages_with_retry "mariadb-server" "mariadb-client"; then + msg_error "Failed to install MariaDB packages from distribution" + return 1 + fi + + # Get installed version for caching + local INSTALLED_VERSION="" + INSTALLED_VERSION=$(mariadb --version 2>/dev/null | grep -oP '\d+\.\d+\.\d+' | head -n1 || echo "distro") + + # Configure runtime directory and finish + _setup_mariadb_runtime_dir + cache_installed_version "mariadb" "$INSTALLED_VERSION" + msg_ok "Setup MariaDB $INSTALLED_VERSION (distribution packages)" + return 0 + fi + + # ============================================================================ + # OFFICIAL REPOSITORY PATH (only when specific version requested) + # ============================================================================ + + # First, check if there's an old/broken repository that needs cleanup + if [[ -f /etc/apt/sources.list.d/mariadb.sources ]] || [[ -f /etc/apt/sources.list.d/mariadb.list ]]; then + local OLD_REPO_VERSION="" + OLD_REPO_VERSION=$(grep -oP 'repo/\K[0-9]+\.[0-9]+(\.[0-9]+)?' /etc/apt/sources.list.d/mariadb.sources 2>/dev/null || \ + grep -oP 'repo/\K[0-9]+\.[0-9]+(\.[0-9]+)?' /etc/apt/sources.list.d/mariadb.list 2>/dev/null || echo "") + + # Check if old repo points to a different version + if [[ -n "$OLD_REPO_VERSION" ]] && [[ "${OLD_REPO_VERSION%.*}" != "${MARIADB_VERSION%.*}" ]]; then + msg_info "Cleaning up old MariaDB repository (was: $OLD_REPO_VERSION, requested: $MARIADB_VERSION)" + cleanup_old_repo_files "mariadb" + $STD apt update || msg_warn "APT update had issues, continuing..." + fi + fi + # Scenario 1: Already installed at target version - just update packages if [[ -n "$CURRENT_VERSION" && "$CURRENT_VERSION" == "$MARIADB_VERSION" ]]; then msg_info "Update MariaDB $MARIADB_VERSION" @@ -3639,9 +3839,7 @@ setup_mariadb() { remove_old_tool_version "mariadb" fi - # Scenario 3: Fresh install or version change - msg_info "Setup MariaDB $MARIADB_VERSION" - + # Scenario 3: Fresh install or version change with specific version # Prepare repository (cleanup + validation) prepare_repository_setup "mariadb" || { msg_error "Failed to prepare MariaDB repository" @@ -3667,31 +3865,39 @@ setup_mariadb() { return 1 } - # Set debconf selections for all potential versions - local MARIADB_MAJOR_MINOR - MARIADB_MAJOR_MINOR=$(echo "$MARIADB_VERSION" | awk -F. '{print $1"."$2}') - if [[ -n "$MARIADB_MAJOR_MINOR" ]]; then - echo "mariadb-server-$MARIADB_MAJOR_MINOR mariadb-server/feedback boolean false" | debconf-set-selections - fi - # Install packages with retry logic - export DEBIAN_FRONTEND=noninteractive if ! install_packages_with_retry "mariadb-server" "mariadb-client"; then - # Fallback: try without specific version - msg_warn "Failed to install MariaDB packages from upstream repo, trying distro fallback..." + # Fallback: try distribution packages + msg_warn "Failed to install MariaDB $MARIADB_VERSION from official repo, falling back to distribution packages..." cleanup_old_repo_files "mariadb" $STD apt update || { msg_warn "APT update also failed, continuing with cache" } - install_packages_with_retry "mariadb-server" "mariadb-client" || { - msg_error "Failed to install MariaDB packages (both upstream and distro)" + if install_packages_with_retry "mariadb-server" "mariadb-client"; then + local FALLBACK_VERSION="" + FALLBACK_VERSION=$(mariadb --version 2>/dev/null | grep -oP '\d+\.\d+\.\d+' | head -n1 || echo "distro") + msg_warn "Installed MariaDB $FALLBACK_VERSION from distribution instead of requested $MARIADB_VERSION" + _setup_mariadb_runtime_dir + cache_installed_version "mariadb" "$FALLBACK_VERSION" + msg_ok "Setup MariaDB $FALLBACK_VERSION (fallback to distribution packages)" + return 0 + else + msg_error "Failed to install MariaDB packages (both official repo and distribution)" return 1 - } + fi fi + _setup_mariadb_runtime_dir + cache_installed_version "mariadb" "$MARIADB_VERSION" + msg_ok "Setup MariaDB $MARIADB_VERSION" +} + +# ------------------------------------------------------------------------------ +# Helper function: Configure MariaDB runtime directory persistence +# ------------------------------------------------------------------------------ +_setup_mariadb_runtime_dir() { # Configure tmpfiles.d to ensure /run/mysqld directory is created on boot # This fixes the issue where MariaDB fails to start after container reboot - msg_info "Configuring MariaDB runtime directory persistence" # Create tmpfiles.d configuration with error handling if ! printf '# Ensure /run/mysqld directory exists with correct permissions for MariaDB\nd /run/mysqld 0755 mysql mysql -\n' >/etc/tmpfiles.d/mariadb.conf; then @@ -3711,11 +3917,6 @@ setup_mariadb() { msg_warn "mysql user not found - directory created with correct permissions but ownership not set" fi fi - - msg_ok "Configured MariaDB runtime directory persistence" - - cache_installed_version "mariadb" "$MARIADB_VERSION" - msg_ok "Setup MariaDB $MARIADB_VERSION" } # ------------------------------------------------------------------------------ @@ -3815,6 +4016,11 @@ function setup_mongodb() { DISTRO_ID=$(get_os_info id) DISTRO_CODENAME=$(get_os_info codename) + # Ensure non-interactive mode for all apt operations + export DEBIAN_FRONTEND=noninteractive + export NEEDRESTART_MODE=a + export NEEDRESTART_SUSPEND=1 + # Check AVX support if ! grep -qm1 'avx[^ ]*' /proc/cpuinfo; then local major="${MONGO_VERSION%%.*}" @@ -3933,6 +4139,11 @@ function setup_mysql() { DISTRO_ID=$(awk -F= '/^ID=/{print $2}' /etc/os-release | tr -d '"') DISTRO_CODENAME=$(awk -F= '/^VERSION_CODENAME=/{print $2}' /etc/os-release) + # Ensure non-interactive mode for all apt operations + export DEBIAN_FRONTEND=noninteractive + export NEEDRESTART_MODE=a + export NEEDRESTART_SUSPEND=1 + # Get currently installed version local CURRENT_VERSION="" CURRENT_VERSION=$(is_tool_installed "mysql" 2>/dev/null) || true @@ -4027,7 +4238,6 @@ EOF ensure_apt_working || return 1 # Try multiple package names with retry logic - export DEBIAN_FRONTEND=noninteractive local mysql_install_success=false if apt-cache search "^mysql-server$" 2>/dev/null | grep -q . && @@ -4315,11 +4525,20 @@ EOF return 1 } - manage_tool_repository "php" "$PHP_VERSION" "" "https://packages.sury.org/debsuryorg-archive-keyring.deb" || { - msg_error "Failed to setup PHP repository" - return 1 - } - + # Use different repository based on OS + if [[ "$DISTRO_ID" == "ubuntu" ]]; then + # Ubuntu: Use ondrej/php PPA + msg_info "Adding ondrej/php PPA for Ubuntu" + $STD apt install -y software-properties-common + # Don't use $STD for add-apt-repository as it uses background processes + add-apt-repository -y ppa:ondrej/php >>"$(get_active_logfile)" 2>&1 + else + # Debian: Use Sury repository + manage_tool_repository "php" "$PHP_VERSION" "" "https://packages.sury.org/debsuryorg-archive-keyring.deb" || { + msg_error "Failed to setup PHP repository" + return 1 + } + fi ensure_apt_working || return 1 $STD apt update @@ -4342,6 +4561,14 @@ EOF if [[ "$PHP_FPM" == "YES" ]]; then MODULE_LIST+=" php${PHP_VERSION}-fpm" + # Create systemd override for PHP-FPM to fix runtime directory issues in LXC containers + mkdir -p /etc/systemd/system/php${PHP_VERSION}-fpm.service.d/ + cat </etc/systemd/system/php${PHP_VERSION}-fpm.service.d/override.conf +[Service] +RuntimeDirectory=php +RuntimeDirectoryMode=0755 +EOF + $STD systemctl daemon-reload fi # install apache2 with PHP support if requested @@ -4466,6 +4693,11 @@ function setup_postgresql() { DISTRO_ID=$(awk -F= '/^ID=/{print $2}' /etc/os-release | tr -d '"') DISTRO_CODENAME=$(awk -F= '/^VERSION_CODENAME=/{print $2}' /etc/os-release) + # Ensure non-interactive mode for all apt operations + export DEBIAN_FRONTEND=noninteractive + export NEEDRESTART_MODE=a + export NEEDRESTART_SUSPEND=1 + # Get currently installed version local CURRENT_PG_VERSION="" if command -v psql >/dev/null; then @@ -4904,6 +5136,146 @@ function setup_ruby() { msg_ok "Setup Ruby $RUBY_VERSION" } +# ------------------------------------------------------------------------------ +# Installs or updates MeiliSearch search engine. +# +# Description: +# - Fresh install: Downloads binary, creates config/service, starts +# - Update: Checks for new release, updates binary if available +# - Waits for service to be ready before returning +# - Exports API keys for use by caller +# +# Variables: +# MEILISEARCH_BIND - Bind address (default: 127.0.0.1:7700) +# MEILISEARCH_ENV - Environment: production/development (default: production) +# MEILISEARCH_DB_PATH - Database path (default: /var/lib/meilisearch/data) +# +# Exports: +# MEILISEARCH_MASTER_KEY - The master key for admin access +# MEILISEARCH_API_KEY - The default search API key +# MEILISEARCH_API_KEY_UID - The UID of the default API key +# +# Example (install script): +# setup_meilisearch +# +# Example (CT update_script): +# setup_meilisearch +# ------------------------------------------------------------------------------ + +function setup_meilisearch() { + local MEILISEARCH_BIND="${MEILISEARCH_BIND:-127.0.0.1:7700}" + local MEILISEARCH_ENV="${MEILISEARCH_ENV:-production}" + local MEILISEARCH_DB_PATH="${MEILISEARCH_DB_PATH:-/var/lib/meilisearch/data}" + local MEILISEARCH_DUMP_DIR="${MEILISEARCH_DUMP_DIR:-/var/lib/meilisearch/dumps}" + local MEILISEARCH_SNAPSHOT_DIR="${MEILISEARCH_SNAPSHOT_DIR:-/var/lib/meilisearch/snapshots}" + + # Get bind address for health checks + local MEILISEARCH_HOST="${MEILISEARCH_BIND%%:*}" + local MEILISEARCH_PORT="${MEILISEARCH_BIND##*:}" + [[ "$MEILISEARCH_HOST" == "0.0.0.0" ]] && MEILISEARCH_HOST="127.0.0.1" + + # Update mode: MeiliSearch already installed + if [[ -f /usr/bin/meilisearch ]]; then + if check_for_gh_release "meilisearch" "meilisearch/meilisearch"; then + msg_info "Updating MeiliSearch" + systemctl stop meilisearch + fetch_and_deploy_gh_release "meilisearch" "meilisearch/meilisearch" "binary" + systemctl start meilisearch + msg_ok "Updated MeiliSearch" + fi + return 0 + fi + + # Fresh install + msg_info "Setup MeiliSearch" + + # Install binary + fetch_and_deploy_gh_release "meilisearch" "meilisearch/meilisearch" "binary" || { + msg_error "Failed to install MeiliSearch binary" + return 1 + } + + # Download default config + curl -fsSL https://raw.githubusercontent.com/meilisearch/meilisearch/latest/config.toml -o /etc/meilisearch.toml || { + msg_error "Failed to download MeiliSearch config" + return 1 + } + + # Generate master key + MEILISEARCH_MASTER_KEY=$(openssl rand -base64 12) + export MEILISEARCH_MASTER_KEY + + # Configure + sed -i \ + -e "s|^env =.*|env = \"${MEILISEARCH_ENV}\"|" \ + -e "s|^# master_key =.*|master_key = \"${MEILISEARCH_MASTER_KEY}\"|" \ + -e "s|^db_path =.*|db_path = \"${MEILISEARCH_DB_PATH}\"|" \ + -e "s|^dump_dir =.*|dump_dir = \"${MEILISEARCH_DUMP_DIR}\"|" \ + -e "s|^snapshot_dir =.*|snapshot_dir = \"${MEILISEARCH_SNAPSHOT_DIR}\"|" \ + -e 's|^# no_analytics = true|no_analytics = true|' \ + -e "s|^http_addr =.*|http_addr = \"${MEILISEARCH_BIND}\"|" \ + /etc/meilisearch.toml + + # Create data directories + mkdir -p "${MEILISEARCH_DB_PATH}" "${MEILISEARCH_DUMP_DIR}" "${MEILISEARCH_SNAPSHOT_DIR}" + + # Create systemd service + cat </etc/systemd/system/meilisearch.service +[Unit] +Description=Meilisearch +After=network.target + +[Service] +ExecStart=/usr/bin/meilisearch --config-file-path /etc/meilisearch.toml +Restart=always + +[Install] +WantedBy=multi-user.target +EOF + + # Enable and start service + systemctl daemon-reload + systemctl enable -q --now meilisearch + + # Wait for MeiliSearch to be ready (up to 30 seconds) + for i in {1..30}; do + if curl -s -o /dev/null -w "%{http_code}" "http://${MEILISEARCH_HOST}:${MEILISEARCH_PORT}/health" 2>/dev/null | grep -q "200"; then + break + fi + sleep 1 + done + + # Verify service is running + if ! systemctl is-active --quiet meilisearch; then + msg_error "MeiliSearch service failed to start" + return 1 + fi + + # Get API keys with retry logic + MEILISEARCH_API_KEY="" + for i in {1..10}; do + MEILISEARCH_API_KEY=$(curl -s -X GET "http://${MEILISEARCH_HOST}:${MEILISEARCH_PORT}/keys" \ + -H "Authorization: Bearer ${MEILISEARCH_MASTER_KEY}" 2>/dev/null | \ + grep -o '"key":"[^"]*"' | head -n 1 | sed 's/"key":"//;s/"//') || true + [[ -n "$MEILISEARCH_API_KEY" ]] && break + sleep 2 + done + + MEILISEARCH_API_KEY_UID=$(curl -s -X GET "http://${MEILISEARCH_HOST}:${MEILISEARCH_PORT}/keys" \ + -H "Authorization: Bearer ${MEILISEARCH_MASTER_KEY}" 2>/dev/null | \ + grep -o '"uid":"[^"]*"' | head -n 1 | sed 's/"uid":"//;s/"//') || true + + export MEILISEARCH_API_KEY + export MEILISEARCH_API_KEY_UID + + # Cache version + local MEILISEARCH_VERSION + MEILISEARCH_VERSION=$(/usr/bin/meilisearch --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1) || true + cache_installed_version "meilisearch" "${MEILISEARCH_VERSION:-unknown}" + + msg_ok "Setup MeiliSearch ${MEILISEARCH_VERSION:-}" +} + # ------------------------------------------------------------------------------ # Installs or upgrades ClickHouse database server. # @@ -4923,6 +5295,11 @@ function setup_clickhouse() { DISTRO_ID=$(awk -F= '/^ID=/{print $2}' /etc/os-release | tr -d '"') DISTRO_CODENAME=$(awk -F= '/^VERSION_CODENAME=/{print $2}' /etc/os-release) + # Ensure non-interactive mode for all apt operations + export DEBIAN_FRONTEND=noninteractive + export NEEDRESTART_MODE=a + export NEEDRESTART_SUSPEND=1 + # Resolve "latest" version if [[ "$CLICKHOUSE_VERSION" == "latest" ]]; then CLICKHOUSE_VERSION=$(curl -fsSL --max-time 15 https://packages.clickhouse.com/tgz/stable/ 2>/dev/null | @@ -4985,7 +5362,6 @@ function setup_clickhouse() { "main" # Install packages with retry logic - export DEBIAN_FRONTEND=noninteractive $STD apt update || { msg_error "APT update failed for ClickHouse repository" return 1 @@ -5628,4 +6004,4 @@ EOF fi msg_ok "Docker setup completed" -} +} \ No newline at end of file