diff --git a/CHANGELOG.md b/CHANGELOG.md index 1362ef08d..c2b6327c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -426,35 +426,20 @@ Exercise vigilance regarding copycat or coat-tailing sites that seek to exploit -## 2026-03-23 +## 2026-03-24 ### 🆕 New Scripts - - Alpine-Borgbackup-Server ([#13219](https://github.com/community-scripts/ProxmoxVE/pull/13219)) + - Homebrew (Addon) ([#13249](https://github.com/community-scripts/ProxmoxVE/pull/13249)) +- NextExplorer ([#13252](https://github.com/community-scripts/ProxmoxVE/pull/13252)) + +## 2026-03-23 ### 🚀 Updated Scripts - - NginxProxyManager: build OpenResty from source via GitHub releases [@MickLesk](https://github.com/MickLesk) ([#13134](https://github.com/community-scripts/ProxmoxVE/pull/13134)) - - - #### 🐞 Bug Fixes - - - Tracearr: modify service restart and modify build ressources [@MickLesk](https://github.com/MickLesk) ([#13230](https://github.com/community-scripts/ProxmoxVE/pull/13230)) - - - #### ✨ New Features - - - Kometa: optimize config.yml sed patterns, add Quickstart integration [@MickLesk](https://github.com/MickLesk) ([#13198](https://github.com/community-scripts/ProxmoxVE/pull/13198)) - - #### 🔧 Refactor - - Refactor: nginxproxymanager update and OpenResty flow [@MickLesk](https://github.com/MickLesk) ([#13216](https://github.com/community-scripts/ProxmoxVE/pull/13216)) - - Refactor: PartDB [@MickLesk](https://github.com/MickLesk) ([#13229](https://github.com/community-scripts/ProxmoxVE/pull/13229)) - -### 💾 Core - - - #### 🔧 Refactor - - - core: alpine - Improve network connectivity and DNS checks [@MickLesk](https://github.com/MickLesk) ([#13222](https://github.com/community-scripts/ProxmoxVE/pull/13222)) - - core: allow /31 and /32 CIDR with out-of-subnet gateway [@MickLesk](https://github.com/MickLesk) ([#13231](https://github.com/community-scripts/ProxmoxVE/pull/13231)) + - core: harden shell scripts against injection and insecure permissions [@MickLesk](https://github.com/MickLesk) ([#13239](https://github.com/community-scripts/ProxmoxVE/pull/13239)) ## 2026-03-22 diff --git a/ct/headers/nextexplorer b/ct/headers/nextexplorer new file mode 100644 index 000000000..d6da7eb98 --- /dev/null +++ b/ct/headers/nextexplorer @@ -0,0 +1,6 @@ + __ ______ __ + ____ ___ _ __/ /_/ ____/ ______ / /___ ________ _____ + / __ \/ _ \| |/_/ __/ __/ | |/_/ __ \/ / __ \/ ___/ _ \/ ___/ + / / / / __/> /etc/nextExplorer/.env +NODE_ENV=production +PORT=3000 + +VOLUME_ROOT=/mnt +CONFIG_DIR=/etc/nextExplorer +CACHE_DIR=/etc/nextExplorer/cache +# USER_ROOT= + +PUBLIC_URL=${LOCAL_IP}:3000 +# TRUST_PROXY= +# CORS_ORIGINS= + +TERMINAL_ENABLED=false + +LOG_LEVEL=info +DEBUG=false +ENABLE_HTTP_LOGGING=false + +AUTH_ENABLED=true +AUTH_MODE=both +SESSION_SECRET="${SECRET}" +# AUTH_MAX_FAILED= +# AUTH_LOCK_MINUTES= +# AUTH_USER_EMAIL= +# AUTH_USER_PASSWORD= + +# OIDC_ENABLED= +# OIDC_ISSUER= +# OIDC_AUTHORIZATION_URL= +# OIDC_TOKEN_URL= +# OIDC_USERINFO_URL= +# OIDC_CLIENT_ID= +# OIDC_CLIENT_SECRET= +# OIDC_CALLBACK_URL= +# OIDC_LOGOUT_URL= +# OIDC_SCOPES= +# OIDC_AUTO_CREATE_USERS=true + +# SEARCH_DEEP= +# SEARCH_RIPGREP= +# SEARCH_MAX_FILESIZE= + +# ONLYOFFICE_URL= +# ONLYOFFICE_SECRET= +# ONLYOFFICE_LANG= +# ONLYOFFICE_FORCE_SAVE= +# ONLYOFFICE_FILE_EXTENSIONS= + +# COLLABORA_URL= +# COLLABORA_DISCOVERY_URL= +# COLLABORA_SECRET= +# COLLABORA_LANG= +# COLLABORA_FILE_EXTENSIONS= + +SHOW_VOLUME_USAGE=true +# USER_DIR_ENABLED= +# SKIP_HOME= + +# EDITOR_EXTENSIONS= + +# FFMPEG_PATH= +# FFPROBE_PATH= + +## Hardware acceleration +# FFMPEG_HWACCEL=vaapi +# FFMPEG_HWACCEL_DEVICE=/dev/dri/renderD128 +# FFMPEG_HWACCEL_OUTPUT_FORMAT=nv12 + +FAVORITES_DEFAULT_ICON=outline.StarIcon + +SHARES_ENABLED=true +# SHARES_TOKEN_LENGTH=10 +# SHARES_MAX_PER_USER=100 +# SHARES_DEFAULT_EXPIRY_DAYS=30 +# SHARES_GUEST_SESSION_HOURS=24 +# SHARES_ALLOW_PASSWORD=true +# SHARES_ALLOW_ANONYMOUS=true +EOF +chmod 600 /etc/nextExplorer/.env +$STD useradd -U -s /usr/sbin/nologin -m -d /home/explorer explorer +chown -R explorer:explorer "$APP_DIR" /etc/nextExplorer +sed -i "\|version|s|$(jq -cr '.version' ${APP_DIR}/package.json)|$(cat ~/.nextexplorer)|" "$APP_DIR"/package.json +msg_ok "Configured nextExplorer" + +msg_info "Creating nextExplorer Service" +cat </etc/systemd/system/nextexplorer.service +[Unit] +Description=nextExplorer Service +After=network.target + +[Service] +Type=simple +User=explorer +Group=explorer +WorkingDirectory=/opt/nextExplorer/app +EnvironmentFile=/etc/nextExplorer/.env +ExecStart=/usr/bin/node ./src/server.js +Restart=always +RestartSec=5 +StandardOutput=journal +StandardError=journal + +[Install] +WantedBy=multi-user.target +EOF +$STD systemctl enable -q --now nextexplorer +msg_ok "Created nextExplorer Service" + +motd_ssh +customize +cleanup_lxc diff --git a/install/shinobi-install.sh b/install/shinobi-install.sh index a5ffe1c94..383325016 100644 --- a/install/shinobi-install.sh +++ b/install/shinobi-install.sh @@ -35,7 +35,7 @@ cd Shinobi gitVersionNumber=$(git rev-parse HEAD) theDateRightNow=$(date) touch version.json -chmod 777 version.json +chmod 644 version.json echo '{"Product" : "'"Shinobi"'" , "Branch" : "'"master"'" , "Version" : "'"$gitVersionNumber"'" , "Date" : "'"$theDateRightNow"'" , "Repository" : "'"https://gitlab.com/Shinobi-Systems/Shinobi.git"'"}' >version.json msg_ok "Cloned Shinobi" diff --git a/install/tasmoadmin-install.sh b/install/tasmoadmin-install.sh index 2186e11b5..9f84f719b 100644 --- a/install/tasmoadmin-install.sh +++ b/install/tasmoadmin-install.sh @@ -23,7 +23,7 @@ fetch_and_deploy_gh_release "tasmoadmin" "TasmoAdmin/TasmoAdmin" "prebuild" "lat msg_info "Configuring TasmoAdmin" rm -rf /etc/php/8.4/apache2/conf.d/10-opcache.ini chown -R www-data:www-data /var/www/tasmoadmin -chmod 777 /var/www/tasmoadmin/tmp /var/www/tasmoadmin/data +chmod 775 /var/www/tasmoadmin/tmp /var/www/tasmoadmin/data cat </etc/apache2/sites-available/tasmoadmin.conf ServerName tasmoadmin diff --git a/misc/api.func b/misc/api.func index f65702679..c90731091 100644 --- a/misc/api.func +++ b/misc/api.func @@ -348,10 +348,10 @@ explain_exit_code() { json_escape() { # Escape a string for safe JSON embedding using awk (handles any input size). # Pipeline: strip ANSI → remove control chars → escape \ " TAB → join lines with \n - printf '%s' "$1" \ - | sed 's/\x1b\[[0-9;]*[a-zA-Z]//g' \ - | tr -d '\000-\010\013\014\016-\037\177\r' \ - | awk ' + printf '%s' "$1" | + sed 's/\x1b\[[0-9;]*[a-zA-Z]//g' | + tr -d '\000-\010\013\014\016-\037\177\r' | + awk ' BEGIN { ORS = "" } { gsub(/\\/, "\\\\") # backslash → \\ @@ -627,7 +627,7 @@ post_to_api() { [[ "${DEV_MODE:-}" == "true" ]] && echo "[DEBUG] post_to_api() DIAGNOSTICS=$DIAGNOSTICS RANDOM_UUID=$RANDOM_UUID NSAPP=$NSAPP" >&2 - # Set type for later status updates (respect pre-set value, e.g. "turnkey") + # Set type for later status updates (preserve if already set, e.g. turnkey) TELEMETRY_TYPE="${TELEMETRY_TYPE:-lxc}" local pve_version="" @@ -692,6 +692,7 @@ EOF # Send initial "installing" record with retry. # This record MUST exist for all subsequent updates to succeed. local http_code="" attempt + local _post_success=false for attempt in 1 2 3; do if [[ "${DEV_MODE:-}" == "true" ]]; then http_code=$(curl -sS -w "%{http_code}" -m "${TELEMETRY_TIMEOUT}" -X POST "${TELEMETRY_URL}" \ @@ -703,11 +704,19 @@ EOF -H "Content-Type: application/json" \ -d "$JSON_PAYLOAD" -o /dev/null 2>/dev/null) || http_code="000" fi - [[ "$http_code" =~ ^2[0-9]{2}$ ]] && break + if [[ "$http_code" =~ ^2[0-9]{2}$ ]]; then + _post_success=true + break + fi [[ "$attempt" -lt 3 ]] && sleep 1 done - POST_TO_API_DONE=true + # Only mark done if at least one attempt succeeded. + # If all 3 failed, POST_TO_API_DONE stays false so post_update_to_api + # and on_exit() know the initial record was never created. + # The server has fallback logic to create a new record on status updates, + # so subsequent calls can still succeed even without the initial record. + POST_TO_API_DONE=${_post_success} } # ------------------------------------------------------------------------------ @@ -798,15 +807,19 @@ EOF # Send initial "installing" record with retry (must succeed for updates to work) local http_code="" attempt + local _post_success=false for attempt in 1 2 3; do http_code=$(curl -sS -w "%{http_code}" -m "${TELEMETRY_TIMEOUT}" -X POST "${TELEMETRY_URL}" \ -H "Content-Type: application/json" \ -d "$JSON_PAYLOAD" -o /dev/null 2>/dev/null) || http_code="000" - [[ "$http_code" =~ ^2[0-9]{2}$ ]] && break + if [[ "$http_code" =~ ^2[0-9]{2}$ ]]; then + _post_success=true + break + fi [[ "$attempt" -lt 3 ]] && sleep 1 done - POST_TO_API_DONE=true + POST_TO_API_DONE=${_post_success} } # ------------------------------------------------------------------------------ @@ -1083,6 +1096,12 @@ EOF # - Used to group errors in dashboard # ------------------------------------------------------------------------------ categorize_error() { + # Allow build.func to override category based on log analysis (exit code 1 subclassification) + if [[ -n "${ERROR_CATEGORY_OVERRIDE:-}" ]]; then + echo "$ERROR_CATEGORY_OVERRIDE" + return + fi + local code="$1" case "$code" in # Network errors (curl/wget) diff --git a/misc/build.func b/misc/build.func index 10aac9927..304f1c46b 100644 --- a/misc/build.func +++ b/misc/build.func @@ -221,6 +221,14 @@ update_motd_ip() { local current_hostname="$(hostname)" local current_ip="$(hostname -I | awk '{print $1}')" + # Escape sed special chars in replacement strings (& \ |) + current_os="${current_os//\\/\\\\}" + current_os="${current_os//&/\\&}" + current_hostname="${current_hostname//\\/\\\\}" + current_hostname="${current_hostname//&/\\&}" + current_ip="${current_ip//\\/\\\\}" + current_ip="${current_ip//&/\\&}" + # 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" @@ -4076,8 +4084,8 @@ EOF if [ "$var_os" == "alpine" ]; then sleep 3 pct exec "$CTID" -- /bin/sh -c 'cat </etc/apk/repositories -http://dl-cdn.alpinelinux.org/alpine/latest-stable/main -http://dl-cdn.alpinelinux.org/alpine/latest-stable/community +https://dl-cdn.alpinelinux.org/alpine/latest-stable/main +https://dl-cdn.alpinelinux.org/alpine/latest-stable/community EOF' pct exec "$CTID" -- ash -c "apk add bash newt curl openssh nano mc ncurses jq" >>"$BUILD_LOG" 2>&1 || { msg_error "Failed to install base packages in Alpine container" @@ -4086,7 +4094,9 @@ EOF' else sleep 3 LANG=${LANG:-en_US.UTF-8} - pct exec "$CTID" -- bash -c "sed -i \"/$LANG/ s/^# //\" /etc/locale.gen" + local LANG_ESC="${LANG//./\\.}" + LANG_ESC="${LANG_ESC//|/\\|}" + pct exec "$CTID" -- bash -c "sed -i \"/$LANG_ESC/ s/^# //\" /etc/locale.gen" pct exec "$CTID" -- bash -c "locale_line=\$(grep -v '^#' /etc/locale.gen | grep -E '^[a-zA-Z]' | awk '{print \$1}' | head -n 1) && \ echo LANG=\$locale_line >/etc/default/locale && \ locale-gen >/dev/null && \ @@ -4216,6 +4226,53 @@ EOF' fi fi + # Defense-in-depth: Ensure error handling stays disabled during recovery. + # Some functions (e.g. silent/$STD) unconditionally re-enable set -Eeuo pipefail + # and trap 'error_handler' ERR. If any code path above called such a function, + # the grep/sed pipelines below would trigger error_handler on non-match (exit 1). + set +Eeuo pipefail + trap - ERR + + # --- Exit code 1 subclassification: analyze logs BEFORE telemetry call --- + # Exit code 1 is generic ("General error"). Analyze logs to determine the + # real error category so telemetry gets a useful classification instead of "shell". + local is_oom=false + local is_network_issue=false + local is_apt_issue=false + local is_cmd_not_found=false + local is_disk_full=false + + if [[ $install_exit_code -eq 1 && -f "$combined_log" ]]; then + if grep -qiE 'E: Unable to|E: Package|E: Failed to fetch|dpkg.*error|broken packages|unmet dependencies|dpkg --configure -a' "$combined_log"; then + is_apt_issue=true + fi + if grep -qiE 'Cannot allocate memory|Out of memory|oom-killer|Killed process|JavaScript heap' "$combined_log"; then + is_oom=true + fi + if grep -qiE 'Could not resolve|DNS|Connection refused|Network is unreachable|No route to host|Temporary failure resolving|Failed to fetch' "$combined_log"; then + is_network_issue=true + fi + if grep -qiE ': command not found|No such file or directory.*/s?bin/' "$combined_log"; then + is_cmd_not_found=true + fi + if grep -qiE 'ENOSPC|no space left on device|Disk quota exceeded|errno -28' "$combined_log"; then + is_disk_full=true + fi + fi + + # Set override for categorize_error() so telemetry gets the real category + if [[ "$is_apt_issue" == true ]]; then + export ERROR_CATEGORY_OVERRIDE="dependency" + elif [[ "$is_oom" == true ]]; then + export ERROR_CATEGORY_OVERRIDE="resource" + elif [[ "$is_network_issue" == true ]]; then + export ERROR_CATEGORY_OVERRIDE="network" + elif [[ "$is_disk_full" == true ]]; then + export ERROR_CATEGORY_OVERRIDE="storage" + elif [[ "$is_cmd_not_found" == true ]]; then + export ERROR_CATEGORY_OVERRIDE="dependency" + fi + # Report failure to telemetry API (now with log available on host) # NOTE: Do NOT use msg_info/spinner here — the background spinner process # causes SIGTSTP in non-interactive shells (bash -c "$(curl ...)"), which @@ -4224,13 +4281,6 @@ EOF' post_update_to_api "failed" "$install_exit_code" $STD echo -e "${TAB}${CM:-✔} Failure reported" - # Defense-in-depth: Ensure error handling stays disabled during recovery. - # Some functions (e.g. silent/$STD) unconditionally re-enable set -Eeuo pipefail - # and trap 'error_handler' ERR. If any code path above called such a function, - # the grep/sed pipelines below would trigger error_handler on non-match (exit 1). - set +Eeuo pipefail - trap - ERR - # Show combined log location if [[ -n "$CTID" && -n "${SESSION_ID:-}" ]]; then msg_custom "📋" "${YW}" "Installation log: ${combined_log}" @@ -4259,12 +4309,9 @@ EOF' # Prompt user for cleanup with 60s timeout echo "" - # Detect error type for smart recovery options - local is_oom=false - local is_network_issue=false - local is_apt_issue=false - local is_cmd_not_found=false - local is_disk_full=false + # Extend error detection for non-exit-1 codes (exit 1 was already analyzed above) + # The is_* flags were set above for exit code 1 log analysis; here we add + # exit-code-specific detections for other codes. local error_explanation="" if declare -f explain_exit_code >/dev/null 2>&1; then error_explanation="$(explain_exit_code "$install_exit_code")" @@ -4314,26 +4361,6 @@ EOF' ;; esac - # Exit 1 subclassification: analyze logs to identify actual root cause - # Many exit 1 errors are actually APT, OOM, network, or command-not-found issues - if [[ $install_exit_code -eq 1 && -f "$combined_log" ]]; then - if grep -qiE 'E: Unable to|E: Package|E: Failed to fetch|dpkg.*error|broken packages|unmet dependencies|dpkg --configure -a' "$combined_log"; then - is_apt_issue=true - fi - if grep -qiE 'Cannot allocate memory|Out of memory|oom-killer|Killed process|JavaScript heap' "$combined_log"; then - is_oom=true - fi - if grep -qiE 'Could not resolve|DNS|Connection refused|Network is unreachable|No route to host|Temporary failure resolving|Failed to fetch' "$combined_log"; then - is_network_issue=true - fi - if grep -qiE ': command not found|No such file or directory.*/s?bin/' "$combined_log"; then - is_cmd_not_found=true - fi - if grep -qiE 'ENOSPC|no space left on device|Disk quota exceeded|errno -28' "$combined_log"; then - is_disk_full=true - fi - fi - # Show error explanation if available if [[ -n "$error_explanation" ]]; then echo -e "${TAB}${RD}Error: ${error_explanation}${CL}" @@ -4535,6 +4562,7 @@ EOF' if [[ $apt_retry_code -eq 0 ]]; then msg_ok "Installation completed successfully after APT repair!" + INSTALL_COMPLETE=true post_update_to_api "done" "0" "force" return 0 else @@ -4759,6 +4787,10 @@ fix_gpu_gids() { pct stop "$CTID" >/dev/null 2>&1 sleep 1 + # Validate GIDs are numeric before sed + [[ "$render_gid" =~ ^[0-9]+$ ]] || render_gid="104" + [[ "$video_gid" =~ ^[0-9]+$ ]] || video_gid="44" + # Update dev entries with correct GIDs sed -i.bak -E "s|(dev[0-9]+: /dev/dri/renderD[0-9]+),gid=[0-9]+|\1,gid=${render_gid}|g" "$LXC_CONFIG" sed -i -E "s|(dev[0-9]+: /dev/dri/card[0-9]+),gid=[0-9]+|\1,gid=${video_gid}|g" "$LXC_CONFIG" @@ -5705,6 +5737,7 @@ EOF systemctl start ping-instances.service fi + INSTALL_COMPLETE=true post_update_to_api "done" "none" } diff --git a/misc/error_handler.func b/misc/error_handler.func index 39e5e667f..bf81ea9bf 100644 --- a/misc/error_handler.func +++ b/misc/error_handler.func @@ -507,14 +507,23 @@ _stop_container_if_installing() { on_exit() { local exit_code=$? - # Report orphaned "installing" records to telemetry API - # Catches ALL exit paths: errors, signals, AND clean exits where - # post_to_api was called but post_update_to_api was never called - if [[ "${POST_TO_API_DONE:-}" == "true" && "${POST_UPDATE_DONE:-}" != "true" ]]; then - if [[ $exit_code -ne 0 ]]; then - _send_abort_telemetry "$exit_code" - elif declare -f post_update_to_api >/dev/null 2>&1; then - post_update_to_api "done" "0" 2>/dev/null || true + # Report orphaned telemetry records + # Two scenarios handled: + # 1. POST_TO_API_DONE=true but POST_UPDATE_DONE=false: Record was created but + # never got a final status update → send abort/done now. + # 2. POST_TO_API_DONE=false but DIAGNOSTICS=yes: Initial post failed (server + # unreachable/timeout), but the server has fallback create-on-update logic, + # so a status update can still create the record. Worth one last try. + if [[ "${POST_UPDATE_DONE:-}" != "true" ]]; then + if [[ "${POST_TO_API_DONE:-}" == "true" || "${DIAGNOSTICS:-no}" == "yes" ]]; then + if [[ $exit_code -ne 0 ]]; then + _send_abort_telemetry "$exit_code" + elif [[ "${INSTALL_COMPLETE:-}" == "true" ]] && declare -f post_update_to_api >/dev/null 2>&1; then + # Only report success if the install was explicitly marked complete. + # Without this guard, early bailouts (e.g. user cancelled) with exit 0 + # would be falsely reported as successful installations. + post_update_to_api "done" "0" 2>/dev/null || true + fi fi fi diff --git a/misc/install.func b/misc/install.func index 94f005b26..024e0633d 100644 --- a/misc/install.func +++ b/misc/install.func @@ -309,14 +309,14 @@ customize() { if [[ "$PASSWORD" == "" ]]; then msg_info "Customizing Container" GETTY_OVERRIDE="/etc/systemd/system/container-getty@1.service.d/override.conf" - mkdir -p $(dirname $GETTY_OVERRIDE) - cat <$GETTY_OVERRIDE + mkdir -p "$(dirname "$GETTY_OVERRIDE")" + cat <"$GETTY_OVERRIDE" [Service] ExecStart= ExecStart=-/sbin/agetty --autologin root --noclear --keep-baud tty%I 115200,38400,9600 \$TERM EOF systemctl daemon-reload - systemctl restart $(basename $(dirname $GETTY_OVERRIDE) | sed 's/\.d//') + systemctl restart "$(basename "$(dirname "$GETTY_OVERRIDE")" | sed 's/\.d//')" msg_ok "Customized Container" fi echo "bash -c \"\$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/${app}.sh)\"" >/usr/bin/update diff --git a/misc/tools.func b/misc/tools.func index 2e54b86e1..62bc83b6a 100644 --- a/misc/tools.func +++ b/misc/tools.func @@ -242,7 +242,7 @@ download_gpg_key() { # Process based on mode if [[ "$mode" == "dearmor" ]]; then - if gpg --dearmor --yes -o "$output" <"$temp_key" 2>/dev/null; then + if gpg --dearmor --yes -o "$output" <"$temp_key" 2>/dev/null && [[ -s "$output" ]]; then rm -f "$temp_key" debug_log "GPG key installed (dearmored): $output" return 0 @@ -5192,7 +5192,7 @@ _setup_gpu_permissions() { for nvidia_dev in /dev/nvidia*; do [[ -e "$nvidia_dev" ]] && { chgrp video "$nvidia_dev" 2>/dev/null || true - chmod 666 "$nvidia_dev" 2>/dev/null || true + chmod 660 "$nvidia_dev" 2>/dev/null || true } done if [[ -d /dev/nvidia-caps ]]; then @@ -5200,7 +5200,7 @@ _setup_gpu_permissions() { for caps_dev in /dev/nvidia-caps/*; do [[ -e "$caps_dev" ]] && { chgrp video "$caps_dev" 2>/dev/null || true - chmod 666 "$caps_dev" 2>/dev/null || true + chmod 660 "$caps_dev" 2>/dev/null || true } done fi @@ -5217,7 +5217,8 @@ _setup_gpu_permissions() { # /dev/kfd permissions (AMD ROCm) if [[ -e /dev/kfd ]]; then - chmod 666 /dev/kfd 2>/dev/null || true + chgrp render /dev/kfd 2>/dev/null || true + chmod 660 /dev/kfd 2>/dev/null || true msg_info "AMD ROCm compute device configured" fi diff --git a/tools/addon/homebrew.sh b/tools/addon/homebrew.sh new file mode 100644 index 000000000..16b114b7a --- /dev/null +++ b/tools/addon/homebrew.sh @@ -0,0 +1,173 @@ +#!/usr/bin/env bash + +# Copyright (c) 2021-2026 community-scripts ORG +# Author: MorganCSIT | MickLesk (CanbiZ) +# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE +# Source: https://brew.sh | Github: https://github.com/Homebrew/brew + +if ! command -v curl &>/dev/null; then + printf "\r\e[2K%b" '\033[93m Setup Source \033[m' >&2 + apt-get update >/dev/null 2>&1 + apt-get install -y curl >/dev/null 2>&1 +fi +source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/core.func) +source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/tools.func) +source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/error_handler.func) +source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/api.func) 2>/dev/null || true + +# Enable error handling +set -Eeuo pipefail +trap 'error_handler' ERR +load_functions +init_tool_telemetry "" "addon" + +# ============================================================================== +# CONFIGURATION +# ============================================================================== +VERBOSE=${var_verbose:-no} +APP="homebrew" +APP_TYPE="tools" +INSTALL_PATH="/home/linuxbrew/.linuxbrew" + +# ============================================================================== +# OS DETECTION +# ============================================================================== +if [[ -f "/etc/alpine-release" ]]; then + echo -e "${CROSS} Alpine is not supported by Homebrew. Exiting." + exit 1 +elif grep -qE 'ID=debian|ID=ubuntu' /etc/os-release; then + OS="Debian" +else + echo -e "${CROSS} Unsupported OS detected. Exiting." + exit 1 +fi + +# ============================================================================== +# UNINSTALL +# ============================================================================== +function uninstall() { + msg_info "Uninstalling Homebrew" + + BREW_USER=$(awk -F: '$3 >= 1000 && $3 < 65534 { print $1; exit }' /etc/passwd) + if [[ -n "$BREW_USER" ]]; then + BREW_USER_HOME=$(getent passwd "$BREW_USER" | cut -d: -f6) + for rc_file in "$BREW_USER_HOME/.bashrc" "$BREW_USER_HOME/.profile"; do + if [[ -f "$rc_file" ]]; then + sed -i '/# Homebrew (Linuxbrew)/,/^fi$/d' "$rc_file" + fi + done + fi + + rm -rf /home/linuxbrew + rm -f /etc/profile.d/homebrew.sh + groupdel linuxbrew &>/dev/null || true + + msg_ok "Homebrew has been uninstalled" +} + +# ============================================================================== +# INSTALL +# ============================================================================== +function install() { + msg_info "Detecting Non-Root User" + BREW_USER=$(awk -F: '$3 >= 1000 && $3 < 65534 { print $1; exit }' /etc/passwd) + if [[ -z "$BREW_USER" ]]; then + msg_warn "No non-root user found (uid >= 1000). Homebrew cannot run as root." + read -r -p "${TAB}Create a 'brew' user automatically? (y/N): " create_user_prompt + if [[ "${create_user_prompt,,}" =~ ^(y|yes)$ ]]; then + msg_info "Creating user 'brew'" + useradd -m -s /bin/bash brew + BREW_USER="brew" + msg_ok "Created user 'brew'" + else + msg_error "Cannot install Homebrew without a non-root user. Exiting." + exit 1 + fi + fi + msg_ok "Detected User: $BREW_USER" + + msg_info "Installing Dependencies" + $STD apt update + $STD apt install -y build-essential git file procps + msg_ok "Installed Dependencies" + + msg_info "Setting Up Homebrew Prefix" + export PATH="/usr/sbin:$PATH" + groupadd -f linuxbrew + mkdir -p /home/linuxbrew/.linuxbrew + chown -R "$BREW_USER":linuxbrew /home/linuxbrew + chmod 2775 /home/linuxbrew + chmod 2775 /home/linuxbrew/.linuxbrew + usermod -aG linuxbrew "$BREW_USER" + msg_ok "Set Up Homebrew Prefix" + + msg_info "Installing Homebrew" + $STD su - "$BREW_USER" -c 'NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"' + msg_ok "Installed Homebrew" + + msg_info "Configuring Shell Integration" + cat <<'EOF' >/etc/profile.d/homebrew.sh +#!/bin/bash +if [ -d "/home/linuxbrew/.linuxbrew" ]; then + eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" +fi +EOF + chmod +x /etc/profile.d/homebrew.sh + + BREW_USER_HOME=$(getent passwd "$BREW_USER" | cut -d: -f6) + BREW_SHELL_BLOCK='\n# Homebrew (Linuxbrew)\nif [ -d "/home/linuxbrew/.linuxbrew" ]; then\n eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"\nfi' + for rc_file in "$BREW_USER_HOME/.bashrc" "$BREW_USER_HOME/.profile"; do + if ! grep -q 'linuxbrew' "$rc_file" 2>/dev/null; then + echo -e "$BREW_SHELL_BLOCK" >>"$rc_file" + fi + done + msg_ok "Configured Shell Integration" + + msg_info "Verifying Installation" + $STD su - "$BREW_USER" -c 'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" && brew --version' + msg_ok "Homebrew Verified" + + echo "" + msg_ok "Homebrew installed successfully" + msg_ok "Ready for user: ${BL}${BREW_USER}${CL}" + echo "" + echo -e "${TAB}${INFO} Usage: Switch to the brew user with a login shell:" + echo -e "${TAB} ${BL}su - ${BREW_USER}${CL}" + echo -e "${TAB} Then run: ${BL}brew install ${CL}" + echo -e "${TAB} Update with: ${BL}brew update${CL}" +} + +# ============================================================================== +# MAIN +# ============================================================================== +header_info + +if [[ -d "$INSTALL_PATH" ]]; then + msg_warn "Homebrew is already installed." + echo "" + + read -r -p "${TAB}Uninstall Homebrew? (y/N): " uninstall_prompt + if [[ "${uninstall_prompt,,}" =~ ^(y|yes)$ ]]; then + uninstall + exit 0 + fi + + msg_warn "No action selected. Exiting." + exit 0 +fi + +# Fresh installation +msg_warn "Homebrew is not installed." +echo "" +echo -e "${TAB}${INFO} This will install:" +echo -e "${TAB} - Homebrew (Linuxbrew) package manager" +echo -e "${TAB} - Shell integration for the detected non-root user" +echo "" + +read -r -p "${TAB}Install Homebrew? (y/N): " install_prompt +if [[ "${install_prompt,,}" =~ ^(y|yes)$ ]]; then + install +else + msg_warn "Installation cancelled. Exiting." + exit 0 +fi diff --git a/tools/addon/runtipi.sh b/tools/addon/runtipi.sh index 064605752..d4ba9bddb 100644 --- a/tools/addon/runtipi.sh +++ b/tools/addon/runtipi.sh @@ -150,7 +150,7 @@ function install() { curl -fsSL "https://raw.githubusercontent.com/runtipi/runtipi/master/scripts/install.sh" -o "install.sh" chmod +x install.sh $STD ./install.sh - chmod 666 /opt/runtipi/state/settings.json 2>/dev/null || true + chmod 660 /opt/runtipi/state/settings.json 2>/dev/null || true rm -f /opt/install.sh msg_ok "Installed ${APP}"