From f680e24aefe5a1236215bd2595aee3f9e9e1843d Mon Sep 17 00:00:00 2001 From: "CanbiZ (MickLesk)" <47820557+MickLesk@users.noreply.github.com> Date: Wed, 21 Jan 2026 14:26:03 +0100 Subject: [PATCH 1/5] test joplin --- ct/joplin.sh | 68 ++++++++++++++++++++++++++++++++++ install/joplin-install.sh | 78 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 146 insertions(+) create mode 100644 ct/joplin.sh create mode 100644 install/joplin-install.sh diff --git a/ct/joplin.sh b/ct/joplin.sh new file mode 100644 index 000000000..dfa01d3c7 --- /dev/null +++ b/ct/joplin.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash +source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/build.func) +# Copyright (c) 2021-2026 community-scripts ORG +# Author: Slaviša Arežina (tremor021) +# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE +# Source: https://joplinapp.org/ + +APP="Joplin" +var_tags="${var_tags:-notes}" +var_cpu="${var_cpu:-2}" +var_ram="${var_ram:-6144}" +var_disk="${var_disk:-20}" +var_os="${var_os:-debian}" +var_version="${var_version:-13}" +var_unprivileged="${var_unprivileged:-1}" + +header_info "$APP" +variables +color +catch_errors + +function update_script() { + header_info + check_container_storage + check_container_resources + if [[ ! -d /opt/joplin-server ]]; then + msg_error "No ${APP} Installation Found!" + exit + fi + + NODE_VERSION=24 NODE_MODULE="yarn,npm,pm2" setup_nodejs + + if check_for_gh_release "joplin-server" "laurent22/joplin"; then + msg_info "Stopping Services" + systemctl stop joplin-server + msg_ok "Stopped Services" + + cp /opt/joplin-server/.env /opt + CLEAN_INSTALL=1 fetch_and_deploy_gh_release "joplin-server" "laurent22/joplin" "tarball" + mv /opt/.env /opt/joplin-server + + msg_info "Updating Joplin-Server" + cd /opt/joplin-server + sed -i "/onenote-converter/d" packages/lib/package.json + $STD yarn config set --home enableTelemetry 0 + export BUILD_SEQUENCIAL=1 + $STD yarn workspaces focus @joplin/server + cd packages/server + $STD yarn run build + $STD yarn run tsc + msg_ok "Updated Joplin-Server" + + msg_info "Starting Services" + systemctl start joplin-server + msg_ok "Started Services" + msg_ok "Updated successfully!" + fi + exit +} + +start +build_container +description + +msg_ok "Completed successfully!\n" +echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}" +echo -e "${INFO}${YW} Access it using the following URL:${CL}" +echo -e "${TAB}${GATEWAY}${BGN}http://${IP}:22300${CL}" diff --git a/install/joplin-install.sh b/install/joplin-install.sh new file mode 100644 index 000000000..b5f7eab81 --- /dev/null +++ b/install/joplin-install.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash + +# Copyright (c) 2021-2026 community-scripts ORG +# Author: Slaviša Arežina (tremor021) +# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE +# Source: https://joplinapp.org/ + +source /dev/stdin <<<"$FUNCTIONS_FILE_PATH" +color +verb_ip6 +catch_errors +setting_up_container +network_check +update_os + +msg_info "Installing Dependencies" +$STD apt install -y \ + git \ + rsync +msg_ok "Installed Dependencies" + +PG_VERSION="17" setup_postgresql +PG_DB_NAME="joplin" PG_DB_USER="joplin" setup_postgresql_db +NODE_VERSION=24 NODE_MODULE="yarn,npm,pm2" setup_nodejs +mkdir -p /opt/pm2 +export PM2_HOME=/opt/pm2 +$STD pm2 install pm2-logrotate +$STD pm2 set pm2-logrotate:max_size 100MB +$STD pm2 set pm2-logrotate:retain 5 +$STD pm2 set pm2-logrotate:compress tr + +fetch_and_deploy_gh_release "joplin-server" "laurent22/joplin" "tarball" + +msg_info "Setting up Joplin Server (Patience)" +cd /opt/joplin-server +sed -i "/onenote-converter/d" packages/lib/package.json +$STD yarn config set --home enableTelemetry 0 +export BUILD_SEQUENCIAL=1 +$STD yarn workspaces focus @joplin/server +$STD yarn workspaces foreach -R --topological-dev --from @joplin/server run build +$STD yarn workspaces foreach -R --topological-dev --from @joplin/server run tsc +cat </opt/joplin-server/.env +PM2_HOME=/opt/pm2 +NODE_ENV=production +APP_BASE_URL=http://$LOCAL_IP:22300 +APP_PORT=22300 +DB_CLIENT=pg +POSTGRES_PASSWORD=$PG_DB_PASS +POSTGRES_DATABASE=$PG_DB_NAME +POSTGRES_USER=$PG_DB_USER +POSTGRES_PORT=5432 +POSTGRES_HOST=localhost +EOF +msg_ok "Setup Joplin Server" + +msg_info "Setting up Service" +cat </etc/systemd/system/joplin-server.service +[Unit] +Description=Joplin Server Service +After=network.target + +[Service] +Type=simple +WorkingDirectory=/opt/joplin-server/packages/server +EnvironmentFile=/opt/joplin-server/.env +ExecStart=/usr/bin/yarn start-prod +Restart=on-failure +RestartSec=5 + +[Install] +WantedBy=multi-user.target +EOF +systemctl enable -q --now joplin-server +msg_ok "Service Setup" + +motd_ssh +customize +cleanup_lxc From db5e26f4f16574af5acc6791dab7e8539fba8478 Mon Sep 17 00:00:00 2001 From: "CanbiZ (MickLesk)" <47820557+MickLesk@users.noreply.github.com> Date: Wed, 21 Jan 2026 14:30:44 +0100 Subject: [PATCH 2/5] merge tools.func from VE to VED --- misc/tools.func | 171 ++++++++++++++++++++++++++++++++++++------------ 1 file changed, 129 insertions(+), 42 deletions(-) diff --git a/misc/tools.func b/misc/tools.func index 7e18d475d..29a95c8aa 100644 --- a/misc/tools.func +++ b/misc/tools.func @@ -2691,7 +2691,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]}" @@ -3010,16 +3010,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 @@ -3032,53 +3040,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 @@ -3102,20 +3172,33 @@ 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 + $STD apt-get -y -o Dpkg::Options::="--force-confold" install --no-install-recommends libnvidia-decode libnvidia-encode nvidia-utils 2>/dev/null || msg_warn "NVIDIA installation failed" + fi else + msg_warn "No NVIDIA packages for version ${nvidia_major_version}.x in CUDA repo" # 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" + $STD apt-get -y -o Dpkg::Options::="--force-confold" install --no-install-recommends libnvidia-decode libnvidia-encode nvidia-utils 2>/dev/null || msg_warn "NVIDIA installation failed" 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" } @@ -3215,6 +3298,10 @@ EOF $STD apt -y update } +# ══════════════════════════════════════════════════════════════════════════════ +# Helper: Setup GPU device permissions +# ══════════════════════════════════════════════════════════════════════════════ + # ══════════════════════════════════════════════════════════════════════════════ # Helper: Setup GPU device permissions # ══════════════════════════════════════════════════════════════════════════════ From 5827704ce8a4afa4c2093ee444031d24b28c4c18 Mon Sep 17 00:00:00 2001 From: "CanbiZ (MickLesk)" <47820557+MickLesk@users.noreply.github.com> Date: Wed, 21 Jan 2026 14:31:05 +0100 Subject: [PATCH 3/5] fix source --- ct/joplin.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ct/joplin.sh b/ct/joplin.sh index dfa01d3c7..c995a5c0e 100644 --- a/ct/joplin.sh +++ b/ct/joplin.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/build.func) +source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/build.func) # Copyright (c) 2021-2026 community-scripts ORG # Author: Slaviša Arežina (tremor021) # License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE From eb605f0c332dd23e73ea62a53112a443c0376240 Mon Sep 17 00:00:00 2001 From: "CanbiZ (MickLesk)" <47820557+MickLesk@users.noreply.github.com> Date: Wed, 21 Jan 2026 14:35:32 +0100 Subject: [PATCH 4/5] merge core from VE to VED --- misc/core.func | 68 ++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 66 insertions(+), 2 deletions(-) diff --git a/misc/core.func b/misc/core.func index 8c2bf008e..8ee70a3df 100644 --- a/misc/core.func +++ b/misc/core.func @@ -838,9 +838,11 @@ cleanup_lxc() { fi # Node.js npm - if command -v npm &>/dev/null; then $STD npm cache clean --force 2>/dev/null || true; fi +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 $STD yarn cache clean 2>/dev/null || true; fi + #if command -v yarn &>/dev/null; then $STD yarn cache clean 2>/dev/null || true; fi # Node.js pnpm if command -v pnpm &>/dev/null; then $STD pnpm store prune 2>/dev/null || true; fi # Go @@ -906,6 +908,68 @@ 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) + 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 + 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 + + # Last resort: Use routing table + local targets=("8.8.8.8" "1.1.1.1" "default") + 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 +} + + # ============================================================================== # SIGNAL TRAPS # ============================================================================== From eb4c45c9fe890271c4869d19f020cb168bfac39e Mon Sep 17 00:00:00 2001 From: "CanbiZ (MickLesk)" <47820557+MickLesk@users.noreply.github.com> Date: Wed, 21 Jan 2026 14:44:57 +0100 Subject: [PATCH 5/5] Big Merge -> build.func (VE) to VED Compared. --- misc/build.func | 598 +++++++++++++++++++++++++++++++++++++++++++++- misc/install.func | 2 + 2 files changed, 589 insertions(+), 11 deletions(-) diff --git a/misc/build.func b/misc/build.func index 300bd6227..05d54df60 100644 --- a/misc/build.func +++ b/misc/build.func @@ -337,6 +337,347 @@ 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_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() # @@ -523,6 +864,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 @@ -588,9 +935,47 @@ 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} + + # Password sanitization - clean up dashes and format properly + 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"} @@ -660,9 +1045,11 @@ base_settings() { # - Safe parser for KEY=VALUE lines from vars files # - Used by default_var_settings and app defaults loading # - Only loads whitelisted var_* keys +# - Optional force parameter to override existing values (for app defaults) # ------------------------------------------------------------------------------ load_vars_file() { local file="$1" + local force="${2:-no}" # If "yes", override existing variables [ -f "$file" ] || return 0 msg_info "Loading defaults from ${file}" @@ -693,10 +1080,9 @@ load_vars_file() { [[ "$var_key" != var_* ]] && continue _is_whitelisted "$var_key" || continue - # Strip inline comments (everything after unquoted #) - # Handle: var=value # comment OR var="value" # comment + # Strip inline comments (anything after unquoted #) + # Only strip if not inside quotes if [[ ! "$var_val" =~ ^[\"\'] ]]; then - # Unquoted value: strip from first # var_val="${var_val%%#*}" fi @@ -710,8 +1096,125 @@ load_vars_file() { # Trim trailing whitespace var_val="${var_val%"${var_val##*[![:space:]]}"}" - # Set only if not already exported - [[ -z "${!var_key+x}" ]] && export "${var_key}=${var_val}" + # 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 + ;; + 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}" + else + [[ -z "${!var_key+x}" ]] && export "${var_key}=${var_val}" + fi fi done <"$file" msg_ok "Loaded ${file}" @@ -1047,6 +1550,7 @@ _build_current_app_vars_tmp() { _apt_cacher_ip="${APT_CACHER_IP:-}" _fuse="${ENABLE_FUSE:-no}" _tun="${ENABLE_TUN:-no}" + _gpu="${ENABLE_GPU:-no}" _nesting="${ENABLE_NESTING:-1}" _keyctl="${ENABLE_KEYCTL:-0}" _mknod="${ENABLE_MKNOD:-0}" @@ -1096,6 +1600,7 @@ _build_current_app_vars_tmp() { [ -n "$_fuse" ] && echo "var_fuse=$(_sanitize_value "$_fuse")" [ -n "$_tun" ] && echo "var_tun=$(_sanitize_value "$_tun")" + [ -n "$_gpu" ] && echo "var_gpu=$(_sanitize_value "$_gpu")" [ -n "$_nesting" ] && echo "var_nesting=$(_sanitize_value "$_nesting")" [ -n "$_keyctl" ] && echo "var_keyctl=$(_sanitize_value "$_keyctl")" [ -n "$_mknod" ] && echo "var_mknod=$(_sanitize_value "$_mknod")" @@ -2695,8 +3200,8 @@ configure_ssh_settings() { # # - Entry point of script # - On Proxmox host: calls install_script -# - In silent mode: runs update_script -# - Otherwise: shows update/setting menu +# - In silent mode: runs update_script with automatic cleanup +# - Otherwise: shows update/setting menu and runs update_script with cleanup # ------------------------------------------------------------------------------ start() { source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/tools.func) @@ -2706,7 +3211,9 @@ start() { elif [ ! -z ${PHS_SILENT+x} ] && [[ "${PHS_SILENT}" == "1" ]]; then VERBOSE="no" set_std_mode + ensure_profile_loaded update_script + cleanup_lxc else CHOICE=$(whiptail --backtitle "[dev] Proxmox VE Helper Scripts" --title "${APP} LXC Update/Setting" --menu \ "Support/Update functions for ${APP} LXC. Choose an option:" \ @@ -2730,7 +3237,9 @@ start() { exit ;; esac + ensure_profile_loaded update_script + cleanup_lxc fi } @@ -2847,6 +3356,8 @@ build_container() { export CTTYPE="$CT_TYPE" export ENABLE_FUSE="$ENABLE_FUSE" export ENABLE_TUN="$ENABLE_TUN" + export ENABLE_GPU="$ENABLE_GPU" + export IPV6_METHOD="$IPV6_METHOD" export PCT_OSTYPE="$var_os" export PCT_OSVERSION="$var_version" export PCT_DISK_SIZE="$DISK_SIZE" @@ -3750,6 +4261,64 @@ select_storage() { 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 +} + create_lxc_container() { # ------------------------------------------------------------------------------ # Optional verbose mode (debug tracing) @@ -3975,7 +4544,14 @@ create_lxc_container() { sed 's|.*/||' | sort -t - -k 2 -V ) - pveam update >/dev/null 2>&1 || msg_warn "Could not update template catalog (pveam update failed)." + # Update template catalog with timeout to prevent hangs on slow networks + if command -v timeout &>/dev/null; then + if ! timeout 30 pveam update >/dev/null 2>&1; then + msg_warn "Template catalog update timed out or failed (continuing with cached data)" + fi + else + pveam update >/dev/null 2>&1 || msg_warn "Could not update template catalog (pveam update failed)." + fi msg_ok "Template search completed" diff --git a/misc/install.func b/misc/install.func index c8d6ae7e1..0a26d7d70 100644 --- a/misc/install.func +++ b/misc/install.func @@ -177,6 +177,8 @@ _bootstrap() { source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/error_handler.func) load_functions catch_errors + + get_lxc_ip } # Run bootstrap and OS detection