Got the terminal working
This commit is contained in:
90
package-lock.json
generated
90
package-lock.json
generated
@@ -16,13 +16,20 @@
|
|||||||
"@trpc/react-query": "^11.0.0",
|
"@trpc/react-query": "^11.0.0",
|
||||||
"@trpc/server": "^11.0.0",
|
"@trpc/server": "^11.0.0",
|
||||||
"@types/ws": "^8.18.1",
|
"@types/ws": "^8.18.1",
|
||||||
|
"@xterm/addon-attach": "^0.11.0",
|
||||||
|
"@xterm/addon-fit": "^0.10.0",
|
||||||
|
"@xterm/addon-web-links": "^0.11.0",
|
||||||
|
"@xterm/xterm": "^5.5.0",
|
||||||
"next": "^15.2.3",
|
"next": "^15.2.3",
|
||||||
|
"node-pty": "^1.0.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"server-only": "^0.0.1",
|
"server-only": "^0.0.1",
|
||||||
"simple-git": "^3.28.0",
|
"simple-git": "^3.28.0",
|
||||||
|
"strip-ansi": "^7.1.2",
|
||||||
"superjson": "^2.2.1",
|
"superjson": "^2.2.1",
|
||||||
"ws": "^8.18.3",
|
"ws": "^8.18.3",
|
||||||
|
"xterm": "^5.3.0",
|
||||||
"zod": "^3.24.2"
|
"zod": "^3.24.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -2115,6 +2122,39 @@
|
|||||||
"win32"
|
"win32"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"node_modules/@xterm/addon-attach": {
|
||||||
|
"version": "0.11.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@xterm/addon-attach/-/addon-attach-0.11.0.tgz",
|
||||||
|
"integrity": "sha512-JboCN0QAY6ZLY/SSB/Zl2cQ5zW1Eh4X3fH7BnuR1NB7xGRhzbqU2Npmpiw/3zFlxDaU88vtKzok44JKi2L2V2Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@xterm/xterm": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@xterm/addon-fit": {
|
||||||
|
"version": "0.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.10.0.tgz",
|
||||||
|
"integrity": "sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@xterm/xterm": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@xterm/addon-web-links": {
|
||||||
|
"version": "0.11.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@xterm/addon-web-links/-/addon-web-links-0.11.0.tgz",
|
||||||
|
"integrity": "sha512-nIHQ38pQI+a5kXnRaTgwqSHnX7KE6+4SVoceompgHL26unAxdfP6IPqUTSYPQgSwM56hsElfoNrrW5V7BUED/Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@xterm/xterm": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@xterm/xterm": {
|
||||||
|
"version": "5.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz",
|
||||||
|
"integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/acorn": {
|
"node_modules/acorn": {
|
||||||
"version": "8.15.0",
|
"version": "8.15.0",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||||
@@ -2155,6 +2195,18 @@
|
|||||||
"url": "https://github.com/sponsors/epoberezkin"
|
"url": "https://github.com/sponsors/epoberezkin"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ansi-regex": {
|
||||||
|
"version": "6.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
|
||||||
|
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ansi-styles": {
|
"node_modules/ansi-styles": {
|
||||||
"version": "4.3.0",
|
"version": "4.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||||
@@ -5039,6 +5091,12 @@
|
|||||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/nan": {
|
||||||
|
"version": "2.23.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/nan/-/nan-2.23.0.tgz",
|
||||||
|
"integrity": "sha512-1UxuyYGdoQHcGg87Lkqm3FzefucTa0NAiOcuRsDmysep3c1LVCRK2krrUDafMWtjSG04htvAmvg96+SDknOmgQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.11",
|
"version": "3.3.11",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||||
@@ -5167,6 +5225,16 @@
|
|||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/node-pty": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-wtBMWWS7dFZm/VgqElrTvtfMq4GzJ6+edFI0Y0zyzygUSZMgZdraDUMUhCIvkjhJjme15qWmbyJbtAx4ot4uZA==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"nan": "^2.17.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/nypm": {
|
"node_modules/nypm": {
|
||||||
"version": "0.6.1",
|
"version": "0.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.1.tgz",
|
||||||
@@ -6320,6 +6388,21 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/strip-ansi": {
|
||||||
|
"version": "7.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
|
||||||
|
"integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-regex": "^6.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/strip-bom": {
|
"node_modules/strip-bom": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
|
||||||
@@ -6877,6 +6960,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/xterm": {
|
||||||
|
"version": "5.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/xterm/-/xterm-5.3.0.tgz",
|
||||||
|
"integrity": "sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg==",
|
||||||
|
"deprecated": "This package is now deprecated. Move to @xterm/xterm instead.",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/yallist": {
|
"node_modules/yallist": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
|
||||||
|
|||||||
@@ -29,13 +29,20 @@
|
|||||||
"@trpc/react-query": "^11.0.0",
|
"@trpc/react-query": "^11.0.0",
|
||||||
"@trpc/server": "^11.0.0",
|
"@trpc/server": "^11.0.0",
|
||||||
"@types/ws": "^8.18.1",
|
"@types/ws": "^8.18.1",
|
||||||
|
"@xterm/addon-attach": "^0.11.0",
|
||||||
|
"@xterm/addon-fit": "^0.10.0",
|
||||||
|
"@xterm/addon-web-links": "^0.11.0",
|
||||||
|
"@xterm/xterm": "^5.5.0",
|
||||||
"next": "^15.2.3",
|
"next": "^15.2.3",
|
||||||
|
"node-pty": "^1.0.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"server-only": "^0.0.1",
|
"server-only": "^0.0.1",
|
||||||
"simple-git": "^3.28.0",
|
"simple-git": "^3.28.0",
|
||||||
|
"strip-ansi": "^7.1.2",
|
||||||
"superjson": "^2.2.1",
|
"superjson": "^2.2.1",
|
||||||
"ws": "^8.18.3",
|
"ws": "^8.18.3",
|
||||||
|
"xterm": "^5.3.0",
|
||||||
"zod": "^3.24.2"
|
"zod": "^3.24.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
801
scripts/core/build.func
Executable file
801
scripts/core/build.func
Executable file
@@ -0,0 +1,801 @@
|
|||||||
|
# Copyright (c) 2021-2025 michelroegl-brunner
|
||||||
|
# Author: michelroegl-brunner
|
||||||
|
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||||
|
|
||||||
|
variables() {
|
||||||
|
NSAPP=$(echo "${APP,,}" | tr -d ' ') # This function sets the NSAPP variable by converting the value of the APP variable to lowercase and removing any spaces.
|
||||||
|
var_install="${NSAPP}-install" # sets the var_install variable by appending "-install" to the value of NSAPP.
|
||||||
|
INTEGER='^[0-9]+([.][0-9]+)?$' # it defines the INTEGER regular expression pattern.
|
||||||
|
PVEHOST_NAME=$(hostname) # gets the Proxmox Hostname and sets it to Uppercase
|
||||||
|
METHOD="default" # sets the METHOD variable to "default", used for the API call.
|
||||||
|
CT_TYPE=${var_unprivileged:-$CT_TYPE}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
source "$(dirname "${BASH_SOURCE[0]}")/core.func"
|
||||||
|
|
||||||
|
# This function enables error handling in the script by setting options and defining a trap for the ERR signal.
|
||||||
|
catch_errors() {
|
||||||
|
set -Eeo pipefail
|
||||||
|
trap 'error_handler $LINENO "$BASH_COMMAND"' ERR
|
||||||
|
}
|
||||||
|
|
||||||
|
# This function is called when an error occurs. It receives the exit code, line number, and command that caused the error, and displays an error message.
|
||||||
|
error_handler() {
|
||||||
|
|
||||||
|
printf "\e[?25h"
|
||||||
|
local exit_code="$?"
|
||||||
|
local line_number="$1"
|
||||||
|
local command="$2"
|
||||||
|
local error_message="${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}"
|
||||||
|
|
||||||
|
echo -e "\n$error_message\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if the shell is using bash
|
||||||
|
shell_check() {
|
||||||
|
if [[ "$(basename "$SHELL")" != "bash" ]]; then
|
||||||
|
clear
|
||||||
|
msg_error "Your default shell is currently not set to Bash. To use these scripts, please switch to the Bash shell."
|
||||||
|
echo -e "\nExiting..."
|
||||||
|
sleep 2
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Run as root only
|
||||||
|
root_check() {
|
||||||
|
if [[ "$(id -u)" -ne 0 || $(ps -o comm= -p $PPID) == "sudo" ]]; then
|
||||||
|
clear
|
||||||
|
msg_error "Please run this script as root."
|
||||||
|
echo -e "\nExiting..."
|
||||||
|
sleep 2
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# This function checks the version of Proxmox Virtual Environment (PVE) and exits if the version is not supported.
|
||||||
|
# Supported: Proxmox VE 8.0.x – 8.9.x and 9.0 (NOT 9.1+)
|
||||||
|
pve_check() {
|
||||||
|
local PVE_VER
|
||||||
|
PVE_VER="$(pveversion | awk -F'/' '{print $2}' | awk -F'-' '{print $1}')"
|
||||||
|
|
||||||
|
# Check for Proxmox VE 8.x: allow 8.0–8.9
|
||||||
|
if [[ "$PVE_VER" =~ ^8\.([0-9]+) ]]; then
|
||||||
|
local MINOR="${BASH_REMATCH[1]}"
|
||||||
|
if ((MINOR < 0 || MINOR > 9)); then
|
||||||
|
msg_error "This version of Proxmox VE is not supported."
|
||||||
|
msg_error "Supported: Proxmox VE version 8.0 – 8.9"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check for Proxmox VE 9.x: allow ONLY 9.0
|
||||||
|
if [[ "$PVE_VER" =~ ^9\.([0-9]+) ]]; then
|
||||||
|
local MINOR="${BASH_REMATCH[1]}"
|
||||||
|
if ((MINOR != 0)); then
|
||||||
|
msg_error "This version of Proxmox VE is not yet supported."
|
||||||
|
msg_error "Supported: Proxmox VE version 9.0"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# All other unsupported versions
|
||||||
|
msg_error "This version of Proxmox VE is not supported."
|
||||||
|
msg_error "Supported versions: Proxmox VE 8.0 – 8.x or 9.0"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# When a node is running tens of containers, it's possible to exceed the kernel's cryptographic key storage allocations.
|
||||||
|
# These are tuneable, so verify if the currently deployment is approaching the limits, advise the user on how to tune the limits, and exit the script.
|
||||||
|
# https://cleveruptime.com/docs/files/proc-key-users | https://docs.kernel.org/security/keys/core.html
|
||||||
|
maxkeys_check() {
|
||||||
|
# Read kernel parameters
|
||||||
|
per_user_maxkeys=$(cat /proc/sys/kernel/keys/maxkeys 2>/dev/null || echo 0)
|
||||||
|
per_user_maxbytes=$(cat /proc/sys/kernel/keys/maxbytes 2>/dev/null || echo 0)
|
||||||
|
|
||||||
|
# Exit if kernel parameters are unavailable
|
||||||
|
if [[ "$per_user_maxkeys" -eq 0 || "$per_user_maxbytes" -eq 0 ]]; then
|
||||||
|
echo -e "${CROSS}${RD} Error: Unable to read kernel parameters. Ensure proper permissions.${CL}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Fetch key usage for user ID 100000 (typical for containers)
|
||||||
|
used_lxc_keys=$(awk '/100000:/ {print $2}' /proc/key-users 2>/dev/null || echo 0)
|
||||||
|
used_lxc_bytes=$(awk '/100000:/ {split($5, a, "/"); print a[1]}' /proc/key-users 2>/dev/null || echo 0)
|
||||||
|
|
||||||
|
# Calculate thresholds and suggested new limits
|
||||||
|
threshold_keys=$((per_user_maxkeys - 100))
|
||||||
|
threshold_bytes=$((per_user_maxbytes - 1000))
|
||||||
|
new_limit_keys=$((per_user_maxkeys * 2))
|
||||||
|
new_limit_bytes=$((per_user_maxbytes * 2))
|
||||||
|
|
||||||
|
# Check if key or byte usage is near limits
|
||||||
|
failure=0
|
||||||
|
if [[ "$used_lxc_keys" -gt "$threshold_keys" ]]; then
|
||||||
|
echo -e "${CROSS}${RD} Warning: Key usage is near the limit (${used_lxc_keys}/${per_user_maxkeys}).${CL}"
|
||||||
|
echo -e "${INFO} Suggested action: Set ${GN}kernel.keys.maxkeys=${new_limit_keys}${CL} in ${BOLD}/etc/sysctl.d/98-community-scripts.conf${CL}."
|
||||||
|
failure=1
|
||||||
|
fi
|
||||||
|
if [[ "$used_lxc_bytes" -gt "$threshold_bytes" ]]; then
|
||||||
|
echo -e "${CROSS}${RD} Warning: Key byte usage is near the limit (${used_lxc_bytes}/${per_user_maxbytes}).${CL}"
|
||||||
|
echo -e "${INFO} Suggested action: Set ${GN}kernel.keys.maxbytes=${new_limit_bytes}${CL} in ${BOLD}/etc/sysctl.d/98-community-scripts.conf${CL}."
|
||||||
|
failure=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Provide next steps if issues are detected
|
||||||
|
if [[ "$failure" -eq 1 ]]; then
|
||||||
|
echo -e "${INFO} To apply changes, run: ${BOLD}service procps force-reload${CL}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${CM}${GN} All kernel key limits are within safe thresholds.${CL}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# This function checks the system architecture and exits if it's not "amd64".
|
||||||
|
arch_check() {
|
||||||
|
if [ "$(dpkg --print-architecture)" != "amd64" ]; then
|
||||||
|
echo -e "\n ${INFO}${YWB}This script will not work with PiMox! \n"
|
||||||
|
echo -e "\n ${YWB}Visit https://github.com/asylumexp/Proxmox for ARM64 support. \n"
|
||||||
|
echo -e "Exiting..."
|
||||||
|
sleep 2
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to get the current IP address based on the distribution
|
||||||
|
get_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}')
|
||||||
|
# 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)
|
||||||
|
else
|
||||||
|
CURRENT_IP="Unknown"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
echo "$CURRENT_IP"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to update the IP address in the MOTD file
|
||||||
|
update_motd_ip() {
|
||||||
|
MOTD_FILE="/etc/motd"
|
||||||
|
|
||||||
|
if [ -f "$MOTD_FILE" ]; then
|
||||||
|
# Remove existing IP Address lines to prevent duplication
|
||||||
|
sed -i '/IP Address:/d' "$MOTD_FILE"
|
||||||
|
|
||||||
|
IP=$(get_current_ip)
|
||||||
|
# Add the new IP address
|
||||||
|
echo -e "${TAB}${NETWORK}${YW} IP Address: ${GN}${IP}${CL}" >>"$MOTD_FILE"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# This function checks if the script is running through SSH and prompts the user to confirm if they want to proceed or exit.
|
||||||
|
ssh_check() {
|
||||||
|
if [ -n "${SSH_CLIENT:+x}" ]; then
|
||||||
|
if whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "SSH DETECTED" --yesno "It's advisable to utilize the Proxmox shell rather than SSH, as there may be potential complications with variable retrieval. Proceed using SSH?" 10 72; then
|
||||||
|
whiptail --backtitle "Proxmox VE Helper Scripts" --msgbox --title "Proceed using SSH" "You've chosen to proceed using SSH. If any issues arise, please run the script in the Proxmox shell before creating a repository issue." 10 72
|
||||||
|
else
|
||||||
|
clear
|
||||||
|
echo "Exiting due to SSH usage. Please consider using the Proxmox shell."
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
base_settings() {
|
||||||
|
# Default Settings
|
||||||
|
CT_TYPE=${var_unprivileged:-"1"}
|
||||||
|
DISK_SIZE=${var_disk:-"4"}
|
||||||
|
CORE_COUNT=${var_cpu:-"1"}
|
||||||
|
RAM_SIZE=${var_ram:-"1024"}
|
||||||
|
VERBOSE=${var_verbose:-"${1:-no}"}
|
||||||
|
PW=${var_pw:-""}
|
||||||
|
CT_ID=${var_ctid:-$NEXTID}
|
||||||
|
HN=${var_hostname:-$NSAPP}
|
||||||
|
BRG=${var_brg:-"vmbr0"}
|
||||||
|
NET=${var_net:-"dhcp"}
|
||||||
|
IPV6_METHOD=${var_ipv6_method:-"none"}
|
||||||
|
IPV6_STATIC=${var_ipv6_static:-""}
|
||||||
|
GATE=${var_gateway:-""}
|
||||||
|
APT_CACHER=${var_apt_cacher:-""}
|
||||||
|
APT_CACHER_IP=${var_apt_cacher_ip:-""}
|
||||||
|
MTU=${var_mtu:-""}
|
||||||
|
SD=${var_storage:-""}
|
||||||
|
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:-""}
|
||||||
|
TAGS="community-script;${var_tags:-}"
|
||||||
|
ENABLE_FUSE=${var_fuse:-"${1:-no}"}
|
||||||
|
ENABLE_TUN=${var_tun:-"${1:-no}"}
|
||||||
|
|
||||||
|
# Since these 2 are only defined outside of default_settings function, we add a temporary fallback. TODO: To align everything, we should add these as constant variables (e.g. OSTYPE and OSVERSION), but that would currently require updating the default_settings function for all existing scripts
|
||||||
|
if [ -z "$var_os" ]; then
|
||||||
|
var_os="debian"
|
||||||
|
fi
|
||||||
|
if [ -z "$var_version" ]; then
|
||||||
|
var_version="12"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
write_config() {
|
||||||
|
mkdir -p /opt/community-scripts
|
||||||
|
# This function writes the configuration to a file.
|
||||||
|
if whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "Write configfile" --yesno "Do you want to write the selections to a config file?" 10 60; then
|
||||||
|
FILEPATH="/opt/community-scripts/${NSAPP}.conf"
|
||||||
|
[[ "$GATE" =~ ",gw=" ]] && local GATE="${GATE##,gw=}"
|
||||||
|
|
||||||
|
# Strip prefixes from parameters for config file storage
|
||||||
|
local SD_VALUE="${SD}"
|
||||||
|
local NS_VALUE="${NS}"
|
||||||
|
local MAC_VALUE="${MAC}"
|
||||||
|
local VLAN_VALUE="${VLAN}"
|
||||||
|
[[ "$SD" =~ ^-searchdomain= ]] && SD_VALUE="${SD#-searchdomain=}"
|
||||||
|
[[ "$NS" =~ ^-nameserver= ]] && NS_VALUE="${NS#-nameserver=}"
|
||||||
|
[[ "$MAC" =~ ^,hwaddr= ]] && MAC_VALUE="${MAC#,hwaddr=}"
|
||||||
|
[[ "$VLAN" =~ ^,tag= ]] && VLAN_VALUE="${VLAN#,tag=}"
|
||||||
|
|
||||||
|
if [[ ! -f $FILEPATH ]]; then
|
||||||
|
cat <<EOF >"$FILEPATH"
|
||||||
|
# ${NSAPP} Configuration File
|
||||||
|
# Generated on $(date)
|
||||||
|
|
||||||
|
CT_TYPE="${CT_TYPE}"
|
||||||
|
DISK_SIZE="${DISK_SIZE}"
|
||||||
|
CORE_COUNT="${CORE_COUNT}"
|
||||||
|
RAM_SIZE="${RAM_SIZE}"
|
||||||
|
VERBOSE="${VERBOSE}"
|
||||||
|
PW="${PW##-password }"
|
||||||
|
#CT_ID=$NEXTID
|
||||||
|
HN="${HN}"
|
||||||
|
BRG="${BRG}"
|
||||||
|
NET="${NET}"
|
||||||
|
IPV6_METHOD="${IPV6_METHOD:-none}"
|
||||||
|
# Set this only if using "IPV6_METHOD=static"
|
||||||
|
#IPV6STATIC="fd00::1234/64"
|
||||||
|
|
||||||
|
GATE="${GATE:-none}"
|
||||||
|
APT_CACHER_IP="${APT_CACHER_IP:-none}"
|
||||||
|
MTU="${MTU:-1500}"
|
||||||
|
SD="${SD_VALUE:-none}"
|
||||||
|
NS="${NS_VALUE:-none}"
|
||||||
|
MAC="${MAC_VALUE:-none}"
|
||||||
|
VLAN="${VLAN_VALUE:-none}"
|
||||||
|
SSH="${SSH}"
|
||||||
|
SSH_AUTHORIZED_KEY="${SSH_AUTHORIZED_KEY}"
|
||||||
|
TAGS="${TAGS:-none}"
|
||||||
|
ENABLE_FUSE="$ENABLE_FUSE"
|
||||||
|
ENABLE_TUN="$ENABLE_TUN"
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo -e "${INFO}${BOLD}${GN}Writing configuration to ${FILEPATH}${CL}"
|
||||||
|
else
|
||||||
|
echo -e "${INFO}${BOLD}${RD}Configuration file already exists at ${FILEPATH}${CL}"
|
||||||
|
if whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "Overwrite configfile" --yesno "Do you want to overwrite the existing config file?" 10 60; then
|
||||||
|
rm -f "$FILEPATH"
|
||||||
|
cat <<EOF >"$FILEPATH"
|
||||||
|
# ${NSAPP} Configuration File
|
||||||
|
# Generated on $(date)
|
||||||
|
|
||||||
|
CT_TYPE="${CT_TYPE}"
|
||||||
|
DISK_SIZE="${DISK_SIZE}"
|
||||||
|
CORE_COUNT="${CORE_COUNT}"
|
||||||
|
RAM_SIZE="${RAM_SIZE}"
|
||||||
|
VERBOSE="${VERBOSE}"
|
||||||
|
PW="${PW##-password }"
|
||||||
|
#CT_ID=$NEXTID
|
||||||
|
HN="${HN}"
|
||||||
|
BRG="${BRG}"
|
||||||
|
NET="${NET}"
|
||||||
|
IPV6_METHOD="${IPV6_METHOD:-none}"
|
||||||
|
|
||||||
|
# Set this only if using "IPV6_METHOD=static"
|
||||||
|
#IPV6STATIC="fd00::1234/64"
|
||||||
|
|
||||||
|
GATE="${GATE:-none}"
|
||||||
|
APT_CACHER_IP="${APT_CACHER_IP:-none}"
|
||||||
|
MTU="${MTU:-1500}"
|
||||||
|
SD="${SD_VALUE:-none}"
|
||||||
|
NS="${NS_VALUE:-none}"
|
||||||
|
MAC="${MAC_VALUE:-none}"
|
||||||
|
VLAN="${VLAN_VALUE:-none}"
|
||||||
|
SSH="${SSH}"
|
||||||
|
SSH_AUTHORIZED_KEY="${SSH_AUTHORIZED_KEY}"
|
||||||
|
TAGS="${TAGS:-none}"
|
||||||
|
ENABLE_FUSE="$ENABLE_FUSE"
|
||||||
|
ENABLE_TUN="$ENABLE_TUN"
|
||||||
|
EOF
|
||||||
|
echo -e "${INFO}${BOLD}${GN}Writing configuration to ${FILEPATH}${CL}"
|
||||||
|
else
|
||||||
|
echo -e "${INFO}${BOLD}${RD}Configuration file not overwritten${CL}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# This function displays the default values for various settings.
|
||||||
|
echo_default() {
|
||||||
|
# Convert CT_TYPE to description
|
||||||
|
CT_TYPE_DESC="Unprivileged"
|
||||||
|
if [ "$CT_TYPE" -eq 0 ]; then
|
||||||
|
CT_TYPE_DESC="Privileged"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Output the selected values with icons
|
||||||
|
echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}${CT_ID}${CL}"
|
||||||
|
echo -e "${OS}${BOLD}${DGN}Operating System: ${BGN}$var_os ($var_version)${CL}"
|
||||||
|
echo -e "${CONTAINERTYPE}${BOLD}${DGN}Container Type: ${BGN}$CT_TYPE_DESC${CL}"
|
||||||
|
echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE} GB${CL}"
|
||||||
|
echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}${CORE_COUNT}${CL}"
|
||||||
|
echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE} MiB${CL}"
|
||||||
|
if [ "$VERBOSE" == "yes" ]; then
|
||||||
|
echo -e "${SEARCH}${BOLD}${DGN}Verbose Mode: ${BGN}Enabled${CL}"
|
||||||
|
fi
|
||||||
|
echo -e "${CREATING}${BOLD}${BL}Creating a ${APP} LXC using the above default settings${CL}"
|
||||||
|
echo -e " "
|
||||||
|
}
|
||||||
|
|
||||||
|
# This function is called when the user decides to exit the script. It clears the screen and displays an exit message.
|
||||||
|
exit_script() {
|
||||||
|
clear
|
||||||
|
echo -e "\n${CROSS}${RD}User exited script${CL}\n"
|
||||||
|
exit
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
install_script() {
|
||||||
|
pve_check
|
||||||
|
shell_check
|
||||||
|
root_check
|
||||||
|
arch_check
|
||||||
|
#ssh_check
|
||||||
|
maxkeys_check
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if systemctl is-active -q ping-instances.service; then
|
||||||
|
systemctl -q stop ping-instances.service
|
||||||
|
fi
|
||||||
|
NEXTID=$(pvesh get /cluster/nextid)
|
||||||
|
timezone=$(cat /etc/timezone)
|
||||||
|
#header_info
|
||||||
|
echo "TEST"
|
||||||
|
while true; do
|
||||||
|
|
||||||
|
TMP_CHOICE=$(whiptail --backtitle "Proxmox VE Helper Scripts" \
|
||||||
|
--title "SETTINGS" \
|
||||||
|
--menu "Choose an option:" 20 60 6 \
|
||||||
|
"1" "Default Settings" \
|
||||||
|
"2" "Default Settings (with verbose)" \
|
||||||
|
"3" "Advanced Settings" \
|
||||||
|
"4" "Exit" \
|
||||||
|
--default-item "1" 3>&1 1>&2 2>&3) || true
|
||||||
|
|
||||||
|
if [ -z "$TMP_CHOICE" ]; then
|
||||||
|
echo -e "\n${CROSS}${RD}Menu canceled. Exiting script.${CL}\n"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
CHOICE="$TMP_CHOICE"
|
||||||
|
|
||||||
|
case $CHOICE in
|
||||||
|
1)
|
||||||
|
header_info
|
||||||
|
echo -e "${DEFAULT}${BOLD}${BL}Using Default Settings on node $PVEHOST_NAME${CL}"
|
||||||
|
VERBOSE="no"
|
||||||
|
METHOD="default"
|
||||||
|
base_settings "$VERBOSE"
|
||||||
|
echo_default
|
||||||
|
break
|
||||||
|
;;
|
||||||
|
2)
|
||||||
|
header_info
|
||||||
|
echo -e "${DEFAULT}${BOLD}${BL}Using Default Settings on node $PVEHOST_NAME (${VERBOSE_CROPPED}Verbose)${CL}"
|
||||||
|
VERBOSE="yes"
|
||||||
|
METHOD="default"
|
||||||
|
base_settings "$VERBOSE"
|
||||||
|
echo_default
|
||||||
|
break
|
||||||
|
;;
|
||||||
|
3)
|
||||||
|
header_info
|
||||||
|
echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Settings on node $PVEHOST_NAME${CL}"
|
||||||
|
METHOD="advanced"
|
||||||
|
base_settings
|
||||||
|
advanced_settings
|
||||||
|
break
|
||||||
|
;;
|
||||||
|
4)
|
||||||
|
echo -e "\n${CROSS}${RD}Script terminated. Have a great day!${CL}\n"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo -e "\n${CROSS}${RD}Invalid option, please try again.${CL}\n"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
check_container_resources() {
|
||||||
|
# Check actual RAM & Cores
|
||||||
|
current_ram=$(free -m | awk 'NR==2{print $2}')
|
||||||
|
current_cpu=$(nproc)
|
||||||
|
|
||||||
|
# Check whether the current RAM is less than the required RAM or the CPU cores are less than required
|
||||||
|
if [[ "$current_ram" -lt "$var_ram" ]] || [[ "$current_cpu" -lt "$var_cpu" ]]; then
|
||||||
|
echo -e "\n${INFO}${HOLD} ${GN}Required: ${var_cpu} CPU, ${var_ram}MB RAM ${CL}| ${RD}Current: ${current_cpu} CPU, ${current_ram}MB RAM${CL}"
|
||||||
|
echo -e "${YWB}Please ensure that the ${APP} LXC is configured with at least ${var_cpu} vCPU and ${var_ram} MB RAM for the build process.${CL}\n"
|
||||||
|
echo -ne "${INFO}${HOLD} May cause data loss! ${INFO} Continue update with under-provisioned LXC? [y/N] "
|
||||||
|
read -r prompt
|
||||||
|
if [[ ! "${prompt,,}" =~ ^(y|yes)$ ]]; then
|
||||||
|
echo -e "${CROSS}${HOLD} ${YWB}Exiting based on user input.${CL}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo -e ""
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check_container_storage() {
|
||||||
|
# Check if the /boot partition is more than 80% full
|
||||||
|
total_size=$(df /boot --output=size | tail -n 1)
|
||||||
|
local used_size=$(df /boot --output=used | tail -n 1)
|
||||||
|
usage=$((100 * used_size / total_size))
|
||||||
|
if ((usage > 80)); then
|
||||||
|
# Prompt the user for confirmation to continue
|
||||||
|
echo -e "${INFO}${HOLD} ${YWB}Warning: Storage is dangerously low (${usage}%).${CL}"
|
||||||
|
echo -ne "Continue anyway? [y/N] "
|
||||||
|
read -r prompt
|
||||||
|
if [[ ! "${prompt,,}" =~ ^(y|yes)$ ]]; then
|
||||||
|
echo -e "${CROSS}${HOLD}${YWB}Exiting based on user input.${CL}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
start() {
|
||||||
|
source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/tools.func)
|
||||||
|
if command -v pveversion >/dev/null 2>&1; then
|
||||||
|
install_script
|
||||||
|
else
|
||||||
|
CHOICE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "${APP} LXC Update/Setting" --menu \
|
||||||
|
"Support/Update functions for ${APP} LXC. Choose an option:" \
|
||||||
|
12 60 3 \
|
||||||
|
"1" "YES (Silent Mode)" \
|
||||||
|
"2" "YES (Verbose Mode)" \
|
||||||
|
"3" "NO (Cancel Update)" --nocancel --default-item "1" 3>&1 1>&2 2>&3)
|
||||||
|
|
||||||
|
case "$CHOICE" in
|
||||||
|
1)
|
||||||
|
VERBOSE="no"
|
||||||
|
set_std_mode
|
||||||
|
;;
|
||||||
|
2)
|
||||||
|
VERBOSE="yes"
|
||||||
|
set_std_mode
|
||||||
|
;;
|
||||||
|
3)
|
||||||
|
clear
|
||||||
|
exit_script
|
||||||
|
exit
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
update_script
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# This function collects user settings and integrates all the collected information.
|
||||||
|
build_container() {
|
||||||
|
# if [ "$VERBOSE" == "yes" ]; then set -x; fi
|
||||||
|
|
||||||
|
NET_STRING="-net0 name=eth0,bridge=$BRG$MAC,ip=$NET$GATE$VLAN$MTU"
|
||||||
|
case "$IPV6_METHOD" in
|
||||||
|
auto) NET_STRING="$NET_STRING,ip6=auto" ;;
|
||||||
|
dhcp) NET_STRING="$NET_STRING,ip6=dhcp" ;;
|
||||||
|
static)
|
||||||
|
NET_STRING="$NET_STRING,ip6=$IPV6_ADDR"
|
||||||
|
[ -n "$IPV6_GATE" ] && NET_STRING="$NET_STRING,gw6=$IPV6_GATE"
|
||||||
|
;;
|
||||||
|
none) ;;
|
||||||
|
esac
|
||||||
|
if [ "$CT_TYPE" == "1" ]; then
|
||||||
|
FEATURES="keyctl=1,nesting=1"
|
||||||
|
else
|
||||||
|
FEATURES="nesting=1"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$ENABLE_FUSE" == "yes" ]; then
|
||||||
|
FEATURES="$FEATURES,fuse=1"
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
TEMP_DIR=$(mktemp -d)
|
||||||
|
pushd "$TEMP_DIR" >/dev/null
|
||||||
|
if [ "$var_os" == "alpine" ]; then
|
||||||
|
export FUNCTIONS_FILE_PATH="$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/alpine-install.func)"
|
||||||
|
else
|
||||||
|
export FUNCTIONS_FILE_PATH="$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/install.func)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
export DIAGNOSTICS="$DIAGNOSTICS"
|
||||||
|
export RANDOM_UUID="$RANDOM_UUID"
|
||||||
|
export CACHER="$APT_CACHER"
|
||||||
|
export CACHER_IP="$APT_CACHER_IP"
|
||||||
|
export tz="$timezone"
|
||||||
|
export APPLICATION="$APP"
|
||||||
|
export app="$NSAPP"
|
||||||
|
export PASSWORD="$PW"
|
||||||
|
export VERBOSE="$VERBOSE"
|
||||||
|
export SSH_ROOT="${SSH}"
|
||||||
|
export SSH_AUTHORIZED_KEY
|
||||||
|
export CTID="$CT_ID"
|
||||||
|
export CTTYPE="$CT_TYPE"
|
||||||
|
export ENABLE_FUSE="$ENABLE_FUSE"
|
||||||
|
export ENABLE_TUN="$ENABLE_TUN"
|
||||||
|
export PCT_OSTYPE="$var_os"
|
||||||
|
export PCT_OSVERSION="$var_version"
|
||||||
|
export PCT_DISK_SIZE="$DISK_SIZE"
|
||||||
|
export PCT_OPTIONS="
|
||||||
|
-features $FEATURES
|
||||||
|
-hostname $HN
|
||||||
|
-tags $TAGS
|
||||||
|
$SD
|
||||||
|
$NS
|
||||||
|
$NET_STRING
|
||||||
|
-onboot 1
|
||||||
|
-cores $CORE_COUNT
|
||||||
|
-memory $RAM_SIZE
|
||||||
|
-unprivileged $CT_TYPE
|
||||||
|
$PW
|
||||||
|
"
|
||||||
|
# This executes create_lxc.sh and creates the container and .conf file
|
||||||
|
bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/create_lxc.sh)" $?
|
||||||
|
|
||||||
|
LXC_CONFIG="/etc/pve/lxc/${CTID}.conf"
|
||||||
|
|
||||||
|
# USB passthrough for privileged LXC (CT_TYPE=0)
|
||||||
|
if [ "$CT_TYPE" == "0" ]; then
|
||||||
|
cat <<EOF >>"$LXC_CONFIG"
|
||||||
|
# USB passthrough
|
||||||
|
lxc.cgroup2.devices.allow: a
|
||||||
|
lxc.cap.drop:
|
||||||
|
lxc.cgroup2.devices.allow: c 188:* rwm
|
||||||
|
lxc.cgroup2.devices.allow: c 189:* rwm
|
||||||
|
lxc.mount.entry: /dev/serial/by-id dev/serial/by-id none bind,optional,create=dir
|
||||||
|
lxc.mount.entry: /dev/ttyUSB0 dev/ttyUSB0 none bind,optional,create=file
|
||||||
|
lxc.mount.entry: /dev/ttyUSB1 dev/ttyUSB1 none bind,optional,create=file
|
||||||
|
lxc.mount.entry: /dev/ttyACM0 dev/ttyACM0 none bind,optional,create=file
|
||||||
|
lxc.mount.entry: /dev/ttyACM1 dev/ttyACM1 none bind,optional,create=file
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
|
||||||
|
# VAAPI passthrough for privileged containers or known apps
|
||||||
|
VAAPI_APPS=(
|
||||||
|
"immich"
|
||||||
|
"Channels"
|
||||||
|
"Emby"
|
||||||
|
"ErsatzTV"
|
||||||
|
"Frigate"
|
||||||
|
"Jellyfin"
|
||||||
|
"Plex"
|
||||||
|
"Scrypted"
|
||||||
|
"Tdarr"
|
||||||
|
"Unmanic"
|
||||||
|
"Ollama"
|
||||||
|
"FileFlows"
|
||||||
|
"Open WebUI"
|
||||||
|
)
|
||||||
|
|
||||||
|
is_vaapi_app=false
|
||||||
|
for vaapi_app in "${VAAPI_APPS[@]}"; do
|
||||||
|
if [[ "$APP" == "$vaapi_app" ]]; then
|
||||||
|
is_vaapi_app=true
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if ([ "$CT_TYPE" == "0" ] || [ "$is_vaapi_app" == "true" ]) &&
|
||||||
|
([[ -e /dev/dri/renderD128 ]] || [[ -e /dev/dri/card0 ]] || [[ -e /dev/fb0 ]]); then
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
msg_custom "⚙️ " "\e[96m" "Configuring VAAPI passthrough for LXC container"
|
||||||
|
if [ "$CT_TYPE" != "0" ]; then
|
||||||
|
msg_custom "⚠️ " "\e[33m" "Container is unprivileged – VAAPI passthrough may not work without additional host configuration (e.g., idmap)."
|
||||||
|
fi
|
||||||
|
msg_custom "ℹ️ " "\e[96m" "VAAPI enables GPU hardware acceleration (e.g., for video transcoding in Jellyfin or Plex)."
|
||||||
|
echo ""
|
||||||
|
read -rp "➤ Automatically mount all available VAAPI devices? [Y/n]: " VAAPI_ALL
|
||||||
|
|
||||||
|
if [[ "$VAAPI_ALL" =~ ^[Yy]$|^$ ]]; then
|
||||||
|
if [ "$CT_TYPE" == "0" ]; then
|
||||||
|
# PRV Container → alles zulässig
|
||||||
|
[[ -e /dev/dri/renderD128 ]] && {
|
||||||
|
echo "lxc.cgroup2.devices.allow: c 226:128 rwm" >>"$LXC_CONFIG"
|
||||||
|
echo "lxc.mount.entry: /dev/dri/renderD128 dev/dri/renderD128 none bind,optional,create=file" >>"$LXC_CONFIG"
|
||||||
|
}
|
||||||
|
[[ -e /dev/dri/card0 ]] && {
|
||||||
|
echo "lxc.cgroup2.devices.allow: c 226:0 rwm" >>"$LXC_CONFIG"
|
||||||
|
echo "lxc.mount.entry: /dev/dri/card0 dev/dri/card0 none bind,optional,create=file" >>"$LXC_CONFIG"
|
||||||
|
}
|
||||||
|
[[ -e /dev/fb0 ]] && {
|
||||||
|
echo "lxc.cgroup2.devices.allow: c 29:0 rwm" >>"$LXC_CONFIG"
|
||||||
|
echo "lxc.mount.entry: /dev/fb0 dev/fb0 none bind,optional,create=file" >>"$LXC_CONFIG"
|
||||||
|
}
|
||||||
|
[[ -d /dev/dri ]] && {
|
||||||
|
echo "lxc.mount.entry: /dev/dri dev/dri none bind,optional,create=dir" >>"$LXC_CONFIG"
|
||||||
|
}
|
||||||
|
else
|
||||||
|
# UNPRV Container → nur devX für UI
|
||||||
|
[[ -e /dev/dri/card0 ]] && echo "dev0: /dev/dri/card0,gid=44" >>"$LXC_CONFIG"
|
||||||
|
[[ -e /dev/dri/card1 ]] && echo "dev0: /dev/dri/card1,gid=44" >>"$LXC_CONFIG"
|
||||||
|
[[ -e /dev/dri/renderD128 ]] && echo "dev1: /dev/dri/renderD128,gid=104" >>"$LXC_CONFIG"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
fi
|
||||||
|
if [ "$CT_TYPE" == "1" ] && [ "$is_vaapi_app" == "true" ]; then
|
||||||
|
if [[ -e /dev/dri/card0 ]]; then
|
||||||
|
echo "dev0: /dev/dri/card0,gid=44" >>"$LXC_CONFIG"
|
||||||
|
elif [[ -e /dev/dri/card1 ]]; then
|
||||||
|
echo "dev0: /dev/dri/card1,gid=44" >>"$LXC_CONFIG"
|
||||||
|
fi
|
||||||
|
if [[ -e /dev/dri/renderD128 ]]; then
|
||||||
|
echo "dev1: /dev/dri/renderD128,gid=104" >>"$LXC_CONFIG"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# TUN device passthrough
|
||||||
|
if [ "$ENABLE_TUN" == "yes" ]; then
|
||||||
|
cat <<EOF >>"$LXC_CONFIG"
|
||||||
|
lxc.cgroup2.devices.allow: c 10:200 rwm
|
||||||
|
lxc.mount.entry: /dev/net/tun dev/net/tun none bind,create=file
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
|
||||||
|
# This starts the container and executes <app>-install.sh
|
||||||
|
msg_info "Starting LXC Container"
|
||||||
|
pct start "$CTID"
|
||||||
|
|
||||||
|
# wait for status 'running'
|
||||||
|
for i in {1..10}; do
|
||||||
|
if pct status "$CTID" | grep -q "status: running"; then
|
||||||
|
msg_ok "Started LXC Container"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
if [ "$i" -eq 10 ]; then
|
||||||
|
msg_error "LXC Container did not reach running state"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$var_os" != "alpine" ]; then
|
||||||
|
msg_info "Waiting for network in LXC container"
|
||||||
|
for i in {1..10}; do
|
||||||
|
# 1. Primary check: ICMP ping (fastest, but may be blocked by ISP/firewall)
|
||||||
|
if pct exec "$CTID" -- ping -c1 -W1 deb.debian.org >/dev/null 2>&1; then
|
||||||
|
msg_ok "Network in LXC is reachable (ping)"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
# Wait and retry if not reachable yet
|
||||||
|
if [ "$i" -lt 10 ]; then
|
||||||
|
msg_warn "No network in LXC yet (try $i/10) – waiting..."
|
||||||
|
sleep 3
|
||||||
|
else
|
||||||
|
# After 10 unsuccessful ping attempts, try HTTP connectivity via wget as fallback
|
||||||
|
msg_warn "Ping failed 10 times. Trying HTTP connectivity check (wget) as fallback..."
|
||||||
|
if pct exec "$CTID" -- wget -q --spider http://deb.debian.org; then
|
||||||
|
msg_ok "Network in LXC is reachable (wget fallback)"
|
||||||
|
else
|
||||||
|
msg_error "No network in LXC after all checks."
|
||||||
|
read -r -p "Set fallback DNS (1.1.1.1/8.8.8.8)? [y/N]: " choice
|
||||||
|
case "$choice" in
|
||||||
|
[yY]*)
|
||||||
|
pct set "$CTID" --nameserver 1.1.1.1
|
||||||
|
pct set "$CTID" --nameserver 8.8.8.8
|
||||||
|
# Final attempt with wget after DNS change
|
||||||
|
if pct exec "$CTID" -- wget -q --spider http://deb.debian.org; then
|
||||||
|
msg_ok "Network reachable after DNS fallback"
|
||||||
|
else
|
||||||
|
msg_error "Still no network/DNS in LXC! Aborting customization."
|
||||||
|
exit_script
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
msg_error "Aborted by user – no DNS fallback set."
|
||||||
|
exit_script
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
msg_info "Customizing LXC Container"
|
||||||
|
: "${tz:=Etc/UTC}"
|
||||||
|
if [ "$var_os" == "alpine" ]; then
|
||||||
|
sleep 3
|
||||||
|
pct exec "$CTID" -- /bin/sh -c 'cat <<EOF >/etc/apk/repositories
|
||||||
|
http://dl-cdn.alpinelinux.org/alpine/latest-stable/main
|
||||||
|
http://dl-cdn.alpinelinux.org/alpine/latest-stable/community
|
||||||
|
EOF'
|
||||||
|
pct exec "$CTID" -- ash -c "apk add bash newt curl openssh nano mc ncurses jq >/dev/null"
|
||||||
|
else
|
||||||
|
sleep 3
|
||||||
|
pct exec "$CTID" -- bash -c "sed -i '/$LANG/ 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 && \
|
||||||
|
export LANG=\$locale_line"
|
||||||
|
|
||||||
|
if [[ -z "${tz:-}" ]]; then
|
||||||
|
tz=$(timedatectl show --property=Timezone --value 2>/dev/null || echo "Etc/UTC")
|
||||||
|
fi
|
||||||
|
if pct exec "$CTID" -- test -e "/usr/share/zoneinfo/$tz"; then
|
||||||
|
pct exec "$CTID" -- bash -c "tz='$tz'; echo \"\$tz\" >/etc/timezone && ln -sf \"/usr/share/zoneinfo/\$tz\" /etc/localtime"
|
||||||
|
else
|
||||||
|
msg_warn "Skipping timezone setup – zone '$tz' not found in container"
|
||||||
|
fi
|
||||||
|
|
||||||
|
pct exec "$CTID" -- bash -c "apt-get update >/dev/null && apt-get install -y sudo curl mc gnupg2 jq >/dev/null"
|
||||||
|
fi
|
||||||
|
msg_ok "Customized LXC Container"
|
||||||
|
|
||||||
|
lxc-attach -n "$CTID" -- bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/install/${var_install}.sh)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# This function sets the description of the container.
|
||||||
|
description() {
|
||||||
|
IP=$(pct exec "$CTID" ip a s dev eth0 | awk '/inet / {print $2}' | cut -d/ -f1)
|
||||||
|
|
||||||
|
# Generate LXC Description
|
||||||
|
DESCRIPTION=$(
|
||||||
|
cat <<EOF
|
||||||
|
<div align='center'>
|
||||||
|
<a href='https://Helper-Scripts.com' target='_blank' rel='noopener noreferrer'>
|
||||||
|
<img src='https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/images/logo-81x112.png' alt='Logo' style='width:81px;height:112px;'/>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<h2 style='font-size: 24px; margin: 20px 0;'>${APP} LXC</h2>
|
||||||
|
|
||||||
|
<p style='margin: 16px 0;'>
|
||||||
|
<a href='https://ko-fi.com/community_scripts' target='_blank' rel='noopener noreferrer'>
|
||||||
|
<img src='https://img.shields.io/badge/☕-Buy us a coffee-blue' alt='spend Coffee' />
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<span style='margin: 0 10px;'>
|
||||||
|
<i class="fa fa-github fa-fw" style="color: #f5f5f5;"></i>
|
||||||
|
<a href='https://github.com/community-scripts/ProxmoxVE' target='_blank' rel='noopener noreferrer' style='text-decoration: none; color: #00617f;'>GitHub</a>
|
||||||
|
</span>
|
||||||
|
<span style='margin: 0 10px;'>
|
||||||
|
<i class="fa fa-comments fa-fw" style="color: #f5f5f5;"></i>
|
||||||
|
<a href='https://github.com/community-scripts/ProxmoxVE/discussions' target='_blank' rel='noopener noreferrer' style='text-decoration: none; color: #00617f;'>Discussions</a>
|
||||||
|
</span>
|
||||||
|
<span style='margin: 0 10px;'>
|
||||||
|
<i class="fa fa-exclamation-circle fa-fw" style="color: #f5f5f5;"></i>
|
||||||
|
<a href='https://github.com/community-scripts/ProxmoxVE/issues' target='_blank' rel='noopener noreferrer' style='text-decoration: none; color: #00617f;'>Issues</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
|
||||||
|
# Set Description in LXC
|
||||||
|
pct set "$CTID" -description "$DESCRIPTION"
|
||||||
|
|
||||||
|
if [[ -f /etc/systemd/system/ping-instances.service ]]; then
|
||||||
|
systemctl start ping-instances.service
|
||||||
|
fi
|
||||||
|
|
||||||
|
}
|
||||||
409
scripts/core/core.func
Normal file
409
scripts/core/core.func
Normal file
@@ -0,0 +1,409 @@
|
|||||||
|
# Copyright (c) 2021-2025 community-scripts ORG
|
||||||
|
# License: MIT | https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/LICENSE
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Loads core utility groups once (colors, formatting, icons, defaults).
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
[[ -n "${_CORE_FUNC_LOADED:-}" ]] && return
|
||||||
|
_CORE_FUNC_LOADED=1
|
||||||
|
|
||||||
|
load_functions() {
|
||||||
|
[[ -n "${__FUNCTIONS_LOADED:-}" ]] && return
|
||||||
|
__FUNCTIONS_LOADED=1
|
||||||
|
color
|
||||||
|
formatting
|
||||||
|
icons
|
||||||
|
default_vars
|
||||||
|
set_std_mode
|
||||||
|
# add more
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Error & Signal Handling – robust, universal, subshell-safe
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
_tool_error_hint() {
|
||||||
|
local cmd="$1"
|
||||||
|
local code="$2"
|
||||||
|
case "$cmd" in
|
||||||
|
curl)
|
||||||
|
case "$code" in
|
||||||
|
6) echo "Curl: Could not resolve host (DNS problem)" ;;
|
||||||
|
7) echo "Curl: Failed to connect to host (connection refused)" ;;
|
||||||
|
22) echo "Curl: HTTP error (404/403 etc)" ;;
|
||||||
|
28) echo "Curl: Operation timeout" ;;
|
||||||
|
*) echo "Curl: Unknown error ($code)" ;;
|
||||||
|
esac
|
||||||
|
;;
|
||||||
|
wget)
|
||||||
|
echo "Wget failed – URL unreachable or permission denied"
|
||||||
|
;;
|
||||||
|
systemctl)
|
||||||
|
echo "Systemd unit failure – check service name and permissions"
|
||||||
|
;;
|
||||||
|
jq)
|
||||||
|
echo "jq parse error – malformed JSON or missing key"
|
||||||
|
;;
|
||||||
|
mariadb | mysql)
|
||||||
|
echo "MySQL/MariaDB command failed – check credentials or DB"
|
||||||
|
;;
|
||||||
|
unzip)
|
||||||
|
echo "unzip failed – corrupt file or missing permission"
|
||||||
|
;;
|
||||||
|
tar)
|
||||||
|
echo "tar failed – invalid format or missing binary"
|
||||||
|
;;
|
||||||
|
node | npm | pnpm | yarn)
|
||||||
|
echo "Node tool failed – check version compatibility or package.json"
|
||||||
|
;;
|
||||||
|
*) echo "" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
catch_errors() {
|
||||||
|
set -Eeuo pipefail
|
||||||
|
trap 'error_handler $LINENO "$BASH_COMMAND"' ERR
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Sets ANSI color codes used for styled terminal output.
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
color() {
|
||||||
|
YW=$(echo "\033[33m")
|
||||||
|
YWB=$'\e[93m'
|
||||||
|
BL=$(echo "\033[36m")
|
||||||
|
RD=$(echo "\033[01;31m")
|
||||||
|
BGN=$(echo "\033[4;92m")
|
||||||
|
GN=$(echo "\033[1;92m")
|
||||||
|
DGN=$(echo "\033[32m")
|
||||||
|
CL=$(echo "\033[m")
|
||||||
|
}
|
||||||
|
|
||||||
|
# Special for spinner and colorized output via printf
|
||||||
|
color_spinner() {
|
||||||
|
CS_YW=$'\033[33m'
|
||||||
|
CS_YWB=$'\033[93m'
|
||||||
|
CS_CL=$'\033[m'
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Defines formatting helpers like tab, bold, and line reset sequences.
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
formatting() {
|
||||||
|
BFR="\\r\\033[K"
|
||||||
|
BOLD=$(echo "\033[1m")
|
||||||
|
HOLD=" "
|
||||||
|
TAB=" "
|
||||||
|
TAB3=" "
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Sets symbolic icons used throughout user feedback and prompts.
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
icons() {
|
||||||
|
CM="${TAB}✔️${TAB}"
|
||||||
|
CROSS="${TAB}✖️${TAB}"
|
||||||
|
DNSOK="✔️ "
|
||||||
|
DNSFAIL="${TAB}✖️${TAB}"
|
||||||
|
INFO="${TAB}💡${TAB}${CL}"
|
||||||
|
OS="${TAB}🖥️${TAB}${CL}"
|
||||||
|
OSVERSION="${TAB}🌟${TAB}${CL}"
|
||||||
|
CONTAINERTYPE="${TAB}📦${TAB}${CL}"
|
||||||
|
DISKSIZE="${TAB}💾${TAB}${CL}"
|
||||||
|
CPUCORE="${TAB}🧠${TAB}${CL}"
|
||||||
|
RAMSIZE="${TAB}🛠️${TAB}${CL}"
|
||||||
|
SEARCH="${TAB}🔍${TAB}${CL}"
|
||||||
|
VERBOSE_CROPPED="🔍${TAB}"
|
||||||
|
VERIFYPW="${TAB}🔐${TAB}${CL}"
|
||||||
|
CONTAINERID="${TAB}🆔${TAB}${CL}"
|
||||||
|
HOSTNAME="${TAB}🏠${TAB}${CL}"
|
||||||
|
BRIDGE="${TAB}🌉${TAB}${CL}"
|
||||||
|
NETWORK="${TAB}📡${TAB}${CL}"
|
||||||
|
GATEWAY="${TAB}🌐${TAB}${CL}"
|
||||||
|
DISABLEIPV6="${TAB}🚫${TAB}${CL}"
|
||||||
|
DEFAULT="${TAB}⚙️${TAB}${CL}"
|
||||||
|
MACADDRESS="${TAB}🔗${TAB}${CL}"
|
||||||
|
VLANTAG="${TAB}🏷️${TAB}${CL}"
|
||||||
|
ROOTSSH="${TAB}🔑${TAB}${CL}"
|
||||||
|
CREATING="${TAB}🚀${TAB}${CL}"
|
||||||
|
ADVANCED="${TAB}🧩${TAB}${CL}"
|
||||||
|
FUSE="${TAB}🗂️${TAB}${CL}"
|
||||||
|
HOURGLASS="${TAB}⏳${TAB}"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Sets default retry and wait variables used for system actions.
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
default_vars() {
|
||||||
|
RETRY_NUM=10
|
||||||
|
RETRY_EVERY=3
|
||||||
|
i=$RETRY_NUM
|
||||||
|
#[[ "${VAR_OS:-}" == "unknown" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Sets default verbose mode for script and os execution.
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
set_std_mode() {
|
||||||
|
if [ "${VERBOSE:-no}" = "yes" ]; then
|
||||||
|
STD=""
|
||||||
|
else
|
||||||
|
STD="silent"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Silent execution function
|
||||||
|
silent() {
|
||||||
|
"$@" >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to download & save header files
|
||||||
|
get_header() {
|
||||||
|
local app_name=$(echo "${APP,,}" | tr -d ' ')
|
||||||
|
local app_type=${APP_TYPE:-ct}
|
||||||
|
local header_url="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/${app_type}/headers/${app_name}"
|
||||||
|
local local_header_path="/usr/local/community-scripts/headers/${app_type}/${app_name}"
|
||||||
|
|
||||||
|
mkdir -p "$(dirname "$local_header_path")"
|
||||||
|
|
||||||
|
if [ ! -s "$local_header_path" ]; then
|
||||||
|
if ! curl -fsSL "$header_url" -o "$local_header_path"; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat "$local_header_path" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
header_info() {
|
||||||
|
local app_name=$(echo "${APP,,}" | tr -d ' ')
|
||||||
|
local header_content
|
||||||
|
|
||||||
|
header_content=$(get_header "$app_name") || header_content=""
|
||||||
|
|
||||||
|
clear
|
||||||
|
local term_width
|
||||||
|
term_width=$(tput cols 2>/dev/null || echo 120)
|
||||||
|
|
||||||
|
if [ -n "$header_content" ]; then
|
||||||
|
echo "$header_content"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_tput() {
|
||||||
|
if ! command -v tput >/dev/null 2>&1; then
|
||||||
|
if grep -qi 'alpine' /etc/os-release; then
|
||||||
|
apk add --no-cache ncurses >/dev/null 2>&1
|
||||||
|
elif command -v apt-get >/dev/null 2>&1; then
|
||||||
|
apt-get update -qq >/dev/null
|
||||||
|
apt-get install -y -qq ncurses-bin >/dev/null 2>&1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
is_alpine() {
|
||||||
|
local os_id="${var_os:-${PCT_OSTYPE:-}}"
|
||||||
|
|
||||||
|
if [[ -z "$os_id" && -f /etc/os-release ]]; then
|
||||||
|
os_id="$(
|
||||||
|
. /etc/os-release 2>/dev/null
|
||||||
|
echo "${ID:-}"
|
||||||
|
)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
[[ "$os_id" == "alpine" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
is_verbose_mode() {
|
||||||
|
local verbose="${VERBOSE:-${var_verbose:-no}}"
|
||||||
|
local tty_status
|
||||||
|
if [[ -t 2 ]]; then
|
||||||
|
tty_status="interactive"
|
||||||
|
else
|
||||||
|
tty_status="not-a-tty"
|
||||||
|
fi
|
||||||
|
[[ "$verbose" != "no" || ! -t 2 ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Handles specific curl error codes and displays descriptive messages.
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
__curl_err_handler() {
|
||||||
|
local exit_code="$1"
|
||||||
|
local target="$2"
|
||||||
|
local curl_msg="$3"
|
||||||
|
|
||||||
|
case $exit_code in
|
||||||
|
1) msg_error "Unsupported protocol: $target" ;;
|
||||||
|
2) msg_error "Curl init failed: $target" ;;
|
||||||
|
3) msg_error "Malformed URL: $target" ;;
|
||||||
|
5) msg_error "Proxy resolution failed: $target" ;;
|
||||||
|
6) msg_error "Host resolution failed: $target" ;;
|
||||||
|
7) msg_error "Connection failed: $target" ;;
|
||||||
|
9) msg_error "Access denied: $target" ;;
|
||||||
|
18) msg_error "Partial file transfer: $target" ;;
|
||||||
|
22) msg_error "HTTP error (e.g. 400/404): $target" ;;
|
||||||
|
23) msg_error "Write error on local system: $target" ;;
|
||||||
|
26) msg_error "Read error from local file: $target" ;;
|
||||||
|
28) msg_error "Timeout: $target" ;;
|
||||||
|
35) msg_error "SSL connect error: $target" ;;
|
||||||
|
47) msg_error "Too many redirects: $target" ;;
|
||||||
|
51) msg_error "SSL cert verify failed: $target" ;;
|
||||||
|
52) msg_error "Empty server response: $target" ;;
|
||||||
|
55) msg_error "Send error: $target" ;;
|
||||||
|
56) msg_error "Receive error: $target" ;;
|
||||||
|
60) msg_error "SSL CA not trusted: $target" ;;
|
||||||
|
67) msg_error "Login denied by server: $target" ;;
|
||||||
|
78) msg_error "Remote file not found (404): $target" ;;
|
||||||
|
*) msg_error "Curl failed with code $exit_code: $target" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
[[ -n "$curl_msg" ]] && printf "%s\n" "$curl_msg" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
fatal() {
|
||||||
|
msg_error "$1"
|
||||||
|
kill -INT $$
|
||||||
|
}
|
||||||
|
|
||||||
|
spinner() {
|
||||||
|
local chars=(⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏)
|
||||||
|
local i=0
|
||||||
|
while true; do
|
||||||
|
local index=$((i++ % ${#chars[@]}))
|
||||||
|
printf "\r\033[2K%s %b" "${CS_YWB}${chars[$index]}${CS_CL}" "${CS_YWB}${SPINNER_MSG:-}${CS_CL}"
|
||||||
|
sleep 0.1
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
clear_line() {
|
||||||
|
tput cr 2>/dev/null || echo -en "\r"
|
||||||
|
tput el 2>/dev/null || echo -en "\033[K"
|
||||||
|
}
|
||||||
|
|
||||||
|
stop_spinner() {
|
||||||
|
local pid="${SPINNER_PID:-}"
|
||||||
|
[[ -z "$pid" && -f /tmp/.spinner.pid ]] && pid=$(</tmp/.spinner.pid)
|
||||||
|
|
||||||
|
if [[ -n "$pid" && "$pid" =~ ^[0-9]+$ ]]; then
|
||||||
|
if kill "$pid" 2>/dev/null; then
|
||||||
|
sleep 0.05
|
||||||
|
kill -9 "$pid" 2>/dev/null || true
|
||||||
|
wait "$pid" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
rm -f /tmp/.spinner.pid
|
||||||
|
fi
|
||||||
|
|
||||||
|
unset SPINNER_PID SPINNER_MSG
|
||||||
|
stty sane 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
msg_info() {
|
||||||
|
local msg="$1"
|
||||||
|
[[ -z "$msg" ]] && return
|
||||||
|
|
||||||
|
if ! declare -p MSG_INFO_SHOWN &>/dev/null || ! declare -A MSG_INFO_SHOWN &>/dev/null; then
|
||||||
|
declare -gA MSG_INFO_SHOWN=()
|
||||||
|
fi
|
||||||
|
[[ -n "${MSG_INFO_SHOWN["$msg"]+x}" ]] && return
|
||||||
|
MSG_INFO_SHOWN["$msg"]=1
|
||||||
|
|
||||||
|
stop_spinner
|
||||||
|
SPINNER_MSG="$msg"
|
||||||
|
|
||||||
|
if is_verbose_mode || is_alpine; then
|
||||||
|
local HOURGLASS="${TAB}⏳${TAB}"
|
||||||
|
printf "\r\e[2K%s %b" "$HOURGLASS" "${YW}${msg}${CL}" >&2
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
color_spinner
|
||||||
|
spinner &
|
||||||
|
SPINNER_PID=$!
|
||||||
|
echo "$SPINNER_PID" >/tmp/.spinner.pid
|
||||||
|
disown "$SPINNER_PID" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
msg_ok() {
|
||||||
|
local msg="$1"
|
||||||
|
[[ -z "$msg" ]] && return
|
||||||
|
stop_spinner
|
||||||
|
clear_line
|
||||||
|
printf "%s %b\n" "$CM" "${GN}${msg}${CL}" >&2
|
||||||
|
unset MSG_INFO_SHOWN["$msg"]
|
||||||
|
}
|
||||||
|
|
||||||
|
msg_error() {
|
||||||
|
stop_spinner
|
||||||
|
local msg="$1"
|
||||||
|
echo -e "${BFR:-} ${CROSS:-✖️} ${RD}${msg}${CL}"
|
||||||
|
}
|
||||||
|
|
||||||
|
msg_warn() {
|
||||||
|
stop_spinner
|
||||||
|
local msg="$1"
|
||||||
|
echo -e "${BFR:-} ${INFO:-ℹ️} ${YWB}${msg}${CL}"
|
||||||
|
}
|
||||||
|
|
||||||
|
msg_custom() {
|
||||||
|
local symbol="${1:-"[*]"}"
|
||||||
|
local color="${2:-"\e[36m"}"
|
||||||
|
local msg="${3:-}"
|
||||||
|
[[ -z "$msg" ]] && return
|
||||||
|
stop_spinner
|
||||||
|
echo -e "${BFR:-} ${symbol} ${color}${msg}${CL:-\e[0m}"
|
||||||
|
}
|
||||||
|
|
||||||
|
run_container_safe() {
|
||||||
|
local ct="$1"
|
||||||
|
shift
|
||||||
|
local cmd="$*"
|
||||||
|
|
||||||
|
lxc-attach -n "$ct" -- bash -euo pipefail -c "
|
||||||
|
trap 'echo Aborted in container; exit 130' SIGINT SIGTERM
|
||||||
|
$cmd
|
||||||
|
" || __handle_general_error "lxc-attach to CT $ct"
|
||||||
|
}
|
||||||
|
|
||||||
|
check_or_create_swap() {
|
||||||
|
msg_info "Checking for active swap"
|
||||||
|
|
||||||
|
if swapon --noheadings --show | grep -q 'swap'; then
|
||||||
|
msg_ok "Swap is active"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
msg_error "No active swap detected"
|
||||||
|
|
||||||
|
read -p "Do you want to create a swap file? [y/N]: " create_swap
|
||||||
|
create_swap="${create_swap,,}" # to lowercase
|
||||||
|
|
||||||
|
if [[ "$create_swap" != "y" && "$create_swap" != "yes" ]]; then
|
||||||
|
msg_info "Skipping swap file creation"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
read -p "Enter swap size in MB (e.g., 2048 for 2GB): " swap_size_mb
|
||||||
|
if ! [[ "$swap_size_mb" =~ ^[0-9]+$ ]]; then
|
||||||
|
msg_error "Invalid size input. Aborting."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local swap_file="/swapfile"
|
||||||
|
|
||||||
|
msg_info "Creating ${swap_size_mb}MB swap file at $swap_file"
|
||||||
|
if dd if=/dev/zero of="$swap_file" bs=1M count="$swap_size_mb" status=progress &&
|
||||||
|
chmod 600 "$swap_file" &&
|
||||||
|
mkswap "$swap_file" &&
|
||||||
|
swapon "$swap_file"; then
|
||||||
|
msg_ok "Swap file created and activated successfully"
|
||||||
|
else
|
||||||
|
msg_error "Failed to create or activate swap"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
trap 'stop_spinner' EXIT INT TERM
|
||||||
206
scripts/core/install.func
Executable file
206
scripts/core/install.func
Executable file
@@ -0,0 +1,206 @@
|
|||||||
|
# Copyright (c) 2021-2025 michelroegl-brunner
|
||||||
|
# Author: michelroegl-brunner
|
||||||
|
# License: MIT
|
||||||
|
# https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||||
|
|
||||||
|
if ! command -v curl >/dev/null 2>&1; 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 "$(dirname "${BASH_SOURCE[0]}")/core.func"
|
||||||
|
load_functions
|
||||||
|
# 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 [ "$DISABLEIPV6" == "yes" ]; then
|
||||||
|
echo "net.ipv6.conf.all.disable_ipv6 = 1" >>/etc/sysctl.conf
|
||||||
|
$STD sysctl -p
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# This function sets error handling options and defines the error_handler function to handle errors
|
||||||
|
catch_errors() {
|
||||||
|
set -Eeuo pipefail
|
||||||
|
trap 'error_handler $LINENO "$BASH_COMMAND"' ERR
|
||||||
|
}
|
||||||
|
|
||||||
|
# This function handles errors
|
||||||
|
error_handler() {
|
||||||
|
printf "\e[?25h"
|
||||||
|
local exit_code="$?"
|
||||||
|
local line_number="$1"
|
||||||
|
local command="$2"
|
||||||
|
local error_message="${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}"
|
||||||
|
echo -e "\n$error_message"
|
||||||
|
if [[ "$line_number" -eq 51 ]]; then
|
||||||
|
echo -e "The silent function has suppressed the error, run the script with verbose mode enabled, which will provide more detailed output.\n"
|
||||||
|
post_update_to_api "failed" "No error message, script ran in silent mode"
|
||||||
|
else
|
||||||
|
post_update_to_api "failed" "${command}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# This function sets up the Container OS by generating the locale, setting the timezone, and checking the network connection
|
||||||
|
setting_up_container() {
|
||||||
|
msg_info "Setting up Container OS"
|
||||||
|
for ((i = RETRY_NUM; i > 0; i--)); do
|
||||||
|
if [ "$(hostname -I)" != "" ]; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
echo 1>&2 -en "${CROSS}${RD} No Network! "
|
||||||
|
sleep $RETRY_EVERY
|
||||||
|
done
|
||||||
|
if [ "$(hostname -I)" = "" ]; then
|
||||||
|
echo 1>&2 -e "\n${CROSS}${RD} No Network After $RETRY_NUM Tries${CL}"
|
||||||
|
echo -e "${NETWORK}Check Network Settings"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
rm -rf /usr/lib/python3.*/EXTERNALLY-MANAGED
|
||||||
|
systemctl disable -q --now systemd-networkd-wait-online.service
|
||||||
|
msg_ok "Set up Container OS"
|
||||||
|
#msg_custom "${CM}" "${GN}" "Network Connected: ${BL}$(hostname -I)"
|
||||||
|
msg_ok "Network Connected: ${BL}$(hostname -I)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# This function checks the network connection by pinging a known IP address and prompts the user to continue if the internet is not connected
|
||||||
|
# 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
|
||||||
|
ipv4_connected=false
|
||||||
|
ipv6_connected=false
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
# Check IPv4 connectivity to Google, Cloudflare & Quad9 DNS servers.
|
||||||
|
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
|
||||||
|
msg_ok "IPv4 Internet Connected"
|
||||||
|
ipv4_connected=true
|
||||||
|
else
|
||||||
|
msg_error "IPv4 Internet Not Connected"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check IPv6 connectivity to Google, Cloudflare & Quad9 DNS servers.
|
||||||
|
if ping6 -c 1 -W 1 2606:4700:4700::1111 &>/dev/null || ping6 -c 1 -W 1 2001:4860:4860::8888 &>/dev/null || ping6 -c 1 -W 1 2620:fe::fe &>/dev/null; then
|
||||||
|
msg_ok "IPv6 Internet Connected"
|
||||||
|
ipv6_connected=true
|
||||||
|
else
|
||||||
|
msg_error "IPv6 Internet Not Connected"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# If both IPv4 and IPv6 checks fail, prompt the user
|
||||||
|
if [[ $ipv4_connected == false && $ipv6_connected == false ]]; then
|
||||||
|
read -r -p "No Internet detected, would you like to continue anyway? <y/N> " prompt
|
||||||
|
if [[ "${prompt,,}" =~ ^(y|yes)$ ]]; then
|
||||||
|
echo -e "${INFO}${RD}Expect Issues Without Internet${CL}"
|
||||||
|
else
|
||||||
|
echo -e "${NETWORK}Check Network Settings"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# DNS resolution checks for GitHub-related domains (IPv4 and/or IPv6)
|
||||||
|
GIT_HOSTS=("github.com" "raw.githubusercontent.com" "api.github.com" "git.community-scripts.org")
|
||||||
|
GIT_STATUS="Git DNS:"
|
||||||
|
DNS_FAILED=false
|
||||||
|
|
||||||
|
for HOST in "${GIT_HOSTS[@]}"; do
|
||||||
|
RESOLVEDIP=$(getent hosts "$HOST" | awk '{ print $1 }' | grep -E '(^([0-9]{1,3}\.){3}[0-9]{1,3}$)|(^[a-fA-F0-9:]+$)' | head -n1)
|
||||||
|
if [[ -z "$RESOLVEDIP" ]]; then
|
||||||
|
GIT_STATUS+="$HOST:($DNSFAIL)"
|
||||||
|
DNS_FAILED=true
|
||||||
|
else
|
||||||
|
GIT_STATUS+=" $HOST:($DNSOK)"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ "$DNS_FAILED" == true ]]; then
|
||||||
|
fatal "$GIT_STATUS"
|
||||||
|
else
|
||||||
|
msg_ok "$GIT_STATUS"
|
||||||
|
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"
|
||||||
|
if [[ "$CACHER" == "yes" ]]; then
|
||||||
|
echo 'Acquire::http::Proxy-Auto-Detect "/usr/local/bin/apt-proxy-detect.sh";' >/etc/apt/apt.conf.d/00aptproxy
|
||||||
|
cat <<EOF >/usr/local/bin/apt-proxy-detect.sh
|
||||||
|
#!/bin/bash
|
||||||
|
if nc -w1 -z "${CACHER_IP}" 3142; then
|
||||||
|
echo -n "http://${CACHER_IP}:3142"
|
||||||
|
else
|
||||||
|
echo -n "DIRECT"
|
||||||
|
fi
|
||||||
|
EOF
|
||||||
|
chmod +x /usr/local/bin/apt-proxy-detect.sh
|
||||||
|
fi
|
||||||
|
$STD apt-get update
|
||||||
|
$STD apt-get -o Dpkg::Options::="--force-confold" -y dist-upgrade
|
||||||
|
rm -rf /usr/lib/python3.*/EXTERNALLY-MANAGED
|
||||||
|
msg_ok "Updated Container OS"
|
||||||
|
|
||||||
|
source "$(dirname "${BASH_SOURCE[0]}")/tools.func")
|
||||||
|
}
|
||||||
|
|
||||||
|
# This function modifies the message of the day (motd) and SSH settings
|
||||||
|
motd_ssh() {
|
||||||
|
# Set terminal to 256-color mode
|
||||||
|
grep -qxF "export TERM='xterm-256color'" /root/.bashrc || echo "export TERM='xterm-256color'" >>/root/.bashrc
|
||||||
|
|
||||||
|
# Get OS information (Debian / Ubuntu)
|
||||||
|
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 '"')
|
||||||
|
elif [ -f "/etc/debian_version" ]; then
|
||||||
|
OS_NAME="Debian"
|
||||||
|
OS_VERSION=$(cat /etc/debian_version)
|
||||||
|
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}${HOSTNAME}${YW} Hostname: ${GN}\$(hostname)${CL}\"" >>"$PROFILE_FILE"
|
||||||
|
echo -e "echo -e \"${TAB}${INFO}${YW} IP Address: ${GN}\$(hostname -I | awk '{print \$1}')${CL}\"" >>"$PROFILE_FILE"
|
||||||
|
|
||||||
|
# Disable default MOTD scripts
|
||||||
|
chmod -x /etc/update-motd.d/*
|
||||||
|
|
||||||
|
if [[ "${SSH_ROOT}" == "yes" ]]; then
|
||||||
|
sed -i "s/#PermitRootLogin prohibit-password/PermitRootLogin yes/g" /etc/ssh/sshd_config
|
||||||
|
systemctl restart sshd
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# This function customizes the container by modifying the getty service and enabling auto-login for the root user
|
||||||
|
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 <<EOF >$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//')
|
||||||
|
msg_ok "Customized Container"
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
if [[ -n "${SSH_AUTHORIZED_KEY}" ]]; then
|
||||||
|
mkdir -p /root/.ssh
|
||||||
|
echo "${SSH_AUTHORIZED_KEY}" >/root/.ssh/authorized_keys
|
||||||
|
chmod 700 /root/.ssh
|
||||||
|
chmod 600 /root/.ssh/authorized_keys
|
||||||
|
fi
|
||||||
|
}
|
||||||
43
scripts/ct/debian.sh
Executable file
43
scripts/ct/debian.sh
Executable file
@@ -0,0 +1,43 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
SCRIPT_DIR="$(dirname "$0")"
|
||||||
|
source "$SCRIPT_DIR/../core/build.func"
|
||||||
|
# Copyright (c) 2021-2025 tteck
|
||||||
|
# Author: tteck (tteckster)
|
||||||
|
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||||
|
# Source: https://www.debian.org/
|
||||||
|
|
||||||
|
APP="Debian"
|
||||||
|
var_tags="${var_tags:-os}"
|
||||||
|
var_cpu="${var_cpu:-1}"
|
||||||
|
var_ram="${var_ram:-512}"
|
||||||
|
var_disk="${var_disk:-2}"
|
||||||
|
var_os="${var_os:-debian}"
|
||||||
|
var_version="${var_version:-12}"
|
||||||
|
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 /var ]]; then
|
||||||
|
msg_error "No ${APP} Installation Found!"
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
msg_info "Updating $APP LXC"
|
||||||
|
$STD apt-get update
|
||||||
|
$STD apt-get -y upgrade
|
||||||
|
msg_ok "Updated $APP LXC"
|
||||||
|
exit
|
||||||
|
}
|
||||||
|
|
||||||
|
start
|
||||||
|
build_container
|
||||||
|
description
|
||||||
|
|
||||||
|
msg_ok "Completed Successfully!\n"
|
||||||
|
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Demo script for PVE Scripts Local Management
|
|
||||||
# This script demonstrates live output streaming
|
|
||||||
|
|
||||||
echo "🚀 Starting PVE Script Demo..."
|
|
||||||
echo "================================"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
echo "📋 System Information:"
|
|
||||||
echo " - Hostname: $(hostname)"
|
|
||||||
echo " - User: $(whoami)"
|
|
||||||
echo " - Date: $(date)"
|
|
||||||
echo " - Uptime: $(uptime)"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
echo "🔧 Simulating Proxmox operations..."
|
|
||||||
echo " - Checking Proxmox API connection..."
|
|
||||||
sleep 2
|
|
||||||
echo " ✅ API connection successful"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
echo " - Listing VMs..."
|
|
||||||
sleep 1
|
|
||||||
echo " 📦 VM 100: Ubuntu Server 22.04 (running)"
|
|
||||||
echo " 📦 VM 101: Windows Server 2022 (stopped)"
|
|
||||||
echo " 📦 VM 102: Debian 12 (running)"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
echo " - Checking storage..."
|
|
||||||
sleep 1
|
|
||||||
echo " 💾 Local storage: 500GB (200GB used)"
|
|
||||||
echo " 💾 NFS storage: 2TB (800GB used)"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
echo " - Checking cluster status..."
|
|
||||||
sleep 1
|
|
||||||
echo " 🏗️ Node: pve-01 (online)"
|
|
||||||
echo " 🏗️ Node: pve-02 (online)"
|
|
||||||
echo " 🏗️ Node: pve-03 (maintenance)"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
echo "🎯 Demo completed successfully!"
|
|
||||||
echo "================================"
|
|
||||||
echo "This script ran for demonstration purposes."
|
|
||||||
echo "In a real scenario, this would perform actual Proxmox operations."
|
|
||||||
22
scripts/install/debian-install.sh
Executable file
22
scripts/install/debian-install.sh
Executable file
@@ -0,0 +1,22 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Copyright (c) 2021-2025 tteck
|
||||||
|
# Author: tteck (tteckster)
|
||||||
|
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||||
|
# Source: https://www.debian.org/
|
||||||
|
|
||||||
|
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
|
||||||
|
color
|
||||||
|
verb_ip6
|
||||||
|
catch_errors
|
||||||
|
setting_up_container
|
||||||
|
network_check
|
||||||
|
update_os
|
||||||
|
|
||||||
|
motd_ssh
|
||||||
|
customize
|
||||||
|
|
||||||
|
msg_info "Cleaning up"
|
||||||
|
$STD apt-get -y autoremove
|
||||||
|
$STD apt-get -y autoclean
|
||||||
|
msg_ok "Cleaned"
|
||||||
14
scripts/test-script.sh
Executable file
14
scripts/test-script.sh
Executable file
@@ -0,0 +1,14 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
echo "Hello from test script!"
|
||||||
|
echo "Current directory: $(pwd)"
|
||||||
|
echo "Script arguments: $@"
|
||||||
|
echo "Environment variables:"
|
||||||
|
env | grep -E "(PATH|HOME|USER)" | head -5
|
||||||
|
|
||||||
|
for i in {1..5}; do
|
||||||
|
echo "Count: $i"
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Test script completed!"
|
||||||
101
server.js
101
server.js
@@ -4,12 +4,13 @@ import next from 'next';
|
|||||||
import { WebSocketServer } from 'ws';
|
import { WebSocketServer } from 'ws';
|
||||||
import { spawn } from 'child_process';
|
import { spawn } from 'child_process';
|
||||||
import { join, resolve } from 'path';
|
import { join, resolve } from 'path';
|
||||||
|
import stripAnsi from 'strip-ansi';
|
||||||
|
import { spawn as ptySpawn } from 'node-pty';
|
||||||
|
|
||||||
const dev = process.env.NODE_ENV !== 'production';
|
const dev = process.env.NODE_ENV !== 'production';
|
||||||
const hostname = 'localhost';
|
const hostname = '0.0.0.0';
|
||||||
const port = process.env.PORT || 3000;
|
const port = process.env.PORT || 3000;
|
||||||
|
|
||||||
// when using middleware `hostname` and `port` must be provided below
|
|
||||||
const app = next({ dev, hostname, port });
|
const app = next({ dev, hostname, port });
|
||||||
const handle = app.getRequestHandler();
|
const handle = app.getRequestHandler();
|
||||||
|
|
||||||
@@ -27,10 +28,19 @@ class ScriptExecutionHandler {
|
|||||||
setupWebSocket() {
|
setupWebSocket() {
|
||||||
this.wss.on('connection', (ws, request) => {
|
this.wss.on('connection', (ws, request) => {
|
||||||
console.log('New WebSocket connection for script execution');
|
console.log('New WebSocket connection for script execution');
|
||||||
|
console.log('Client IP:', request.socket.remoteAddress);
|
||||||
|
console.log('User-Agent:', request.headers['user-agent']);
|
||||||
|
console.log('WebSocket readyState:', ws.readyState);
|
||||||
|
console.log('Request URL:', request.url);
|
||||||
|
|
||||||
|
// Set connection metadata
|
||||||
|
ws.connectionTime = Date.now();
|
||||||
|
ws.clientIP = request.socket.remoteAddress;
|
||||||
|
|
||||||
ws.on('message', (data) => {
|
ws.on('message', (data) => {
|
||||||
try {
|
try {
|
||||||
const message = JSON.parse(data.toString());
|
const message = JSON.parse(data.toString());
|
||||||
|
console.log('Received message from client:', message);
|
||||||
this.handleMessage(ws, message);
|
this.handleMessage(ws, message);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error parsing WebSocket message:', error);
|
console.error('Error parsing WebSocket message:', error);
|
||||||
@@ -42,8 +52,8 @@ class ScriptExecutionHandler {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ws.on('close', () => {
|
ws.on('close', (code, reason) => {
|
||||||
console.log('WebSocket connection closed');
|
console.log(`WebSocket connection closed: ${code} - ${reason}`);
|
||||||
this.cleanupActiveExecutions(ws);
|
this.cleanupActiveExecutions(ws);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -55,13 +65,16 @@ class ScriptExecutionHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handleMessage(ws, message) {
|
async handleMessage(ws, message) {
|
||||||
const { action, scriptPath, executionId } = message;
|
const { action, scriptPath, executionId, input } = message;
|
||||||
|
console.log('Handling message:', { action, scriptPath, executionId });
|
||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case 'start':
|
case 'start':
|
||||||
if (scriptPath && executionId) {
|
if (scriptPath && executionId) {
|
||||||
|
console.log('Starting script execution for:', scriptPath);
|
||||||
await this.startScriptExecution(ws, scriptPath, executionId);
|
await this.startScriptExecution(ws, scriptPath, executionId);
|
||||||
} else {
|
} else {
|
||||||
|
console.log('Missing scriptPath or executionId');
|
||||||
this.sendMessage(ws, {
|
this.sendMessage(ws, {
|
||||||
type: 'error',
|
type: 'error',
|
||||||
data: 'Missing scriptPath or executionId',
|
data: 'Missing scriptPath or executionId',
|
||||||
@@ -76,6 +89,12 @@ class ScriptExecutionHandler {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'input':
|
||||||
|
if (executionId && input !== undefined) {
|
||||||
|
this.sendInputToProcess(executionId, input);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
this.sendMessage(ws, {
|
this.sendMessage(ws, {
|
||||||
type: 'error',
|
type: 'error',
|
||||||
@@ -87,11 +106,17 @@ class ScriptExecutionHandler {
|
|||||||
|
|
||||||
async startScriptExecution(ws, scriptPath, executionId) {
|
async startScriptExecution(ws, scriptPath, executionId) {
|
||||||
try {
|
try {
|
||||||
|
console.log('Starting script execution...');
|
||||||
// Basic validation
|
// Basic validation
|
||||||
const scriptsDir = join(process.cwd(), 'scripts');
|
const scriptsDir = join(process.cwd(), 'scripts');
|
||||||
const resolvedPath = resolve(scriptPath);
|
const resolvedPath = resolve(scriptPath);
|
||||||
|
|
||||||
|
console.log('Scripts directory:', scriptsDir);
|
||||||
|
console.log('Resolved path:', resolvedPath);
|
||||||
|
console.log('Is within scripts dir:', resolvedPath.startsWith(resolve(scriptsDir)));
|
||||||
|
|
||||||
if (!resolvedPath.startsWith(resolve(scriptsDir))) {
|
if (!resolvedPath.startsWith(resolve(scriptsDir))) {
|
||||||
|
console.log('Script path validation failed');
|
||||||
this.sendMessage(ws, {
|
this.sendMessage(ws, {
|
||||||
type: 'error',
|
type: 'error',
|
||||||
data: 'Script path is not within the allowed scripts directory',
|
data: 'Script path is not within the allowed scripts directory',
|
||||||
@@ -110,15 +135,25 @@ class ScriptExecutionHandler {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start script execution
|
// Start script execution with pty for proper TTY support
|
||||||
const process = spawn('bash', [scriptPath], {
|
const childProcess = ptySpawn('bash', [scriptPath], {
|
||||||
cwd: scriptsDir,
|
cwd: scriptsDir,
|
||||||
stdio: ['pipe', 'pipe', 'pipe'],
|
name: 'xterm-256color',
|
||||||
shell: true
|
cols: 80,
|
||||||
|
rows: 24,
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
TERM: 'xterm-256color', // Enable proper terminal support
|
||||||
|
FORCE_ANSI: 'true', // Allow ANSI codes for proper display
|
||||||
|
COLUMNS: '80', // Set terminal width
|
||||||
|
LINES: '24' // Set terminal height
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// pty handles encoding automatically
|
||||||
|
|
||||||
// Store the execution
|
// Store the execution
|
||||||
this.activeExecutions.set(executionId, { process, ws });
|
this.activeExecutions.set(executionId, { process: childProcess, ws });
|
||||||
|
|
||||||
// Send start message
|
// Send start message
|
||||||
this.sendMessage(ws, {
|
this.sendMessage(ws, {
|
||||||
@@ -127,8 +162,8 @@ class ScriptExecutionHandler {
|
|||||||
timestamp: Date.now()
|
timestamp: Date.now()
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle stdout
|
// Handle pty data (both stdout and stderr combined)
|
||||||
process.stdout?.on('data', (data) => {
|
childProcess.onData((data) => {
|
||||||
this.sendMessage(ws, {
|
this.sendMessage(ws, {
|
||||||
type: 'output',
|
type: 'output',
|
||||||
data: data.toString(),
|
data: data.toString(),
|
||||||
@@ -136,32 +171,11 @@ class ScriptExecutionHandler {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle stderr
|
|
||||||
process.stderr?.on('data', (data) => {
|
|
||||||
this.sendMessage(ws, {
|
|
||||||
type: 'error',
|
|
||||||
data: data.toString(),
|
|
||||||
timestamp: Date.now()
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle process exit
|
// Handle process exit
|
||||||
process.on('exit', (code, signal) => {
|
childProcess.onExit((exitCode, signal) => {
|
||||||
this.sendMessage(ws, {
|
this.sendMessage(ws, {
|
||||||
type: 'end',
|
type: 'end',
|
||||||
data: `Script execution finished with code: ${code}, signal: ${signal}`,
|
data: `Script execution finished with code: ${exitCode}, signal: ${signal}`,
|
||||||
timestamp: Date.now()
|
|
||||||
});
|
|
||||||
|
|
||||||
// Clean up
|
|
||||||
this.activeExecutions.delete(executionId);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle process error
|
|
||||||
process.on('error', (error) => {
|
|
||||||
this.sendMessage(ws, {
|
|
||||||
type: 'error',
|
|
||||||
data: `Process error: ${error.message}`,
|
|
||||||
timestamp: Date.now()
|
timestamp: Date.now()
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -192,6 +206,16 @@ class ScriptExecutionHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sendInputToProcess(executionId, input) {
|
||||||
|
const execution = this.activeExecutions.get(executionId);
|
||||||
|
if (execution && execution.process.write) {
|
||||||
|
console.log('Sending input to process:', JSON.stringify(input), 'Length:', input.length);
|
||||||
|
execution.process.write(input);
|
||||||
|
} else {
|
||||||
|
console.log('No active execution found for input:', executionId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
sendMessage(ws, message) {
|
sendMessage(ws, message) {
|
||||||
if (ws.readyState === 1) { // WebSocket.OPEN
|
if (ws.readyState === 1) { // WebSocket.OPEN
|
||||||
ws.send(JSON.stringify(message));
|
ws.send(JSON.stringify(message));
|
||||||
@@ -208,6 +232,8 @@ class ScriptExecutionHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TerminalHandler removed - not used by current application
|
||||||
|
|
||||||
app.prepare().then(() => {
|
app.prepare().then(() => {
|
||||||
const httpServer = createServer(async (req, res) => {
|
const httpServer = createServer(async (req, res) => {
|
||||||
try {
|
try {
|
||||||
@@ -229,15 +255,16 @@ app.prepare().then(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create WebSocket handler
|
// Create WebSocket handlers
|
||||||
const scriptHandler = new ScriptExecutionHandler(httpServer);
|
const scriptHandler = new ScriptExecutionHandler(httpServer);
|
||||||
|
// Note: TerminalHandler removed as it's not being used by the current application
|
||||||
|
|
||||||
httpServer
|
httpServer
|
||||||
.once('error', (err) => {
|
.once('error', (err) => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
})
|
})
|
||||||
.listen(port, () => {
|
.listen(port, hostname, () => {
|
||||||
console.log(`> Ready on http://${hostname}:${port}`);
|
console.log(`> Ready on http://${hostname}:${port}`);
|
||||||
console.log(`> WebSocket server running on ws://${hostname}:${port}/ws/script-execution`);
|
console.log(`> WebSocket server running on ws://${hostname}:${port}/ws/script-execution`);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { api } from '~/trpc/react';
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Terminal } from './Terminal';
|
import { Terminal } from './Terminal';
|
||||||
|
|
||||||
|
|
||||||
interface ScriptsListProps {
|
interface ScriptsListProps {
|
||||||
onRunScript: (scriptPath: string, scriptName: string) => void;
|
onRunScript: (scriptPath: string, scriptName: string) => void;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { api } from '~/trpc/react';
|
import { Terminal as XTerm } from '@xterm/xterm';
|
||||||
|
import { FitAddon } from '@xterm/addon-fit';
|
||||||
|
import { WebLinksAddon } from '@xterm/addon-web-links';
|
||||||
|
import '@xterm/xterm/css/xterm.css';
|
||||||
|
|
||||||
interface TerminalProps {
|
interface TerminalProps {
|
||||||
scriptPath: string;
|
scriptPath: string;
|
||||||
@@ -17,70 +20,203 @@ interface TerminalMessage {
|
|||||||
export function Terminal({ scriptPath, onClose }: TerminalProps) {
|
export function Terminal({ scriptPath, onClose }: TerminalProps) {
|
||||||
const [isConnected, setIsConnected] = useState(false);
|
const [isConnected, setIsConnected] = useState(false);
|
||||||
const [isRunning, setIsRunning] = useState(false);
|
const [isRunning, setIsRunning] = useState(false);
|
||||||
const [output, setOutput] = useState<string[]>([]);
|
const terminalRef = useRef<HTMLDivElement>(null);
|
||||||
const [executionId] = useState(() => `exec_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`);
|
const xtermRef = useRef<XTerm | null>(null);
|
||||||
|
const fitAddonRef = useRef<FitAddon | null>(null);
|
||||||
const wsRef = useRef<WebSocket | null>(null);
|
const wsRef = useRef<WebSocket | null>(null);
|
||||||
const outputRef = useRef<HTMLDivElement>(null);
|
const [executionId] = useState(() => `exec_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`);
|
||||||
|
const isConnectingRef = useRef<boolean>(false);
|
||||||
|
const hasConnectedRef = useRef<boolean>(false);
|
||||||
|
|
||||||
const scriptName = scriptPath.split('/').pop() || scriptPath.split('\\').pop() || 'Unknown Script';
|
const scriptName = scriptPath.split('/').pop() || scriptPath.split('\\').pop() || 'Unknown Script';
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Connect to WebSocket
|
// Initialize xterm.js terminal with proper timing
|
||||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
if (!terminalRef.current || xtermRef.current) return;
|
||||||
const wsUrl = `${protocol}//${window.location.host}/ws/script-execution`;
|
|
||||||
|
|
||||||
const ws = new WebSocket(wsUrl);
|
|
||||||
wsRef.current = ws;
|
|
||||||
|
|
||||||
ws.onopen = () => {
|
// Use setTimeout to ensure DOM is fully ready
|
||||||
console.log('WebSocket connected');
|
const initTerminal = () => {
|
||||||
setIsConnected(true);
|
if (!terminalRef.current || xtermRef.current) return;
|
||||||
|
|
||||||
|
const terminal = new XTerm({
|
||||||
|
theme: {
|
||||||
|
background: '#000000',
|
||||||
|
foreground: '#00ff00',
|
||||||
|
cursor: '#00ff00',
|
||||||
|
},
|
||||||
|
fontSize: 14,
|
||||||
|
fontFamily: 'Courier New, monospace',
|
||||||
|
cursorBlink: true,
|
||||||
|
cursorStyle: 'block',
|
||||||
|
scrollback: 1000,
|
||||||
|
tabStopWidth: 4,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add addons
|
||||||
|
const fitAddon = new FitAddon();
|
||||||
|
const webLinksAddon = new WebLinksAddon();
|
||||||
|
terminal.loadAddon(fitAddon);
|
||||||
|
terminal.loadAddon(webLinksAddon);
|
||||||
|
|
||||||
|
// Open terminal
|
||||||
|
terminal.open(terminalRef.current);
|
||||||
|
|
||||||
|
// Fit after a small delay to ensure proper sizing
|
||||||
|
setTimeout(() => {
|
||||||
|
fitAddon.fit();
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
// Store references
|
||||||
|
xtermRef.current = terminal;
|
||||||
|
fitAddonRef.current = fitAddon;
|
||||||
|
|
||||||
|
// Handle terminal input
|
||||||
|
terminal.onData((data) => {
|
||||||
|
if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
|
||||||
|
wsRef.current.send(JSON.stringify({
|
||||||
|
action: 'input',
|
||||||
|
executionId,
|
||||||
|
input: data
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle terminal resize
|
||||||
|
const handleResize = () => {
|
||||||
|
if (fitAddonRef.current) {
|
||||||
|
fitAddonRef.current.fit();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('resize', handleResize);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', handleResize);
|
||||||
|
terminal.dispose();
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onmessage = (event) => {
|
// Initialize with a small delay
|
||||||
try {
|
const timeoutId = setTimeout(initTerminal, 50);
|
||||||
const message: TerminalMessage = JSON.parse(event.data);
|
|
||||||
handleMessage(message);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error parsing WebSocket message:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
ws.onclose = () => {
|
|
||||||
console.log('WebSocket disconnected');
|
|
||||||
setIsConnected(false);
|
|
||||||
setIsRunning(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
ws.onerror = (error) => {
|
|
||||||
console.error('WebSocket error:', error);
|
|
||||||
setIsConnected(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (ws.readyState === WebSocket.OPEN) {
|
clearTimeout(timeoutId);
|
||||||
ws.close();
|
if (xtermRef.current) {
|
||||||
|
xtermRef.current.dispose();
|
||||||
|
xtermRef.current = null;
|
||||||
|
fitAddonRef.current = null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Prevent multiple connections in React Strict Mode
|
||||||
|
if (hasConnectedRef.current || isConnectingRef.current || (wsRef.current && wsRef.current.readyState === WebSocket.OPEN)) {
|
||||||
|
console.log('WebSocket already connected, connecting, or has connected, skipping...');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close any existing connection first
|
||||||
|
if (wsRef.current) {
|
||||||
|
console.log('Closing existing WebSocket connection');
|
||||||
|
wsRef.current.close();
|
||||||
|
wsRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
isConnectingRef.current = true;
|
||||||
|
hasConnectedRef.current = true;
|
||||||
|
|
||||||
|
// Small delay to prevent rapid reconnection
|
||||||
|
const connectWithDelay = () => {
|
||||||
|
// Connect to WebSocket
|
||||||
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||||
|
const wsUrl = `${protocol}//${window.location.host}/ws/script-execution`;
|
||||||
|
|
||||||
|
console.log('Connecting to WebSocket:', wsUrl);
|
||||||
|
const ws = new WebSocket(wsUrl);
|
||||||
|
wsRef.current = ws;
|
||||||
|
|
||||||
|
ws.onopen = () => {
|
||||||
|
console.log('WebSocket connected successfully');
|
||||||
|
console.log('Script path:', scriptPath);
|
||||||
|
console.log('Execution ID:', executionId);
|
||||||
|
setIsConnected(true);
|
||||||
|
isConnectingRef.current = false;
|
||||||
|
|
||||||
|
// Send start message immediately after connection
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
action: 'start',
|
||||||
|
scriptPath,
|
||||||
|
executionId
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onmessage = (event) => {
|
||||||
|
try {
|
||||||
|
const message: TerminalMessage = JSON.parse(event.data);
|
||||||
|
console.log('Received message:', message);
|
||||||
|
handleMessage(message);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error parsing WebSocket message:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onclose = (event) => {
|
||||||
|
console.log('WebSocket disconnected:', event.code, event.reason);
|
||||||
|
setIsConnected(false);
|
||||||
|
setIsRunning(false);
|
||||||
|
isConnectingRef.current = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onerror = (error) => {
|
||||||
|
console.error('WebSocket error:', error);
|
||||||
|
console.error('WebSocket readyState:', ws.readyState);
|
||||||
|
setIsConnected(false);
|
||||||
|
isConnectingRef.current = false;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add small delay to prevent rapid reconnection
|
||||||
|
const timeoutId = setTimeout(connectWithDelay, 100);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
isConnectingRef.current = false;
|
||||||
|
hasConnectedRef.current = false;
|
||||||
|
if (wsRef.current && (wsRef.current.readyState === WebSocket.OPEN || wsRef.current.readyState === WebSocket.CONNECTING)) {
|
||||||
|
console.log('Cleaning up WebSocket connection');
|
||||||
|
wsRef.current.close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [scriptPath, executionId]);
|
||||||
|
|
||||||
const handleMessage = (message: TerminalMessage) => {
|
const handleMessage = (message: TerminalMessage) => {
|
||||||
|
if (!xtermRef.current) return;
|
||||||
|
|
||||||
const timestamp = new Date(message.timestamp).toLocaleTimeString();
|
const timestamp = new Date(message.timestamp).toLocaleTimeString();
|
||||||
const prefix = `[${timestamp}] `;
|
const prefix = `[${timestamp}] `;
|
||||||
|
|
||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
case 'start':
|
case 'start':
|
||||||
setOutput(prev => [...prev, `${prefix}🚀 ${message.data}`]);
|
xtermRef.current.writeln(`${prefix}🚀 ${message.data}`);
|
||||||
setIsRunning(true);
|
setIsRunning(true);
|
||||||
break;
|
break;
|
||||||
case 'output':
|
case 'output':
|
||||||
setOutput(prev => [...prev, message.data]);
|
// Write directly to terminal - xterm.js handles ANSI codes natively
|
||||||
|
xtermRef.current.write(message.data);
|
||||||
break;
|
break;
|
||||||
case 'error':
|
case 'error':
|
||||||
setOutput(prev => [...prev, `${prefix}❌ ${message.data}`]);
|
// Check if this looks like ANSI terminal output (contains escape codes)
|
||||||
|
if (message.data.includes('\x1B[') || message.data.includes('\u001b[')) {
|
||||||
|
// This is likely terminal output sent to stderr, treat it as normal output
|
||||||
|
xtermRef.current.write(message.data);
|
||||||
|
} else {
|
||||||
|
// This is a real error, show it with error prefix
|
||||||
|
xtermRef.current.writeln(`${prefix}❌ ${message.data}`);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case 'end':
|
case 'end':
|
||||||
setOutput(prev => [...prev, `${prefix}✅ ${message.data}`]);
|
xtermRef.current.writeln(`${prefix}✅ ${message.data}`);
|
||||||
setIsRunning(false);
|
setIsRunning(false);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -106,15 +242,10 @@ export function Terminal({ scriptPath, onClose }: TerminalProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const clearOutput = () => {
|
const clearOutput = () => {
|
||||||
setOutput([]);
|
if (xtermRef.current) {
|
||||||
};
|
xtermRef.current.clear();
|
||||||
|
|
||||||
// Auto-scroll to bottom when new output arrives
|
|
||||||
useEffect(() => {
|
|
||||||
if (outputRef.current) {
|
|
||||||
outputRef.current.scrollTop = outputRef.current.scrollHeight;
|
|
||||||
}
|
}
|
||||||
}, [output]);
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-gray-900 rounded-lg border border-gray-700 overflow-hidden">
|
<div className="bg-gray-900 rounded-lg border border-gray-700 overflow-hidden">
|
||||||
@@ -141,23 +272,10 @@ export function Terminal({ scriptPath, onClose }: TerminalProps) {
|
|||||||
|
|
||||||
{/* Terminal Output */}
|
{/* Terminal Output */}
|
||||||
<div
|
<div
|
||||||
ref={outputRef}
|
ref={terminalRef}
|
||||||
className="h-96 overflow-y-auto p-4 font-mono text-sm"
|
className="h-96 w-full"
|
||||||
style={{ backgroundColor: '#000000', color: '#00ff00' }}
|
style={{ minHeight: '384px' }}
|
||||||
>
|
/>
|
||||||
{output.length === 0 ? (
|
|
||||||
<div className="text-gray-500">
|
|
||||||
<p>Terminal ready. Click "Start Script" to begin execution.</p>
|
|
||||||
<p className="mt-2">Script: {scriptPath}</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
output.map((line, index) => (
|
|
||||||
<div key={index} className="whitespace-pre-wrap">
|
|
||||||
{line}
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Terminal Controls */}
|
{/* Terminal Controls */}
|
||||||
<div className="bg-gray-800 px-4 py-2 flex items-center justify-between border-t border-gray-700">
|
<div className="bg-gray-800 px-4 py-2 flex items-center justify-between border-t border-gray-700">
|
||||||
@@ -203,4 +321,4 @@ export function Terminal({ scriptPath, onClose }: TerminalProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -69,8 +69,11 @@ export class ScriptManager {
|
|||||||
*/
|
*/
|
||||||
private async isExecutable(filePath: string): Promise<boolean> {
|
private async isExecutable(filePath: string): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
await access(filePath, 0o111); // Check execute permission
|
const stats = await stat(filePath);
|
||||||
return true;
|
// Check if file has execute permission for owner, group, or others
|
||||||
|
const mode = stats.mode;
|
||||||
|
const isExecutable = !!(mode & parseInt('111', 8));
|
||||||
|
return isExecutable;
|
||||||
} catch {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,3 +4,19 @@
|
|||||||
--font-sans: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif,
|
--font-sans: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif,
|
||||||
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Terminal-specific styles for ANSI escape code rendering */
|
||||||
|
.terminal-output {
|
||||||
|
font-family: 'Courier New', 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-output span {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure proper rendering of ANSI colors */
|
||||||
|
.terminal-output * {
|
||||||
|
color: inherit;
|
||||||
|
background-color: inherit;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user