Three-tier defaults system | security improvements | error_handler | improved logging | improved container creation | improved architecture (#9540)
* Refactor Core Refactored misc/alpine-install.func to improve error handling, network checks, and MOTD setup. Added misc/alpine-tools.func and misc/error_handler.func for modular tool installation and error management. Enhanced misc/api.func with detailed exit code explanations and telemetry functions. Updated misc/core.func for better initialization, validation, and execution helpers. Removed misc/create_lxc.sh as part of cleanup. * Delete config-file.func * Update install.func * Refactor stop_all_services function and variable names Refactor service stopping logic and improve variable handling * Refactor installation script and update copyright Updated copyright information and adjusted package installation commands. Enhanced IPv6 disabling logic and improved container customization process. * Update install.func * Update license comment format in install.func * Refactor IPv6 handling and enhance MOTD and SSH Refactor IPv6 handling and update OS function. Enhance MOTD with additional details and configure SSH settings. * big core refactor * Enhance IPv6 configuration menu options Updated IPv6 Address Management menu options for clarity and added a new option for fully disabling IPv6. * Update default Node.js version to 24 LTS * Update misc/alpine-tools.func Co-authored-by: Michel Roegl-Brunner <73236783+michelroegl-brunner@users.noreply.github.com> * indention * remove debugf and duplicate codes * Update whiptail backtitles and error codes Removed '[dev]' from whiptail --backtitle strings for consistency. Refactored custom exit codes in build.func and error_handler.func: updated Proxmox error codes, shifted MySQL/MariaDB codes to 260-263, and removed unused MongoDB code. Updated error descriptions to match new codes. * comments * Refactor error handling and clean up debug comments Standardized bash variable checks, removed unnecessary debug and commented code, and clarified error handling logic in container build and setup scripts. These changes improve code readability and maintainability without altering functional behavior. * Update build.func * feat: Improve LXC network checks and LINSTOR storage handling Enhanced LXC container network setup to check for both IPv4 and IPv6 addresses, added connectivity (ping) tests, and provided troubleshooting tips on failure. Updated storage validation to support LINSTOR, including cluster connectivity checks and special handling for LINSTOR template storage. --------- Co-authored-by: Michel Roegl-Brunner <73236783+michelroegl-brunner@users.noreply.github.com>
This commit is contained in:
parent
a5e6810872
commit
a826769899
@ -7,13 +7,15 @@ if ! command -v curl >/dev/null 2>&1; then
|
|||||||
apk update && apk add curl >/dev/null 2>&1
|
apk update && apk add curl >/dev/null 2>&1
|
||||||
fi
|
fi
|
||||||
source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/core.func)
|
source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/core.func)
|
||||||
|
source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/error_handler.func)
|
||||||
load_functions
|
load_functions
|
||||||
|
catch_errors
|
||||||
|
|
||||||
# This function enables IPv6 if it's not disabled and sets verbose mode
|
# This function enables IPv6 if it's not disabled and sets verbose mode
|
||||||
verb_ip6() {
|
verb_ip6() {
|
||||||
set_std_mode # Set STD mode based on VERBOSE
|
set_std_mode # Set STD mode based on VERBOSE
|
||||||
|
|
||||||
if [ "$IPV6_METHOD" == "disable" ]; then
|
if [ "${IPV6_METHOD:-}" = "disable" ]; then
|
||||||
msg_info "Disabling IPv6 (this may affect some services)"
|
msg_info "Disabling IPv6 (this may affect some services)"
|
||||||
$STD sysctl -w net.ipv6.conf.all.disable_ipv6=1
|
$STD sysctl -w net.ipv6.conf.all.disable_ipv6=1
|
||||||
$STD sysctl -w net.ipv6.conf.default.disable_ipv6=1
|
$STD sysctl -w net.ipv6.conf.default.disable_ipv6=1
|
||||||
@ -29,19 +31,40 @@ EOF
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# This function catches errors and handles them with the error handler function
|
set -Eeuo pipefail
|
||||||
catch_errors() {
|
trap 'error_handler $? $LINENO "$BASH_COMMAND"' ERR
|
||||||
set -Eeuo pipefail
|
trap on_exit EXIT
|
||||||
trap 'error_handler $LINENO "$BASH_COMMAND"' ERR
|
trap on_interrupt INT
|
||||||
|
trap on_terminate TERM
|
||||||
|
|
||||||
|
error_handler() {
|
||||||
|
local exit_code="$1"
|
||||||
|
local line_number="$2"
|
||||||
|
local command="$3"
|
||||||
|
|
||||||
|
if [[ "$exit_code" -eq 0 ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf "\e[?25h"
|
||||||
|
echo -e "\n${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}\n"
|
||||||
|
exit "$exit_code"
|
||||||
}
|
}
|
||||||
|
|
||||||
# This function handles errors
|
on_exit() {
|
||||||
error_handler() {
|
|
||||||
local exit_code="$?"
|
local exit_code="$?"
|
||||||
local line_number="$1"
|
[[ -n "${lockfile:-}" && -e "$lockfile" ]] && rm -f "$lockfile"
|
||||||
local command="$2"
|
exit "$exit_code"
|
||||||
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"
|
|
||||||
|
on_interrupt() {
|
||||||
|
echo -e "\n${RD}Interrupted by user (SIGINT)${CL}"
|
||||||
|
exit 130
|
||||||
|
}
|
||||||
|
|
||||||
|
on_terminate() {
|
||||||
|
echo -e "\n${RD}Terminated by signal (SIGTERM)${CL}"
|
||||||
|
exit 143
|
||||||
}
|
}
|
||||||
|
|
||||||
# This function sets up the Container OS by generating the locale, setting the timezone, and checking the network connection
|
# This function sets up the Container OS by generating the locale, setting the timezone, and checking the network connection
|
||||||
@ -70,10 +93,10 @@ network_check() {
|
|||||||
set +e
|
set +e
|
||||||
trap - ERR
|
trap - ERR
|
||||||
if ping -c 1 -W 1 1.1.1.1 &>/dev/null || ping -c 1 -W 1 8.8.8.8 &>/dev/null || ping -c 1 -W 1 9.9.9.9 &>/dev/null; then
|
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 "Internet Connected"
|
ipv4_status="${GN}✔${CL} IPv4"
|
||||||
else
|
else
|
||||||
msg_error "Internet NOT Connected"
|
ipv4_status="${RD}✖${CL} IPv4"
|
||||||
read -r -p "Would you like to continue anyway? <y/N> " prompt
|
read -r -p "Internet NOT connected. Continue anyway? <y/N> " prompt
|
||||||
if [[ "${prompt,,}" =~ ^(y|yes)$ ]]; then
|
if [[ "${prompt,,}" =~ ^(y|yes)$ ]]; then
|
||||||
echo -e "${INFO}${RD}Expect Issues Without Internet${CL}"
|
echo -e "${INFO}${RD}Expect Issues Without Internet${CL}"
|
||||||
else
|
else
|
||||||
@ -82,7 +105,11 @@ network_check() {
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
RESOLVEDIP=$(getent hosts github.com | awk '{ print $1 }')
|
RESOLVEDIP=$(getent hosts github.com | awk '{ print $1 }')
|
||||||
if [[ -z "$RESOLVEDIP" ]]; then msg_error "DNS Lookup Failure"; else msg_ok "DNS Resolved github.com to ${BL}$RESOLVEDIP${CL}"; fi
|
if [[ -z "$RESOLVEDIP" ]]; then
|
||||||
|
msg_error "Internet: ${ipv4_status} DNS Failed"
|
||||||
|
else
|
||||||
|
msg_ok "Internet: ${ipv4_status} DNS: ${BL}${RESOLVEDIP}${CL}"
|
||||||
|
fi
|
||||||
set -e
|
set -e
|
||||||
trap 'error_handler $LINENO "$BASH_COMMAND"' ERR
|
trap 'error_handler $LINENO "$BASH_COMMAND"' ERR
|
||||||
}
|
}
|
||||||
@ -163,10 +190,4 @@ EOF
|
|||||||
echo "bash -c \"\$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/${app}.sh)\"" >/usr/bin/update
|
echo "bash -c \"\$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/${app}.sh)\"" >/usr/bin/update
|
||||||
chmod +x /usr/bin/update
|
chmod +x /usr/bin/update
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|||||||
507
misc/alpine-tools.func
Normal file
507
misc/alpine-tools.func
Normal file
@ -0,0 +1,507 @@
|
|||||||
|
#!/bin/ash
|
||||||
|
# shellcheck shell=ash
|
||||||
|
|
||||||
|
# Expects existing msg_* functions and optional $STD from the framework.
|
||||||
|
|
||||||
|
# ------------------------------
|
||||||
|
# helpers
|
||||||
|
# ------------------------------
|
||||||
|
lower() { printf '%s' "$1" | tr '[:upper:]' '[:lower:]'; }
|
||||||
|
has() { command -v "$1" >/dev/null 2>&1; }
|
||||||
|
|
||||||
|
need_tool() {
|
||||||
|
# usage: need_tool curl jq unzip ...
|
||||||
|
# setup missing tools via apk
|
||||||
|
local missing=0 t
|
||||||
|
for t in "$@"; do
|
||||||
|
if ! has "$t"; then missing=1; fi
|
||||||
|
done
|
||||||
|
if [ "$missing" -eq 1 ]; then
|
||||||
|
msg_info "Installing tools: $*"
|
||||||
|
apk add --no-cache "$@" >/dev/null 2>&1 || {
|
||||||
|
msg_error "apk add failed for: $*"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
msg_ok "Tools ready: $*"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
net_resolves() {
|
||||||
|
# better handling for missing getent on Alpine
|
||||||
|
# usage: net_resolves api.github.com
|
||||||
|
local host="$1"
|
||||||
|
ping -c1 -W1 "$host" >/dev/null 2>&1 || nslookup "$host" >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_usr_local_bin_persist() {
|
||||||
|
local PROFILE_FILE="/etc/profile.d/10-localbin.sh"
|
||||||
|
if [ ! -f "$PROFILE_FILE" ]; then
|
||||||
|
echo 'case ":$PATH:" in *:/usr/local/bin:*) ;; *) export PATH="/usr/local/bin:$PATH";; esac' >"$PROFILE_FILE"
|
||||||
|
chmod +x "$PROFILE_FILE"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
download_with_progress() {
|
||||||
|
# $1 url, $2 dest
|
||||||
|
local url="$1" out="$2" cl
|
||||||
|
need_tool curl pv || return 1
|
||||||
|
cl=$(curl -fsSLI "$url" 2>/dev/null | awk 'tolower($0) ~ /^content-length:/ {print $2}' | tr -d '\r')
|
||||||
|
if [ -n "$cl" ]; then
|
||||||
|
curl -fsSL "$url" | pv -s "$cl" >"$out" || {
|
||||||
|
msg_error "Download failed: $url"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
else
|
||||||
|
curl -fL# -o "$out" "$url" || {
|
||||||
|
msg_error "Download failed: $url"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------
|
||||||
|
# GitHub: check Release
|
||||||
|
# ------------------------------
|
||||||
|
check_for_gh_release() {
|
||||||
|
# app, repo, [pinned]
|
||||||
|
local app="$1" source="$2" pinned="${3:-}"
|
||||||
|
local app_lc
|
||||||
|
app_lc="$(lower "$app" | tr -d ' ')"
|
||||||
|
local current_file="$HOME/.${app_lc}"
|
||||||
|
local current="" release tag
|
||||||
|
|
||||||
|
msg_info "Check for update: $app"
|
||||||
|
|
||||||
|
net_resolves api.github.com || {
|
||||||
|
msg_error "DNS/network error: api.github.com"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
need_tool curl jq || return 1
|
||||||
|
|
||||||
|
tag=$(curl -fsSL "https://api.github.com/repos/${source}/releases/latest" | jq -r '.tag_name // empty')
|
||||||
|
[ -z "$tag" ] && {
|
||||||
|
msg_error "Unable to fetch latest tag for $app"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
release="${tag#v}"
|
||||||
|
|
||||||
|
[ -f "$current_file" ] && current="$(cat "$current_file")"
|
||||||
|
|
||||||
|
if [ -n "$pinned" ]; then
|
||||||
|
if [ "$pinned" = "$release" ]; then
|
||||||
|
msg_ok "$app pinned to v$pinned (no update)"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if [ "$current" = "$pinned" ]; then
|
||||||
|
msg_ok "$app pinned v$pinned installed (upstream v$release)"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
msg_info "$app pinned v$pinned (upstream v$release) → update/downgrade"
|
||||||
|
CHECK_UPDATE_RELEASE="$pinned"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$release" != "$current" ] || [ ! -f "$current_file" ]; then
|
||||||
|
CHECK_UPDATE_RELEASE="$release"
|
||||||
|
msg_info "New release available: v$release (current: v${current:-none})"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
msg_ok "$app is up to date (v$release)"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------
|
||||||
|
# GitHub: get Release & deploy (Alpine)
|
||||||
|
# modes: tarball | prebuild | singlefile
|
||||||
|
# ------------------------------
|
||||||
|
fetch_and_deploy_gh() {
|
||||||
|
# $1 app, $2 repo, [$3 mode], [$4 version], [$5 target], [$6 asset_pattern
|
||||||
|
local app="$1" repo="$2" mode="${3:-tarball}" version="${4:-latest}" target="${5:-/opt/$1}" pattern="${6:-}"
|
||||||
|
local app_lc
|
||||||
|
app_lc="$(lower "$app" | tr -d ' ')"
|
||||||
|
local vfile="$HOME/.${app_lc}"
|
||||||
|
local json url filename tmpd unpack
|
||||||
|
|
||||||
|
net_resolves api.github.com || {
|
||||||
|
msg_error "DNS/network error"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
need_tool curl jq tar || return 1
|
||||||
|
[ "$mode" = "prebuild" ] || [ "$mode" = "singlefile" ] && need_tool unzip >/dev/null 2>&1 || true
|
||||||
|
|
||||||
|
tmpd="$(mktemp -d)" || return 1
|
||||||
|
mkdir -p "$target"
|
||||||
|
|
||||||
|
# Release JSON
|
||||||
|
if [ "$version" = "latest" ]; then
|
||||||
|
json="$(curl -fsSL "https://api.github.com/repos/$repo/releases/latest")" || {
|
||||||
|
msg_error "GitHub API failed"
|
||||||
|
rm -rf "$tmpd"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
else
|
||||||
|
json="$(curl -fsSL "https://api.github.com/repos/$repo/releases/tags/$version")" || {
|
||||||
|
msg_error "GitHub API failed"
|
||||||
|
rm -rf "$tmpd"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
|
||||||
|
# correct Version
|
||||||
|
version="$(printf '%s' "$json" | jq -r '.tag_name // empty')"
|
||||||
|
version="${version#v}"
|
||||||
|
|
||||||
|
[ -z "$version" ] && {
|
||||||
|
msg_error "No tag in release json"
|
||||||
|
rm -rf "$tmpd"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
case "$mode" in
|
||||||
|
tarball | source)
|
||||||
|
url="$(printf '%s' "$json" | jq -r '.tarball_url // empty')"
|
||||||
|
[ -z "$url" ] && url="https://github.com/$repo/archive/refs/tags/v$version.tar.gz"
|
||||||
|
filename="${app_lc}-${version}.tar.gz"
|
||||||
|
download_with_progress "$url" "$tmpd/$filename" || {
|
||||||
|
rm -rf "$tmpd"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
tar -xzf "$tmpd/$filename" -C "$tmpd" || {
|
||||||
|
msg_error "tar extract failed"
|
||||||
|
rm -rf "$tmpd"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
unpack="$(find "$tmpd" -mindepth 1 -maxdepth 1 -type d | head -n1)"
|
||||||
|
# copy content of unpack to target
|
||||||
|
(cd "$unpack" && tar -cf - .) | (cd "$target" && tar -xf -) || {
|
||||||
|
msg_error "copy failed"
|
||||||
|
rm -rf "$tmpd"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
;;
|
||||||
|
prebuild)
|
||||||
|
[ -n "$pattern" ] || {
|
||||||
|
msg_error "prebuild requires asset pattern"
|
||||||
|
rm -rf "$tmpd"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
url="$(printf '%s' "$json" | jq -r '.assets[].browser_download_url' | awk -v p="$pattern" '
|
||||||
|
BEGIN{IGNORECASE=1}
|
||||||
|
$0 ~ p {print; exit}
|
||||||
|
')"
|
||||||
|
[ -z "$url" ] && {
|
||||||
|
msg_error "asset not found for pattern: $pattern"
|
||||||
|
rm -rf "$tmpd"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
filename="${url##*/}"
|
||||||
|
download_with_progress "$url" "$tmpd/$filename" || {
|
||||||
|
rm -rf "$tmpd"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
# unpack archive (Zip or tarball)
|
||||||
|
case "$filename" in
|
||||||
|
*.zip)
|
||||||
|
need_tool unzip || {
|
||||||
|
rm -rf "$tmpd"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
mkdir -p "$tmpd/unp"
|
||||||
|
unzip -q "$tmpd/$filename" -d "$tmpd/unp"
|
||||||
|
;;
|
||||||
|
*.tar.gz | *.tgz | *.tar.xz | *.tar.zst | *.tar.bz2)
|
||||||
|
mkdir -p "$tmpd/unp"
|
||||||
|
tar -xf "$tmpd/$filename" -C "$tmpd/unp"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
msg_error "unsupported archive: $filename"
|
||||||
|
rm -rf "$tmpd"
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
# top-level folder strippen
|
||||||
|
if [ "$(find "$tmpd/unp" -mindepth 1 -maxdepth 1 -type d | wc -l)" -eq 1 ] && [ -z "$(find "$tmpd/unp" -mindepth 1 -maxdepth 1 -type f | head -n1)" ]; then
|
||||||
|
unpack="$(find "$tmpd/unp" -mindepth 1 -maxdepth 1 -type d)"
|
||||||
|
(cd "$unpack" && tar -cf - .) | (cd "$target" && tar -xf -) || {
|
||||||
|
msg_error "copy failed"
|
||||||
|
rm -rf "$tmpd"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
else
|
||||||
|
(cd "$tmpd/unp" && tar -cf - .) | (cd "$target" && tar -xf -) || {
|
||||||
|
msg_error "copy failed"
|
||||||
|
rm -rf "$tmpd"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
singlefile)
|
||||||
|
[ -n "$pattern" ] || {
|
||||||
|
msg_error "singlefile requires asset pattern"
|
||||||
|
rm -rf "$tmpd"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
url="$(printf '%s' "$json" | jq -r '.assets[].browser_download_url' | awk -v p="$pattern" '
|
||||||
|
BEGIN{IGNORECASE=1}
|
||||||
|
$0 ~ p {print; exit}
|
||||||
|
')"
|
||||||
|
[ -z "$url" ] && {
|
||||||
|
msg_error "asset not found for pattern: $pattern"
|
||||||
|
rm -rf "$tmpd"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
filename="${url##*/}"
|
||||||
|
download_with_progress "$url" "$target/$app" || {
|
||||||
|
rm -rf "$tmpd"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
chmod +x "$target/$app"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
msg_error "Unknown mode: $mode"
|
||||||
|
rm -rf "$tmpd"
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo "$version" >"$vfile"
|
||||||
|
ensure_usr_local_bin_persist
|
||||||
|
rm -rf "$tmpd"
|
||||||
|
msg_ok "Deployed $app ($version) → $target"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------
|
||||||
|
# yq (mikefarah) – Alpine
|
||||||
|
# ------------------------------
|
||||||
|
setup_yq() {
|
||||||
|
# prefer apk, unless FORCE_GH=1
|
||||||
|
if [ "${FORCE_GH:-0}" != "1" ] && apk info -e yq >/dev/null 2>&1; then
|
||||||
|
msg_info "Updating yq via apk"
|
||||||
|
apk add --no-cache --upgrade yq >/dev/null 2>&1 || true
|
||||||
|
msg_ok "yq ready ($(yq --version 2>/dev/null))"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
need_tool curl || return 1
|
||||||
|
local arch bin url tmp
|
||||||
|
case "$(uname -m)" in
|
||||||
|
x86_64) arch="amd64" ;;
|
||||||
|
aarch64) arch="arm64" ;;
|
||||||
|
*)
|
||||||
|
msg_error "Unsupported arch for yq: $(uname -m)"
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
url="https://github.com/mikefarah/yq/releases/latest/download/yq_linux_${arch}"
|
||||||
|
tmp="$(mktemp)"
|
||||||
|
download_with_progress "$url" "$tmp" || return 1
|
||||||
|
install -m 0755 "$tmp" /usr/local/bin/yq
|
||||||
|
rm -f "$tmp"
|
||||||
|
msg_ok "Setup yq ($(yq --version 2>/dev/null))"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------
|
||||||
|
# Adminer – Alpine
|
||||||
|
# ------------------------------
|
||||||
|
setup_adminer() {
|
||||||
|
need_tool curl || return 1
|
||||||
|
msg_info "Setup Adminer (Alpine)"
|
||||||
|
mkdir -p /var/www/localhost/htdocs/adminer
|
||||||
|
curl -fsSL https://github.com/vrana/adminer/releases/latest/download/adminer.php \
|
||||||
|
-o /var/www/localhost/htdocs/adminer/index.php || {
|
||||||
|
msg_error "Adminer download failed"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
msg_ok "Adminer at /adminer (served by your webserver)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------
|
||||||
|
# uv – Alpine (musl tarball)
|
||||||
|
# optional: PYTHON_VERSION="3.12"
|
||||||
|
# ------------------------------
|
||||||
|
setup_uv() {
|
||||||
|
need_tool curl tar || return 1
|
||||||
|
local UV_BIN="/usr/local/bin/uv"
|
||||||
|
local arch tarball url tmpd ver installed
|
||||||
|
|
||||||
|
case "$(uname -m)" in
|
||||||
|
x86_64) arch="x86_64-unknown-linux-musl" ;;
|
||||||
|
aarch64) arch="aarch64-unknown-linux-musl" ;;
|
||||||
|
*)
|
||||||
|
msg_error "Unsupported arch for uv: $(uname -m)"
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
ver="$(curl -fsSL https://api.github.com/repos/astral-sh/uv/releases/latest | jq -r '.tag_name' 2>/dev/null)"
|
||||||
|
ver="${ver#v}"
|
||||||
|
[ -z "$ver" ] && {
|
||||||
|
msg_error "uv: cannot determine latest version"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if has "$UV_BIN"; then
|
||||||
|
installed="$($UV_BIN -V 2>/dev/null | awk '{print $2}')"
|
||||||
|
[ "$installed" = "$ver" ] && {
|
||||||
|
msg_ok "uv $ver already installed"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
msg_info "Updating uv $installed → $ver"
|
||||||
|
else
|
||||||
|
msg_info "Setup uv $ver"
|
||||||
|
fi
|
||||||
|
|
||||||
|
tmpd="$(mktemp -d)" || return 1
|
||||||
|
tarball="uv-${arch}.tar.gz"
|
||||||
|
url="https://github.com/astral-sh/uv/releases/download/v${ver}/${tarball}"
|
||||||
|
|
||||||
|
download_with_progress "$url" "$tmpd/uv.tar.gz" || {
|
||||||
|
rm -rf "$tmpd"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
tar -xzf "$tmpd/uv.tar.gz" -C "$tmpd" || {
|
||||||
|
msg_error "uv: extract failed"
|
||||||
|
rm -rf "$tmpd"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# tar contains ./uv
|
||||||
|
if [ -x "$tmpd/uv" ]; then
|
||||||
|
install -m 0755 "$tmpd/uv" "$UV_BIN"
|
||||||
|
else
|
||||||
|
# fallback: in subfolder
|
||||||
|
install -m 0755 "$tmpd"/*/uv "$UV_BIN" 2>/dev/null || {
|
||||||
|
msg_error "uv binary not found in tar"
|
||||||
|
rm -rf "$tmpd"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
rm -rf "$tmpd"
|
||||||
|
ensure_usr_local_bin_persist
|
||||||
|
msg_ok "Setup uv $ver"
|
||||||
|
|
||||||
|
if [ -n "${PYTHON_VERSION:-}" ]; then
|
||||||
|
local match
|
||||||
|
match="$(uv python list --only-downloads 2>/dev/null | awk -v maj="$PYTHON_VERSION" '
|
||||||
|
$0 ~ "^cpython-"maj"\\." { print $0 }' | awk -F- '{print $2}' | sort -V | tail -n1)"
|
||||||
|
[ -z "$match" ] && {
|
||||||
|
msg_error "No matching Python for $PYTHON_VERSION"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if ! uv python list | grep -q "cpython-${match}-linux"; then
|
||||||
|
msg_info "Installing Python $match via uv"
|
||||||
|
uv python install "$match" || {
|
||||||
|
msg_error "uv python install failed"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
msg_ok "Python $match installed (uv)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------
|
||||||
|
# Java – Alpine (OpenJDK)
|
||||||
|
# JAVA_VERSION: 17|21 (Default 21)
|
||||||
|
# ------------------------------
|
||||||
|
setup_java() {
|
||||||
|
local JAVA_VERSION="${JAVA_VERSION:-21}" pkg
|
||||||
|
case "$JAVA_VERSION" in
|
||||||
|
17) pkg="openjdk17-jdk" ;;
|
||||||
|
21 | *) pkg="openjdk21-jdk" ;;
|
||||||
|
esac
|
||||||
|
msg_info "Setup Java (OpenJDK $JAVA_VERSION)"
|
||||||
|
apk add --no-cache "$pkg" >/dev/null 2>&1 || {
|
||||||
|
msg_error "apk add $pkg failed"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
# set JAVA_HOME
|
||||||
|
local prof="/etc/profile.d/20-java.sh"
|
||||||
|
if [ ! -f "$prof" ]; then
|
||||||
|
echo 'export JAVA_HOME=$(dirname $(dirname $(readlink -f $(command -v java))))' >"$prof"
|
||||||
|
echo 'case ":$PATH:" in *:$JAVA_HOME/bin:*) ;; *) export PATH="$JAVA_HOME/bin:$PATH";; esac' >>"$prof"
|
||||||
|
chmod +x "$prof"
|
||||||
|
fi
|
||||||
|
msg_ok "Java ready: $(java -version 2>&1 | head -n1)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------
|
||||||
|
# Go – Alpine (apk prefers, else tarball)
|
||||||
|
# ------------------------------
|
||||||
|
setup_go() {
|
||||||
|
if [ -z "${GO_VERSION:-}" ]; then
|
||||||
|
msg_info "Setup Go (apk)"
|
||||||
|
apk add --no-cache go >/dev/null 2>&1 || {
|
||||||
|
msg_error "apk add go failed"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
msg_ok "Go ready: $(go version 2>/dev/null)"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
need_tool curl tar || return 1
|
||||||
|
local ARCH TARBALL URL TMP
|
||||||
|
case "$(uname -m)" in
|
||||||
|
x86_64) ARCH="amd64" ;;
|
||||||
|
aarch64) ARCH="arm64" ;;
|
||||||
|
*)
|
||||||
|
msg_error "Unsupported arch for Go: $(uname -m)"
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
TARBALL="go${GO_VERSION}.linux-${ARCH}.tar.gz"
|
||||||
|
URL="https://go.dev/dl/${TARBALL}"
|
||||||
|
msg_info "Setup Go $GO_VERSION (tarball)"
|
||||||
|
TMP="$(mktemp)"
|
||||||
|
download_with_progress "$URL" "$TMP" || return 1
|
||||||
|
rm -rf /usr/local/go
|
||||||
|
tar -C /usr/local -xzf "$TMP" || {
|
||||||
|
msg_error "extract go failed"
|
||||||
|
rm -f "$TMP"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
rm -f "$TMP"
|
||||||
|
ln -sf /usr/local/go/bin/go /usr/local/bin/go
|
||||||
|
ln -sf /usr/local/go/bin/gofmt /usr/local/bin/gofmt
|
||||||
|
ensure_usr_local_bin_persist
|
||||||
|
msg_ok "Go ready: $(go version 2>/dev/null)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------
|
||||||
|
# Composer – Alpine
|
||||||
|
# uses php83-cli + openssl + phar
|
||||||
|
# ------------------------------
|
||||||
|
setup_composer() {
|
||||||
|
local COMPOSER_BIN="/usr/local/bin/composer"
|
||||||
|
if ! has php; then
|
||||||
|
# prefers php83
|
||||||
|
msg_info "Installing PHP CLI for Composer"
|
||||||
|
apk add --no-cache php83-cli php83-openssl php83-phar php83-iconv >/dev/null 2>&1 || {
|
||||||
|
# Fallback to generic php if 83 not available
|
||||||
|
apk add --no-cache php-cli php-openssl php-phar php-iconv >/dev/null 2>&1 || {
|
||||||
|
msg_error "Failed to install php-cli for composer"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
msg_ok "PHP CLI ready: $(php -v | head -n1)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -x "$COMPOSER_BIN" ]; then
|
||||||
|
msg_info "Updating Composer"
|
||||||
|
else
|
||||||
|
msg_info "Setup Composer"
|
||||||
|
fi
|
||||||
|
|
||||||
|
need_tool curl || return 1
|
||||||
|
curl -fsSL https://getcomposer.org/installer -o /tmp/composer-setup.php || {
|
||||||
|
msg_error "composer installer download failed"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
php /tmp/composer-setup.php --install-dir=/usr/local/bin --filename=composer >/dev/null 2>&1 || {
|
||||||
|
msg_error "composer install failed"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
rm -f /tmp/composer-setup.php
|
||||||
|
ensure_usr_local_bin_persist
|
||||||
|
msg_ok "Composer ready: $(composer --version 2>/dev/null)"
|
||||||
|
}
|
||||||
202
misc/api.func
202
misc/api.func
@ -2,6 +2,153 @@
|
|||||||
# Author: michelroegl-brunner
|
# Author: michelroegl-brunner
|
||||||
# License: MIT | https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/LICENSE
|
# License: MIT | https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/LICENSE
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# API.FUNC - TELEMETRY & DIAGNOSTICS API
|
||||||
|
# ==============================================================================
|
||||||
|
#
|
||||||
|
# Provides functions for sending anonymous telemetry data to Community-Scripts
|
||||||
|
# API for analytics and diagnostics purposes.
|
||||||
|
#
|
||||||
|
# Features:
|
||||||
|
# - Container/VM creation statistics
|
||||||
|
# - Installation success/failure tracking
|
||||||
|
# - Error code mapping and reporting
|
||||||
|
# - Privacy-respecting anonymous telemetry
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# source <(curl -fsSL .../api.func)
|
||||||
|
# post_to_api # Report container creation
|
||||||
|
# post_update_to_api # Report installation status
|
||||||
|
#
|
||||||
|
# Privacy:
|
||||||
|
# - Only anonymous statistics (no personal data)
|
||||||
|
# - User can opt-out via diagnostics settings
|
||||||
|
# - Random UUID for session tracking only
|
||||||
|
#
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# SECTION 1: ERROR CODE DESCRIPTIONS
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# explain_exit_code()
|
||||||
|
#
|
||||||
|
# - Maps numeric exit codes to human-readable error descriptions
|
||||||
|
# - Supports:
|
||||||
|
# * Generic/Shell errors (1, 2, 126, 127, 128, 130, 137, 139, 143)
|
||||||
|
# * Package manager errors (APT, DPKG: 100, 101, 255)
|
||||||
|
# * Node.js/npm errors (243-249, 254)
|
||||||
|
# * Python/pip/uv errors (210-212)
|
||||||
|
# * PostgreSQL errors (231-234)
|
||||||
|
# * MySQL/MariaDB errors (241-244)
|
||||||
|
# * MongoDB errors (251-254)
|
||||||
|
# * Proxmox custom codes (200-231)
|
||||||
|
# - Returns description string for given exit code
|
||||||
|
# - Shared function with error_handler.func for consistency
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
explain_exit_code() {
|
||||||
|
local code="$1"
|
||||||
|
case "$code" in
|
||||||
|
# --- Generic / Shell ---
|
||||||
|
1) echo "General error / Operation not permitted" ;;
|
||||||
|
2) echo "Misuse of shell builtins (e.g. syntax error)" ;;
|
||||||
|
126) echo "Command invoked cannot execute (permission problem?)" ;;
|
||||||
|
127) echo "Command not found" ;;
|
||||||
|
128) echo "Invalid argument to exit" ;;
|
||||||
|
130) echo "Terminated by Ctrl+C (SIGINT)" ;;
|
||||||
|
137) echo "Killed (SIGKILL / Out of memory?)" ;;
|
||||||
|
139) echo "Segmentation fault (core dumped)" ;;
|
||||||
|
143) echo "Terminated (SIGTERM)" ;;
|
||||||
|
|
||||||
|
# --- Package manager / APT / DPKG ---
|
||||||
|
100) echo "APT: Package manager error (broken packages / dependency problems)" ;;
|
||||||
|
101) echo "APT: Configuration error (bad sources.list, malformed config)" ;;
|
||||||
|
255) echo "DPKG: Fatal internal error" ;;
|
||||||
|
|
||||||
|
# --- Node.js / npm / pnpm / yarn ---
|
||||||
|
243) echo "Node.js: Out of memory (JavaScript heap out of memory)" ;;
|
||||||
|
245) echo "Node.js: Invalid command-line option" ;;
|
||||||
|
246) echo "Node.js: Internal JavaScript Parse Error" ;;
|
||||||
|
247) echo "Node.js: Fatal internal error" ;;
|
||||||
|
248) echo "Node.js: Invalid C++ addon / N-API failure" ;;
|
||||||
|
249) echo "Node.js: Inspector error" ;;
|
||||||
|
254) echo "npm/pnpm/yarn: Unknown fatal error" ;;
|
||||||
|
|
||||||
|
# --- Python / pip / uv ---
|
||||||
|
210) echo "Python: Virtualenv / uv environment missing or broken" ;;
|
||||||
|
211) echo "Python: Dependency resolution failed" ;;
|
||||||
|
212) echo "Python: Installation aborted (permissions or EXTERNALLY-MANAGED)" ;;
|
||||||
|
|
||||||
|
# --- PostgreSQL ---
|
||||||
|
231) echo "PostgreSQL: Connection failed (server not running / wrong socket)" ;;
|
||||||
|
232) echo "PostgreSQL: Authentication failed (bad user/password)" ;;
|
||||||
|
233) echo "PostgreSQL: Database does not exist" ;;
|
||||||
|
234) echo "PostgreSQL: Fatal error in query / syntax" ;;
|
||||||
|
|
||||||
|
# --- MySQL / MariaDB ---
|
||||||
|
241) echo "MySQL/MariaDB: Connection failed (server not running / wrong socket)" ;;
|
||||||
|
242) echo "MySQL/MariaDB: Authentication failed (bad user/password)" ;;
|
||||||
|
243) echo "MySQL/MariaDB: Database does not exist" ;;
|
||||||
|
244) echo "MySQL/MariaDB: Fatal error in query / syntax" ;;
|
||||||
|
|
||||||
|
# --- MongoDB ---
|
||||||
|
251) echo "MongoDB: Connection failed (server not running)" ;;
|
||||||
|
252) echo "MongoDB: Authentication failed (bad user/password)" ;;
|
||||||
|
253) echo "MongoDB: Database not found" ;;
|
||||||
|
254) echo "MongoDB: Fatal query error" ;;
|
||||||
|
|
||||||
|
# --- Proxmox Custom Codes ---
|
||||||
|
200) echo "Custom: Failed to create lock file" ;;
|
||||||
|
203) echo "Custom: Missing CTID variable" ;;
|
||||||
|
204) echo "Custom: Missing PCT_OSTYPE variable" ;;
|
||||||
|
205) echo "Custom: Invalid CTID (<100)" ;;
|
||||||
|
206) echo "Custom: CTID already in use (check 'pct list' and /etc/pve/lxc/)" ;;
|
||||||
|
207) echo "Custom: Password contains unescaped special characters (-, /, \\, *, etc.)" ;;
|
||||||
|
208) echo "Custom: Invalid configuration (DNS/MAC/Network format error)" ;;
|
||||||
|
209) echo "Custom: Container creation failed (check logs for pct create output)" ;;
|
||||||
|
210) echo "Custom: Cluster not quorate" ;;
|
||||||
|
211) echo "Custom: Timeout waiting for template lock (concurrent download in progress)" ;;
|
||||||
|
214) echo "Custom: Not enough storage space" ;;
|
||||||
|
215) echo "Custom: Container created but not listed (ghost state - check /etc/pve/lxc/)" ;;
|
||||||
|
216) echo "Custom: RootFS entry missing in config (incomplete creation)" ;;
|
||||||
|
217) echo "Custom: Storage does not support rootdir (check storage capabilities)" ;;
|
||||||
|
218) echo "Custom: Template file corrupted or incomplete download (size <1MB or invalid archive)" ;;
|
||||||
|
220) echo "Custom: Unable to resolve template path" ;;
|
||||||
|
221) echo "Custom: Template file exists but not readable (check file permissions)" ;;
|
||||||
|
222) echo "Custom: Template download failed after 3 attempts (network/storage issue)" ;;
|
||||||
|
223) echo "Custom: Template not available after download (storage sync issue)" ;;
|
||||||
|
225) echo "Custom: No template available for OS/Version (check 'pveam available')" ;;
|
||||||
|
231) echo "Custom: LXC stack upgrade/retry failed (outdated pve-container - check https://github.com/community-scripts/ProxmoxVE/discussions/8126)" ;;
|
||||||
|
|
||||||
|
# --- Default ---
|
||||||
|
*) echo "Unknown error" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# SECTION 2: TELEMETRY FUNCTIONS
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# post_to_api()
|
||||||
|
#
|
||||||
|
# - Sends LXC container creation statistics to Community-Scripts API
|
||||||
|
# - Only executes if:
|
||||||
|
# * curl is available
|
||||||
|
# * DIAGNOSTICS=yes
|
||||||
|
# * RANDOM_UUID is set
|
||||||
|
# - Payload includes:
|
||||||
|
# * Container type, disk size, CPU cores, RAM
|
||||||
|
# * OS type and version
|
||||||
|
# * IPv6 disable status
|
||||||
|
# * Application name (NSAPP)
|
||||||
|
# * Installation method
|
||||||
|
# * PVE version
|
||||||
|
# * Status: "installing"
|
||||||
|
# * Random UUID for session tracking
|
||||||
|
# - Anonymous telemetry (no personal data)
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
post_to_api() {
|
post_to_api() {
|
||||||
|
|
||||||
if ! command -v curl &>/dev/null; then
|
if ! command -v curl &>/dev/null; then
|
||||||
@ -38,14 +185,26 @@ post_to_api() {
|
|||||||
}
|
}
|
||||||
EOF
|
EOF
|
||||||
)
|
)
|
||||||
|
|
||||||
if [[ "$DIAGNOSTICS" == "yes" ]]; then
|
if [[ "$DIAGNOSTICS" == "yes" ]]; then
|
||||||
RESPONSE=$(curl -s -w "%{http_code}" -L -X POST "$API_URL" --post301 --post302 \
|
RESPONSE=$(curl -s -w "%{http_code}" -L -X POST "$API_URL" --post301 --post302 \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d "$JSON_PAYLOAD") || true
|
-d "$JSON_PAYLOAD") || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# post_to_api_vm()
|
||||||
|
#
|
||||||
|
# - Sends VM creation statistics to Community-Scripts API
|
||||||
|
# - Similar to post_to_api() but for virtual machines (not containers)
|
||||||
|
# - Reads DIAGNOSTICS from /usr/local/community-scripts/diagnostics file
|
||||||
|
# - Payload differences:
|
||||||
|
# * ct_type=2 (VM instead of LXC)
|
||||||
|
# * type="vm"
|
||||||
|
# * Disk size without 'G' suffix (parsed from DISK_SIZE variable)
|
||||||
|
# - Only executes if DIAGNOSTICS=yes and RANDOM_UUID is set
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
post_to_api_vm() {
|
post_to_api_vm() {
|
||||||
|
|
||||||
if [[ ! -f /usr/local/community-scripts/diagnostics ]]; then
|
if [[ ! -f /usr/local/community-scripts/diagnostics ]]; then
|
||||||
@ -88,7 +247,6 @@ post_to_api_vm() {
|
|||||||
}
|
}
|
||||||
EOF
|
EOF
|
||||||
)
|
)
|
||||||
|
|
||||||
if [[ "$DIAGNOSTICS" == "yes" ]]; then
|
if [[ "$DIAGNOSTICS" == "yes" ]]; then
|
||||||
RESPONSE=$(curl -s -w "%{http_code}" -L -X POST "$API_URL" --post301 --post302 \
|
RESPONSE=$(curl -s -w "%{http_code}" -L -X POST "$API_URL" --post301 --post302 \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
@ -96,19 +254,54 @@ EOF
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
POST_UPDATE_DONE=false
|
# ------------------------------------------------------------------------------
|
||||||
|
# post_update_to_api()
|
||||||
|
#
|
||||||
|
# - Reports installation completion status to API
|
||||||
|
# - Prevents duplicate submissions via POST_UPDATE_DONE flag
|
||||||
|
# - Arguments:
|
||||||
|
# * $1: status ("success" or "failed")
|
||||||
|
# * $2: exit_code (default: 1 for failed, 0 for success)
|
||||||
|
# - Payload includes:
|
||||||
|
# * Final status (success/failed)
|
||||||
|
# * Error description via get_error_description()
|
||||||
|
# * Random UUID for session correlation
|
||||||
|
# - Only executes once per session
|
||||||
|
# - Silently returns if:
|
||||||
|
# * curl not available
|
||||||
|
# * Already reported (POST_UPDATE_DONE=true)
|
||||||
|
# * DIAGNOSTICS=no
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
post_update_to_api() {
|
post_update_to_api() {
|
||||||
|
|
||||||
if ! command -v curl &>/dev/null; then
|
if ! command -v curl &>/dev/null; then
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Initialize flag if not set (prevents 'unbound variable' error with set -u)
|
||||||
|
POST_UPDATE_DONE=${POST_UPDATE_DONE:-false}
|
||||||
|
|
||||||
if [ "$POST_UPDATE_DONE" = true ]; then
|
if [ "$POST_UPDATE_DONE" = true ]; then
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
exit_code=${2:-1}
|
||||||
local API_URL="http://api.community-scripts.org/upload/updatestatus"
|
local API_URL="http://api.community-scripts.org/upload/updatestatus"
|
||||||
local status="${1:-failed}"
|
local status="${1:-failed}"
|
||||||
local error="${2:-No error message}"
|
if [[ "$status" == "failed" ]]; then
|
||||||
|
local exit_code="${2:-1}"
|
||||||
|
elif [[ "$status" == "success" ]]; then
|
||||||
|
local exit_code="${2:-0}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$exit_code" ]]; then
|
||||||
|
exit_code=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
error=$(explain_exit_code "$exit_code")
|
||||||
|
|
||||||
|
if [ -z "$error" ]; then
|
||||||
|
error="Unknown error"
|
||||||
|
fi
|
||||||
|
|
||||||
JSON_PAYLOAD=$(
|
JSON_PAYLOAD=$(
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
@ -119,7 +312,6 @@ post_update_to_api() {
|
|||||||
}
|
}
|
||||||
EOF
|
EOF
|
||||||
)
|
)
|
||||||
|
|
||||||
if [[ "$DIAGNOSTICS" == "yes" ]]; then
|
if [[ "$DIAGNOSTICS" == "yes" ]]; then
|
||||||
RESPONSE=$(curl -s -w "%{http_code}" -L -X POST "$API_URL" --post301 --post302 \
|
RESPONSE=$(curl -s -w "%{http_code}" -L -X POST "$API_URL" --post301 --post302 \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
|
|||||||
4216
misc/build.func
4216
misc/build.func
File diff suppressed because it is too large
Load Diff
@ -1,699 +0,0 @@
|
|||||||
config_file() {
|
|
||||||
CONFIG_FILE="/opt/community-scripts/.settings"
|
|
||||||
|
|
||||||
if [[ -f "/opt/community-scripts/${NSAPP}.conf" ]]; then
|
|
||||||
CONFIG_FILE="/opt/community-scripts/${NSAPP}.conf"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if CONFIG_FILE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set absolute path to config file" 8 58 "$CONFIG_FILE" --title "CONFIG FILE" 3>&1 1>&2 2>&3); then
|
|
||||||
if [[ ! -f "$CONFIG_FILE" ]]; then
|
|
||||||
echo -e "${CROSS}${RD}Config file not found, exiting script!.${CL}"
|
|
||||||
exit
|
|
||||||
else
|
|
||||||
echo -e "${INFO}${BOLD}${DGN}Using config File: ${BGN}$CONFIG_FILE${CL}"
|
|
||||||
source "$CONFIG_FILE"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
if [[ -n "${CT_ID-}" ]]; then
|
|
||||||
if [[ "$CT_ID" =~ ^([0-9]{3,4})-([0-9]{3,4})$ ]]; then
|
|
||||||
MIN_ID=${BASH_REMATCH[1]}
|
|
||||||
MAX_ID=${BASH_REMATCH[2]}
|
|
||||||
if ((MIN_ID >= MAX_ID)); then
|
|
||||||
msg_error "Invalid Container ID range. The first number must be smaller than the second number, was ${CT_ID}"
|
|
||||||
exit
|
|
||||||
fi
|
|
||||||
|
|
||||||
LIST_OF_IDS=$(pvesh get /cluster/resources --type vm --output-format json 2>/dev/null | grep -oP '"vmid":\s*\K\d+') || true
|
|
||||||
if [[ -n "$LIST_OF_IDS" ]]; then
|
|
||||||
for ((ID = MIN_ID; ID <= MAX_ID; ID++)); do
|
|
||||||
if ! grep -q "^$ID$" <<<"$LIST_OF_IDS"; then
|
|
||||||
CT_ID=$ID
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}$CT_ID${CL}"
|
|
||||||
|
|
||||||
elif [[ "$CT_ID" =~ ^[0-9]+$ ]]; then
|
|
||||||
LIST_OF_IDS=$(pvesh get /cluster/resources --type vm --output-format json 2>/dev/null | grep -oP '"vmid":\s*\K\d+') || true
|
|
||||||
if [[ -n "$LIST_OF_IDS" ]]; then
|
|
||||||
|
|
||||||
if ! grep -q "^$CT_ID$" <<<"$LIST_OF_IDS"; then
|
|
||||||
echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}$CT_ID${CL}"
|
|
||||||
else
|
|
||||||
msg_error "Container ID $CT_ID already exists"
|
|
||||||
exit
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}$CT_ID${CL}"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
msg_error "Invalid Container ID format. Needs to be 0000-9999 or 0-9999, was ${CT_ID}"
|
|
||||||
exit
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
if CT_ID=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Container ID" 8 58 "$NEXTID" --title "CONTAINER ID" 3>&1 1>&2 2>&3); then
|
|
||||||
if [ -z "$CT_ID" ]; then
|
|
||||||
CT_ID="$NEXTID"
|
|
||||||
echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}$CT_ID${CL}"
|
|
||||||
else
|
|
||||||
echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}$CT_ID${CL}"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
exit_script
|
|
||||||
fi
|
|
||||||
|
|
||||||
fi
|
|
||||||
if [[ -n "${CT_TYPE-}" ]]; then
|
|
||||||
if [[ "$CT_TYPE" -eq 0 ]]; then
|
|
||||||
CT_TYPE_DESC="Privileged"
|
|
||||||
elif [[ "$CT_TYPE" -eq 1 ]]; then
|
|
||||||
CT_TYPE_DESC="Unprivileged"
|
|
||||||
else
|
|
||||||
msg_error "Unknown setting for CT_TYPE, should be 1 or 0, was ${CT_TYPE}"
|
|
||||||
exit
|
|
||||||
fi
|
|
||||||
echo -e "${CONTAINERTYPE}${BOLD}${DGN}Container Type: ${BGN}$CT_TYPE_DESC${CL}"
|
|
||||||
else
|
|
||||||
if CT_TYPE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "CONTAINER TYPE" --radiolist "Choose Type" 10 58 2 \
|
|
||||||
"1" "Unprivileged" ON \
|
|
||||||
"0" "Privileged" OFF \
|
|
||||||
3>&1 1>&2 2>&3); then
|
|
||||||
if [ -n "$CT_TYPE" ]; then
|
|
||||||
CT_TYPE_DESC="Unprivileged"
|
|
||||||
if [ "$CT_TYPE" -eq 0 ]; then
|
|
||||||
CT_TYPE_DESC="Privileged"
|
|
||||||
fi
|
|
||||||
echo -e "${CONTAINERTYPE}${BOLD}${DGN}Container Type: ${BGN}$CT_TYPE_DESC${CL}"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
exit_script
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -n "${PW-}" ]]; then
|
|
||||||
if [[ "$PW" == "none" ]]; then
|
|
||||||
PW=""
|
|
||||||
else
|
|
||||||
if [[ "$PW" == *" "* ]]; then
|
|
||||||
msg_error "Password cannot be empty"
|
|
||||||
exit
|
|
||||||
elif [[ ${#PW} -lt 5 ]]; then
|
|
||||||
msg_error "Password must be at least 5 characters long"
|
|
||||||
exit
|
|
||||||
else
|
|
||||||
echo -e "${VERIFYPW}${BOLD}${DGN}Root Password: ${BGN}********${CL}"
|
|
||||||
fi
|
|
||||||
PW="-password $PW"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
while true; do
|
|
||||||
if PW1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --passwordbox "\nSet Root Password (needed for root ssh access)" 9 58 --title "PASSWORD (leave blank for automatic login)" 3>&1 1>&2 2>&3); then
|
|
||||||
if [[ -n "$PW1" ]]; then
|
|
||||||
if [[ "$PW1" == *" "* ]]; then
|
|
||||||
whiptail --msgbox "Password cannot contain spaces. Please try again." 8 58
|
|
||||||
elif [ ${#PW1} -lt 5 ]; then
|
|
||||||
whiptail --msgbox "Password must be at least 5 characters long. Please try again." 8 58
|
|
||||||
else
|
|
||||||
if PW2=$(whiptail --backtitle "Proxmox VE Helper Scripts" --passwordbox "\nVerify Root Password" 9 58 --title "PASSWORD VERIFICATION" 3>&1 1>&2 2>&3); then
|
|
||||||
if [[ "$PW1" == "$PW2" ]]; then
|
|
||||||
PW="-password $PW1"
|
|
||||||
echo -e "${VERIFYPW}${BOLD}${DGN}Root Password: ${BGN}********${CL}"
|
|
||||||
break
|
|
||||||
else
|
|
||||||
whiptail --msgbox "Passwords do not match. Please try again." 8 58
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
exit_script
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
PW1="Automatic Login"
|
|
||||||
PW=""
|
|
||||||
echo -e "${VERIFYPW}${BOLD}${DGN}Root Password: ${BGN}$PW1${CL}"
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
exit_script
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -n "${HN-}" ]]; then
|
|
||||||
echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}$HN${CL}"
|
|
||||||
else
|
|
||||||
if CT_NAME=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Hostname" 8 58 "$NSAPP" --title "HOSTNAME" 3>&1 1>&2 2>&3); then
|
|
||||||
if [ -z "$CT_NAME" ]; then
|
|
||||||
HN="$NSAPP"
|
|
||||||
else
|
|
||||||
HN=$(echo "${CT_NAME,,}" | tr -d ' ')
|
|
||||||
fi
|
|
||||||
echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}$HN${CL}"
|
|
||||||
else
|
|
||||||
exit_script
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -n "${DISK_SIZE-}" ]]; then
|
|
||||||
if [[ "$DISK_SIZE" =~ ^-?[0-9]+$ ]]; then
|
|
||||||
echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE} GB${CL}"
|
|
||||||
else
|
|
||||||
msg_error "DISK_SIZE must be an integer, was ${DISK_SIZE}"
|
|
||||||
exit
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
if DISK_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Disk Size in GB" 8 58 "$var_disk" --title "DISK SIZE" 3>&1 1>&2 2>&3); then
|
|
||||||
if [ -z "$DISK_SIZE" ]; then
|
|
||||||
DISK_SIZE="$var_disk"
|
|
||||||
echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE} GB${CL}"
|
|
||||||
else
|
|
||||||
if ! [[ $DISK_SIZE =~ $INTEGER ]]; then
|
|
||||||
echo -e "{INFO}${HOLD}${RD} DISK SIZE MUST BE AN INTEGER NUMBER!${CL}"
|
|
||||||
advanced_settings
|
|
||||||
fi
|
|
||||||
echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE} GB${CL}"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
exit_script
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -n "${CORE_COUNT-}" ]]; then
|
|
||||||
if [[ "$CORE_COUNT" =~ ^-?[0-9]+$ ]]; then
|
|
||||||
echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}${CORE_COUNT}${CL}"
|
|
||||||
else
|
|
||||||
msg_error "CORE_COUNT must be an integer, was ${CORE_COUNT}"
|
|
||||||
exit
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
if CORE_COUNT=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Allocate CPU Cores" 8 58 "$var_cpu" --title "CORE COUNT" 3>&1 1>&2 2>&3); then
|
|
||||||
if [ -z "$CORE_COUNT" ]; then
|
|
||||||
CORE_COUNT="$var_cpu"
|
|
||||||
echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}$CORE_COUNT${CL}"
|
|
||||||
else
|
|
||||||
echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}$CORE_COUNT${CL}"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
exit_script
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -n "${RAM_SIZE-}" ]]; then
|
|
||||||
if [[ "$RAM_SIZE" =~ ^-?[0-9]+$ ]]; then
|
|
||||||
echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE} MiB${CL}"
|
|
||||||
else
|
|
||||||
msg_error "RAM_SIZE must be an integer, was ${RAM_SIZE}"
|
|
||||||
exit
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
if RAM_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Allocate RAM in MiB" 8 58 "$var_ram" --title "RAM" 3>&1 1>&2 2>&3); then
|
|
||||||
if [ -z "$RAM_SIZE" ]; then
|
|
||||||
RAM_SIZE="$var_ram"
|
|
||||||
echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE} MiB${CL}"
|
|
||||||
else
|
|
||||||
echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE} MiB${CL}"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
exit_script
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
IFACE_FILEPATH_LIST="/etc/network/interfaces"$'\n'$(find "/etc/network/interfaces.d/" -type f)
|
|
||||||
BRIDGES=""
|
|
||||||
OLD_IFS=$IFS
|
|
||||||
IFS=$'\n'
|
|
||||||
|
|
||||||
for iface_filepath in ${IFACE_FILEPATH_LIST}; do
|
|
||||||
|
|
||||||
iface_indexes_tmpfile=$(mktemp -q -u '.iface-XXXX')
|
|
||||||
(grep -Pn '^\s*iface' "${iface_filepath}" | cut -d':' -f1 && wc -l "${iface_filepath}" | cut -d' ' -f1) | awk 'FNR==1 {line=$0; next} {print line":"$0-1; line=$0}' >"${iface_indexes_tmpfile}" || true
|
|
||||||
|
|
||||||
if [ -f "${iface_indexes_tmpfile}" ]; then
|
|
||||||
|
|
||||||
while read -r pair; do
|
|
||||||
start=$(echo "${pair}" | cut -d':' -f1)
|
|
||||||
end=$(echo "${pair}" | cut -d':' -f2)
|
|
||||||
if awk "NR >= ${start} && NR <= ${end}" "${iface_filepath}" | grep -qP '^\s*(bridge[-_](ports|stp|fd|vlan-aware|vids)|ovs_type\s+OVSBridge)\b'; then
|
|
||||||
iface_name=$(sed "${start}q;d" "${iface_filepath}" | awk '{print $2}')
|
|
||||||
BRIDGES="${iface_name}"$'\n'"${BRIDGES}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
done <"${iface_indexes_tmpfile}"
|
|
||||||
rm -f "${iface_indexes_tmpfile}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
done
|
|
||||||
IFS=$OLD_IFS
|
|
||||||
BRIDGES=$(echo "$BRIDGES" | grep -v '^\s*$' | sort | uniq)
|
|
||||||
|
|
||||||
if [[ -n "${BRG-}" ]]; then
|
|
||||||
if echo "$BRIDGES" | grep -q "${BRG}"; then
|
|
||||||
echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}"
|
|
||||||
else
|
|
||||||
msg_error "Bridge '${BRG}' does not exist in /etc/network/interfaces or /etc/network/interfaces.d/sdn"
|
|
||||||
exit
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
BRG=$(whiptail --backtitle "Proxmox VE Helper Scripts" --menu "Select network bridge:" 15 40 6 $(echo "$BRIDGES" | awk '{print $0, "Bridge"}') 3>&1 1>&2 2>&3)
|
|
||||||
if [ -z "$BRG" ]; then
|
|
||||||
exit_script
|
|
||||||
else
|
|
||||||
echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
local ip_cidr_regex='^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})/([0-9]{1,2})$'
|
|
||||||
local ip_regex='^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$'
|
|
||||||
|
|
||||||
if [[ -n ${NET-} ]]; then
|
|
||||||
if [ "$NET" == "dhcp" ]; then
|
|
||||||
echo -e "${NETWORK}${BOLD}${DGN}IP Address: ${BGN}DHCP${CL}"
|
|
||||||
echo -e "${GATEWAY}${BOLD}${DGN}Gateway IP Address: ${BGN}Default${CL}"
|
|
||||||
GATE=""
|
|
||||||
elif [[ "$NET" =~ $ip_cidr_regex ]]; then
|
|
||||||
echo -e "${NETWORK}${BOLD}${DGN}IP Address: ${BGN}$NET${CL}"
|
|
||||||
if [[ -n "$GATE" ]]; then
|
|
||||||
[[ "$GATE" =~ ",gw=" ]] && GATE="${GATE##,gw=}"
|
|
||||||
if [[ "$GATE" =~ $ip_regex ]]; then
|
|
||||||
echo -e "${GATEWAY}${BOLD}${DGN}Gateway IP Address: ${BGN}$GATE${CL}"
|
|
||||||
GATE=",gw=$GATE"
|
|
||||||
else
|
|
||||||
msg_error "Invalid IP Address format for Gateway. Needs to be 0.0.0.0, was ${GATE}"
|
|
||||||
exit
|
|
||||||
fi
|
|
||||||
|
|
||||||
else
|
|
||||||
while true; do
|
|
||||||
GATE1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Enter gateway IP address" 8 58 --title "Gateway IP" 3>&1 1>&2 2>&3)
|
|
||||||
if [ -z "$GATE1" ]; then
|
|
||||||
whiptail --backtitle "Proxmox VE Helper Scripts" --msgbox "Gateway IP address cannot be empty" 8 58
|
|
||||||
elif [[ ! "$GATE1" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
|
|
||||||
whiptail --backtitle "Proxmox VE Helper Scripts" --msgbox "Invalid IP address format" 8 58
|
|
||||||
else
|
|
||||||
GATE=",gw=$GATE1"
|
|
||||||
echo -e "${GATEWAY}${BOLD}${DGN}Gateway IP Address: ${BGN}$GATE1${CL}"
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
elif [[ "$NET" == *-* ]]; then
|
|
||||||
IFS="-" read -r ip_start ip_end <<<"$NET"
|
|
||||||
|
|
||||||
if [[ ! "$ip_start" =~ $ip_cidr_regex ]] || [[ ! "$ip_end" =~ $ip_cidr_regex ]]; then
|
|
||||||
msg_error "Invalid IP range format, was $NET should be 0.0.0.0/0-0.0.0.0/0"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
ip1="${ip_start%%/*}"
|
|
||||||
ip2="${ip_end%%/*}"
|
|
||||||
cidr="${ip_start##*/}"
|
|
||||||
|
|
||||||
ip_to_int() {
|
|
||||||
local IFS=.
|
|
||||||
read -r i1 i2 i3 i4 <<<"$1"
|
|
||||||
echo $(((i1 << 24) + (i2 << 16) + (i3 << 8) + i4))
|
|
||||||
}
|
|
||||||
|
|
||||||
int_to_ip() {
|
|
||||||
local ip=$1
|
|
||||||
echo "$(((ip >> 24) & 0xFF)).$(((ip >> 16) & 0xFF)).$(((ip >> 8) & 0xFF)).$((ip & 0xFF))"
|
|
||||||
}
|
|
||||||
|
|
||||||
start_int=$(ip_to_int "$ip1")
|
|
||||||
end_int=$(ip_to_int "$ip2")
|
|
||||||
|
|
||||||
for ((ip_int = start_int; ip_int <= end_int; ip_int++)); do
|
|
||||||
ip=$(int_to_ip $ip_int)
|
|
||||||
msg_info "Checking IP: $ip"
|
|
||||||
if ! ping -c 2 -W 1 "$ip" >/dev/null 2>&1; then
|
|
||||||
NET="$ip/$cidr"
|
|
||||||
msg_ok "Using free IP Address: ${BGN}$NET${CL}"
|
|
||||||
sleep 3
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
if [[ "$NET" == *-* ]]; then
|
|
||||||
msg_error "No free IP found in range"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [ -n "$GATE" ]; then
|
|
||||||
if [[ "$GATE" =~ $ip_regex ]]; then
|
|
||||||
echo -e "${GATEWAY}${BOLD}${DGN}Gateway IP Address: ${BGN}$GATE${CL}"
|
|
||||||
GATE=",gw=$GATE"
|
|
||||||
else
|
|
||||||
msg_error "Invalid IP Address format for Gateway. Needs to be 0.0.0.0, was ${GATE}"
|
|
||||||
exit
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
while true; do
|
|
||||||
GATE1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Enter gateway IP address" 8 58 --title "Gateway IP" 3>&1 1>&2 2>&3)
|
|
||||||
if [ -z "$GATE1" ]; then
|
|
||||||
whiptail --backtitle "Proxmox VE Helper Scripts" --msgbox "Gateway IP address cannot be empty" 8 58
|
|
||||||
elif [[ ! "$GATE1" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
|
|
||||||
whiptail --backtitle "Proxmox VE Helper Scripts" --msgbox "Invalid IP address format" 8 58
|
|
||||||
else
|
|
||||||
GATE=",gw=$GATE1"
|
|
||||||
echo -e "${GATEWAY}${BOLD}${DGN}Gateway IP Address: ${BGN}$GATE1${CL}"
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
msg_error "Invalid IP Address format. Needs to be 0.0.0.0/0 or a range like 10.0.0.1/24-10.0.0.10/24, was ${NET}"
|
|
||||||
exit
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
while true; do
|
|
||||||
NET=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a Static IPv4 CIDR Address (/24)" 8 58 dhcp --title "IP ADDRESS" 3>&1 1>&2 2>&3)
|
|
||||||
exit_status=$?
|
|
||||||
if [ $exit_status -eq 0 ]; then
|
|
||||||
if [ "$NET" = "dhcp" ]; then
|
|
||||||
echo -e "${NETWORK}${BOLD}${DGN}IP Address: ${BGN}$NET${CL}"
|
|
||||||
break
|
|
||||||
else
|
|
||||||
if [[ "$NET" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}/([0-9]|[1-2][0-9]|3[0-2])$ ]]; then
|
|
||||||
echo -e "${NETWORK}${BOLD}${DGN}IP Address: ${BGN}$NET${CL}"
|
|
||||||
break
|
|
||||||
else
|
|
||||||
whiptail --backtitle "Proxmox VE Helper Scripts" --msgbox "$NET is an invalid IPv4 CIDR address. Please enter a valid IPv4 CIDR address or 'dhcp'" 8 58
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
exit_script
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
if [ "$NET" != "dhcp" ]; then
|
|
||||||
while true; do
|
|
||||||
GATE1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Enter gateway IP address" 8 58 --title "Gateway IP" 3>&1 1>&2 2>&3)
|
|
||||||
if [ -z "$GATE1" ]; then
|
|
||||||
whiptail --backtitle "Proxmox VE Helper Scripts" --msgbox "Gateway IP address cannot be empty" 8 58
|
|
||||||
elif [[ ! "$GATE1" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
|
|
||||||
whiptail --backtitle "Proxmox VE Helper Scripts" --msgbox "Invalid IP address format" 8 58
|
|
||||||
else
|
|
||||||
GATE=",gw=$GATE1"
|
|
||||||
echo -e "${GATEWAY}${BOLD}${DGN}Gateway IP Address: ${BGN}$GATE1${CL}"
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
else
|
|
||||||
GATE=""
|
|
||||||
echo -e "${GATEWAY}${BOLD}${DGN}Gateway IP Address: ${BGN}Default${CL}"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$var_os" == "alpine" ]; then
|
|
||||||
APT_CACHER=""
|
|
||||||
APT_CACHER_IP=""
|
|
||||||
else
|
|
||||||
if [[ -n "${APT_CACHER_IP-}" ]]; then
|
|
||||||
if [[ ! $APT_CACHER_IP == "none" ]]; then
|
|
||||||
APT_CACHER="yes"
|
|
||||||
echo -e "${NETWORK}${BOLD}${DGN}APT-CACHER IP Address: ${BGN}$APT_CACHER_IP${CL}"
|
|
||||||
else
|
|
||||||
APT_CACHER=""
|
|
||||||
echo -e "${NETWORK}${BOLD}${DGN}APT-Cacher IP Address: ${BGN}No${CL}"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
if APT_CACHER_IP=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set APT-Cacher IP (leave blank for none)" 8 58 --title "APT-Cacher IP" 3>&1 1>&2 2>&3); then
|
|
||||||
APT_CACHER="${APT_CACHER_IP:+yes}"
|
|
||||||
echo -e "${NETWORK}${BOLD}${DGN}APT-Cacher IP Address: ${BGN}${APT_CACHER_IP:-Default}${CL}"
|
|
||||||
if [[ -n $APT_CACHER_IP ]]; then
|
|
||||||
APT_CACHER_IP="none"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
exit_script
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -n "${MTU-}" ]]; then
|
|
||||||
if [[ "$MTU" =~ ^-?[0-9]+$ ]]; then
|
|
||||||
echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}$MTU${CL}"
|
|
||||||
MTU=",mtu=$MTU"
|
|
||||||
else
|
|
||||||
msg_error "MTU must be an integer, was ${MTU}"
|
|
||||||
exit
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
if MTU1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Interface MTU Size (leave blank for default [The MTU of your selected vmbr, default is 1500])" 8 58 --title "MTU SIZE" 3>&1 1>&2 2>&3); then
|
|
||||||
if [ -z "$MTU1" ]; then
|
|
||||||
MTU1="Default"
|
|
||||||
MTU=""
|
|
||||||
else
|
|
||||||
MTU=",mtu=$MTU1"
|
|
||||||
fi
|
|
||||||
echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}$MTU1${CL}"
|
|
||||||
else
|
|
||||||
exit_script
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$IPV6_METHOD" == "static" ]]; then
|
|
||||||
if [[ -n "$IPV6STATIC" ]]; then
|
|
||||||
IP6=",ip6=${IPV6STATIC}"
|
|
||||||
echo -e "${NETWORK}${BOLD}${DGN}IPv6 Address: ${BGN}${IPV6STATIC}${CL}"
|
|
||||||
else
|
|
||||||
msg_error "IPV6_METHOD is set to static but IPV6STATIC is empty"
|
|
||||||
exit
|
|
||||||
fi
|
|
||||||
elif [[ "$IPV6_METHOD" == "auto" ]]; then
|
|
||||||
IP6=",ip6=auto"
|
|
||||||
echo -e "${NETWORK}${BOLD}${DGN}IPv6 Address: ${BGN}auto${CL}"
|
|
||||||
else
|
|
||||||
IP6=""
|
|
||||||
echo -e "${NETWORK}${BOLD}${DGN}IPv6 Address: ${BGN}none${CL}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -n "${SD-}" ]]; then
|
|
||||||
if [[ "$SD" == "none" ]]; then
|
|
||||||
SD=""
|
|
||||||
echo -e "${SEARCH}${BOLD}${DGN}DNS Search Domain: ${BGN}Host${CL}"
|
|
||||||
else
|
|
||||||
# Strip prefix if present for config file storage
|
|
||||||
local SD_VALUE="$SD"
|
|
||||||
[[ "$SD" =~ ^-searchdomain= ]] && SD_VALUE="${SD#-searchdomain=}"
|
|
||||||
echo -e "${SEARCH}${BOLD}${DGN}DNS Search Domain: ${BGN}$SD_VALUE${CL}"
|
|
||||||
SD="-searchdomain=$SD_VALUE"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
if SD=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a DNS Search Domain (leave blank for HOST)" 8 58 --title "DNS Search Domain" 3>&1 1>&2 2>&3); then
|
|
||||||
if [ -z "$SD" ]; then
|
|
||||||
SX=Host
|
|
||||||
SD=""
|
|
||||||
else
|
|
||||||
SX=$SD
|
|
||||||
SD="-searchdomain=$SD"
|
|
||||||
fi
|
|
||||||
echo -e "${SEARCH}${BOLD}${DGN}DNS Search Domain: ${BGN}$SX${CL}"
|
|
||||||
else
|
|
||||||
exit_script
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -n "${NS-}" ]]; then
|
|
||||||
if [[ $NS == "none" ]]; then
|
|
||||||
NS=""
|
|
||||||
echo -e "${NETWORK}${BOLD}${DGN}DNS Server IP Address: ${BGN}Host${CL}"
|
|
||||||
else
|
|
||||||
# Strip prefix if present for config file storage
|
|
||||||
local NS_VALUE="$NS"
|
|
||||||
[[ "$NS" =~ ^-nameserver= ]] && NS_VALUE="${NS#-nameserver=}"
|
|
||||||
if [[ "$NS_VALUE" =~ $ip_regex ]]; then
|
|
||||||
echo -e "${NETWORK}${BOLD}${DGN}DNS Server IP Address: ${BGN}$NS_VALUE${CL}"
|
|
||||||
NS="-nameserver=$NS_VALUE"
|
|
||||||
else
|
|
||||||
msg_error "Invalid IP Address format for DNS Server. Needs to be 0.0.0.0, was ${NS_VALUE}"
|
|
||||||
exit
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
if NX=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a DNS Server IP (leave blank for HOST)" 8 58 --title "DNS SERVER IP" 3>&1 1>&2 2>&3); then
|
|
||||||
if [ -z "$NX" ]; then
|
|
||||||
NX=Host
|
|
||||||
NS=""
|
|
||||||
else
|
|
||||||
NS="-nameserver=$NX"
|
|
||||||
fi
|
|
||||||
echo -e "${NETWORK}${BOLD}${DGN}DNS Server IP Address: ${BGN}$NX${CL}"
|
|
||||||
else
|
|
||||||
exit_script
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -n "${MAC-}" ]]; then
|
|
||||||
if [[ "$MAC" == "none" ]]; then
|
|
||||||
MAC=""
|
|
||||||
echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}Host${CL}"
|
|
||||||
else
|
|
||||||
# Strip prefix if present for config file storage
|
|
||||||
local MAC_VALUE="$MAC"
|
|
||||||
[[ "$MAC" =~ ^,hwaddr= ]] && MAC_VALUE="${MAC#,hwaddr=}"
|
|
||||||
if [[ "$MAC_VALUE" =~ ^([A-Fa-f0-9]{2}:){5}[A-Fa-f0-9]{2}$ ]]; then
|
|
||||||
echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}$MAC_VALUE${CL}"
|
|
||||||
MAC=",hwaddr=$MAC_VALUE"
|
|
||||||
else
|
|
||||||
msg_error "MAC Address must be in the format xx:xx:xx:xx:xx:xx, was ${MAC_VALUE}"
|
|
||||||
exit
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
if MAC1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a MAC Address(leave blank for generated MAC)" 8 58 --title "MAC ADDRESS" 3>&1 1>&2 2>&3); then
|
|
||||||
if [ -z "$MAC1" ]; then
|
|
||||||
MAC1="Default"
|
|
||||||
MAC=""
|
|
||||||
else
|
|
||||||
MAC=",hwaddr=$MAC1"
|
|
||||||
echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}$MAC1${CL}"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
exit_script
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -n "${VLAN-}" ]]; then
|
|
||||||
if [[ "$VLAN" == "none" ]]; then
|
|
||||||
VLAN=""
|
|
||||||
echo -e "${VLANTAG}${BOLD}${DGN}Vlan: ${BGN}Host${CL}"
|
|
||||||
else
|
|
||||||
# Strip prefix if present for config file storage
|
|
||||||
local VLAN_VALUE="$VLAN"
|
|
||||||
[[ "$VLAN" =~ ^,tag= ]] && VLAN_VALUE="${VLAN#,tag=}"
|
|
||||||
if [[ "$VLAN_VALUE" =~ ^-?[0-9]+$ ]]; then
|
|
||||||
echo -e "${VLANTAG}${BOLD}${DGN}Vlan: ${BGN}$VLAN_VALUE${CL}"
|
|
||||||
VLAN=",tag=$VLAN_VALUE"
|
|
||||||
else
|
|
||||||
msg_error "VLAN must be an integer, was ${VLAN_VALUE}"
|
|
||||||
exit
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
if VLAN1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a Vlan(leave blank for no VLAN)" 8 58 --title "VLAN" 3>&1 1>&2 2>&3); then
|
|
||||||
if [ -z "$VLAN1" ]; then
|
|
||||||
VLAN1="Default"
|
|
||||||
VLAN=""
|
|
||||||
else
|
|
||||||
VLAN=",tag=$VLAN1"
|
|
||||||
fi
|
|
||||||
echo -e "${VLANTAG}${BOLD}${DGN}Vlan: ${BGN}$VLAN1${CL}"
|
|
||||||
else
|
|
||||||
exit_script
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -n "${TAGS-}" ]]; then
|
|
||||||
if [[ "$TAGS" == *"DEFAULT"* ]]; then
|
|
||||||
TAGS="${TAGS//DEFAULT/}"
|
|
||||||
TAGS="${TAGS//;/}"
|
|
||||||
TAGS="$TAGS;${var_tags:-}"
|
|
||||||
echo -e "${NETWORK}${BOLD}${DGN}Tags: ${BGN}$TAGS${CL}"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
TAGS="community-scripts;"
|
|
||||||
if ADV_TAGS=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Custom Tags?[If you remove all, there will be no tags!]" 8 58 "${TAGS}" --title "Advanced Tags" 3>&1 1>&2 2>&3); then
|
|
||||||
if [ -n "${ADV_TAGS}" ]; then
|
|
||||||
ADV_TAGS=$(echo "$ADV_TAGS" | tr -d '[:space:]')
|
|
||||||
TAGS="${ADV_TAGS}"
|
|
||||||
else
|
|
||||||
TAGS=";"
|
|
||||||
fi
|
|
||||||
echo -e "${NETWORK}${BOLD}${DGN}Tags: ${BGN}$TAGS${CL}"
|
|
||||||
else
|
|
||||||
exit_script
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -n "${SSH-}" ]]; then
|
|
||||||
if [[ "$SSH" == "yes" ]]; then
|
|
||||||
echo -e "${ROOTSSH}${BOLD}${DGN}Root SSH Access: ${BGN}$SSH${CL}"
|
|
||||||
if [[ ! -z "$SSH_AUTHORIZED_KEY" ]]; then
|
|
||||||
echo -e "${ROOTSSH}${BOLD}${DGN}SSH Authorized Key: ${BGN}********************${CL}"
|
|
||||||
else
|
|
||||||
echo -e "${ROOTSSH}${BOLD}${DGN}SSH Authorized Key: ${BGN}None${CL}"
|
|
||||||
fi
|
|
||||||
elif [[ "$SSH" == "no" ]]; then
|
|
||||||
echo -e "${ROOTSSH}${BOLD}${DGN}Root SSH Access: ${BGN}$SSH${CL}"
|
|
||||||
else
|
|
||||||
msg_error "SSH needs to be 'yes' or 'no', was ${SSH}"
|
|
||||||
exit
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
SSH_AUTHORIZED_KEY="$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "SSH Authorized key for root (leave empty for none)" 8 58 --title "SSH Key" 3>&1 1>&2 2>&3)"
|
|
||||||
if [[ -z "${SSH_AUTHORIZED_KEY}" ]]; then
|
|
||||||
SSH_AUTHORIZED_KEY=""
|
|
||||||
fi
|
|
||||||
if [[ "$PW" == -password* || -n "$SSH_AUTHORIZED_KEY" ]]; then
|
|
||||||
if (whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "SSH ACCESS" --yesno "Enable Root SSH Access?" 10 58); then
|
|
||||||
SSH="yes"
|
|
||||||
else
|
|
||||||
SSH="no"
|
|
||||||
fi
|
|
||||||
echo -e "${ROOTSSH}${BOLD}${DGN}Root SSH Access: ${BGN}$SSH${CL}"
|
|
||||||
else
|
|
||||||
SSH="no"
|
|
||||||
echo -e "${ROOTSSH}${BOLD}${DGN}Root SSH Access: ${BGN}$SSH${CL}"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -n "$ENABLE_FUSE" ]]; then
|
|
||||||
if [[ "$ENABLE_FUSE" == "yes" ]]; then
|
|
||||||
echo -e "${FUSE}${BOLD}${DGN}Enable FUSE: ${BGN}Yes${CL}"
|
|
||||||
elif [[ "$ENABLE_FUSE" == "no" ]]; then
|
|
||||||
echo -e "${FUSE}${BOLD}${DGN}Enable FUSE: ${BGN}No${CL}"
|
|
||||||
else
|
|
||||||
msg_error "Enable FUSE needs to be 'yes' or 'no', was ${ENABLE_FUSE}"
|
|
||||||
exit
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
if (whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "FUSE" --yesno "Enable FUSE?" 10 58); then
|
|
||||||
ENABLE_FUSE="yes"
|
|
||||||
else
|
|
||||||
ENABLE_FUSE="no"
|
|
||||||
fi
|
|
||||||
echo -e "${FUSE}${BOLD}${DGN}Enable FUSE: ${BGN}$ENABLE_FUSE${CL}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -n "$ENABLE_TUN" ]]; then
|
|
||||||
if [[ "$ENABLE_TUN" == "yes" ]]; then
|
|
||||||
echo -e "${FUSE}${BOLD}${DGN}Enable TUN: ${BGN}Yes${CL}"
|
|
||||||
elif [[ "$ENABLE_TUN" == "no" ]]; then
|
|
||||||
echo -e "${FUSE}${BOLD}${DGN}Enable TUN: ${BGN}No${CL}"
|
|
||||||
else
|
|
||||||
msg_error "Enable TUN needs to be 'yes' or 'no', was ${ENABLE_TUN}"
|
|
||||||
exit
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
if (whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "TUN" --yesno "Enable TUN?" 10 58); then
|
|
||||||
ENABLE_TUN="yes"
|
|
||||||
else
|
|
||||||
ENABLE_TUN="no"
|
|
||||||
fi
|
|
||||||
echo -e "${FUSE}${BOLD}${DGN}Enable TUN: ${BGN}$ENABLE_TUN${CL}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -n "${VERBOSE-}" ]]; then
|
|
||||||
if [[ "$VERBOSE" == "yes" ]]; then
|
|
||||||
echo -e "${SEARCH}${BOLD}${DGN}Verbose Mode: ${BGN}$VERBOSE${CL}"
|
|
||||||
elif [[ "$VERBOSE" == "no" ]]; then
|
|
||||||
echo -e "${SEARCH}${BOLD}${DGN}Verbose Mode: ${BGN}No${CL}"
|
|
||||||
else
|
|
||||||
msg_error "Verbose Mode needs to be 'yes' or 'no', was ${VERBOSE}"
|
|
||||||
exit
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
if (whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "VERBOSE MODE" --yesno "Enable Verbose Mode?" 10 58); then
|
|
||||||
VERBOSE="yes"
|
|
||||||
else
|
|
||||||
VERBOSE="no"
|
|
||||||
fi
|
|
||||||
echo -e "${SEARCH}${BOLD}${DGN}Verbose Mode: ${BGN}$VERBOSE${CL}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "ADVANCED SETTINGS WITH CONFIG FILE COMPLETE" --yesno "Ready to create ${APP} LXC?" 10 58); then
|
|
||||||
echo -e "${CREATING}${BOLD}${RD}Creating a ${APP} LXC using the above settings${CL}"
|
|
||||||
else
|
|
||||||
clear
|
|
||||||
header_info
|
|
||||||
echo -e "${INFO}${HOLD} ${GN}Using Config File on node $PVEHOST_NAME${CL}"
|
|
||||||
config_file
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
774
misc/core.func
774
misc/core.func
@ -1,13 +1,35 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
# Copyright (c) 2021-2025 community-scripts ORG
|
# Copyright (c) 2021-2025 community-scripts ORG
|
||||||
# License: MIT | https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/LICENSE
|
# License: MIT | https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/LICENSE
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ==============================================================================
|
||||||
# Loads core utility groups once (colors, formatting, icons, defaults).
|
# CORE FUNCTIONS - LXC CONTAINER UTILITIES
|
||||||
# ------------------------------------------------------------------------------
|
# ==============================================================================
|
||||||
|
#
|
||||||
|
# This file provides core utility functions for LXC container management
|
||||||
|
# including colors, formatting, validation checks, message output, and
|
||||||
|
# execution helpers used throughout the Community-Scripts ecosystem.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# source <(curl -fsSL https://git.community-scripts.org/.../core.func)
|
||||||
|
# load_functions
|
||||||
|
#
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
[[ -n "${_CORE_FUNC_LOADED:-}" ]] && return
|
[[ -n "${_CORE_FUNC_LOADED:-}" ]] && return
|
||||||
_CORE_FUNC_LOADED=1
|
_CORE_FUNC_LOADED=1
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# SECTION 1: INITIALIZATION & SETUP
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# load_functions()
|
||||||
|
#
|
||||||
|
# - Initializes all core utility groups (colors, formatting, icons, defaults)
|
||||||
|
# - Ensures functions are loaded only once via __FUNCTIONS_LOADED flag
|
||||||
|
# - Must be called at start of any script using these utilities
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
load_functions() {
|
load_functions() {
|
||||||
[[ -n "${__FUNCTIONS_LOADED:-}" ]] && return
|
[[ -n "${__FUNCTIONS_LOADED:-}" ]] && return
|
||||||
__FUNCTIONS_LOADED=1
|
__FUNCTIONS_LOADED=1
|
||||||
@ -16,58 +38,14 @@ load_functions() {
|
|||||||
icons
|
icons
|
||||||
default_vars
|
default_vars
|
||||||
set_std_mode
|
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()
|
||||||
|
#
|
||||||
|
# - Sets ANSI color codes for styled terminal output
|
||||||
|
# - Variables: YW (yellow), YWB (yellow bright), BL (blue), RD (red)
|
||||||
|
# GN (green), DGN (dark green), BGN (background green), CL (clear)
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
color() {
|
color() {
|
||||||
YW=$(echo "\033[33m")
|
YW=$(echo "\033[33m")
|
||||||
@ -80,7 +58,14 @@ color() {
|
|||||||
CL=$(echo "\033[m")
|
CL=$(echo "\033[m")
|
||||||
}
|
}
|
||||||
|
|
||||||
# Special for spinner and colorized output via printf
|
# ------------------------------------------------------------------------------
|
||||||
|
# color_spinner()
|
||||||
|
#
|
||||||
|
# - Sets ANSI color codes specifically for spinner animation
|
||||||
|
# - Variables: CS_YW (spinner yellow), CS_YWB (spinner yellow bright),
|
||||||
|
# CS_CL (spinner clear)
|
||||||
|
# - Used by spinner() function to avoid color conflicts
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
color_spinner() {
|
color_spinner() {
|
||||||
CS_YW=$'\033[33m'
|
CS_YW=$'\033[33m'
|
||||||
CS_YWB=$'\033[93m'
|
CS_YWB=$'\033[93m'
|
||||||
@ -88,7 +73,12 @@ color_spinner() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
# Defines formatting helpers like tab, bold, and line reset sequences.
|
# formatting()
|
||||||
|
#
|
||||||
|
# - Defines formatting helpers for terminal output
|
||||||
|
# - BFR: Backspace and clear line sequence
|
||||||
|
# - BOLD: Bold text escape code
|
||||||
|
# - TAB/TAB3: Indentation spacing
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
formatting() {
|
formatting() {
|
||||||
BFR="\\r\\033[K"
|
BFR="\\r\\033[K"
|
||||||
@ -99,7 +89,11 @@ formatting() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
# Sets symbolic icons used throughout user feedback and prompts.
|
# icons()
|
||||||
|
#
|
||||||
|
# - Sets symbolic emoji icons used throughout user feedback
|
||||||
|
# - Provides consistent visual indicators for success, error, info, etc.
|
||||||
|
# - Icons: CM (checkmark), CROSS (error), INFO (info), HOURGLASS (wait), etc.
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
icons() {
|
icons() {
|
||||||
CM="${TAB}✔️${TAB}"
|
CM="${TAB}✔️${TAB}"
|
||||||
@ -130,21 +124,29 @@ icons() {
|
|||||||
ADVANCED="${TAB}🧩${TAB}${CL}"
|
ADVANCED="${TAB}🧩${TAB}${CL}"
|
||||||
FUSE="${TAB}🗂️${TAB}${CL}"
|
FUSE="${TAB}🗂️${TAB}${CL}"
|
||||||
HOURGLASS="${TAB}⏳${TAB}"
|
HOURGLASS="${TAB}⏳${TAB}"
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
# Sets default retry and wait variables used for system actions.
|
# default_vars()
|
||||||
|
#
|
||||||
|
# - Sets default retry and wait variables used for system actions
|
||||||
|
# - RETRY_NUM: Maximum number of retry attempts (default: 10)
|
||||||
|
# - RETRY_EVERY: Seconds to wait between retries (default: 3)
|
||||||
|
# - i: Counter variable initialized to RETRY_NUM
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
default_vars() {
|
default_vars() {
|
||||||
RETRY_NUM=10
|
RETRY_NUM=10
|
||||||
RETRY_EVERY=3
|
RETRY_EVERY=3
|
||||||
i=$RETRY_NUM
|
i=$RETRY_NUM
|
||||||
#[[ "${VAR_OS:-}" == "unknown" ]]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
# Sets default verbose mode for script and os execution.
|
# set_std_mode()
|
||||||
|
#
|
||||||
|
# - Sets default verbose mode for script and OS execution
|
||||||
|
# - If VERBOSE=yes: STD="" (show all output)
|
||||||
|
# - If VERBOSE=no: STD="silent" (suppress output via silent() wrapper)
|
||||||
|
# - If DEV_MODE_TRACE=true: Enables bash tracing (set -x)
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
set_std_mode() {
|
set_std_mode() {
|
||||||
if [ "${VERBOSE:-no}" = "yes" ]; then
|
if [ "${VERBOSE:-no}" = "yes" ]; then
|
||||||
@ -152,138 +154,338 @@ set_std_mode() {
|
|||||||
else
|
else
|
||||||
STD="silent"
|
STD="silent"
|
||||||
fi
|
fi
|
||||||
}
|
|
||||||
|
|
||||||
# Silent execution function
|
# Enable bash tracing if trace mode active
|
||||||
silent() {
|
if [[ "${DEV_MODE_TRACE:-false}" == "true" ]]; then
|
||||||
"$@" >/dev/null 2>&1
|
set -x
|
||||||
}
|
export PS4='+(${BASH_SOURCE}:${LINENO}): ${FUNCNAME[0]:+${FUNCNAME[0]}(): }'
|
||||||
|
|
||||||
# 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
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
ensure_tput() {
|
# ------------------------------------------------------------------------------
|
||||||
if ! command -v tput >/dev/null 2>&1; then
|
# parse_dev_mode()
|
||||||
if grep -qi 'alpine' /etc/os-release; then
|
#
|
||||||
apk add --no-cache ncurses >/dev/null 2>&1
|
# - Parses comma-separated dev_mode variable (e.g., "motd,keep,trace")
|
||||||
elif command -v apt-get >/dev/null 2>&1; then
|
# - Sets global flags for each mode:
|
||||||
apt-get update -qq >/dev/null
|
# * DEV_MODE_MOTD: Setup SSH/MOTD before installation
|
||||||
apt-get install -y -qq ncurses-bin >/dev/null 2>&1
|
# * DEV_MODE_KEEP: Never delete container on failure
|
||||||
|
# * DEV_MODE_TRACE: Enable bash set -x tracing
|
||||||
|
# * DEV_MODE_PAUSE: Pause after each msg_info step
|
||||||
|
# * DEV_MODE_BREAKPOINT: Open shell on error instead of cleanup
|
||||||
|
# * DEV_MODE_LOGS: Persist all logs to /var/log/community-scripts/
|
||||||
|
# * DEV_MODE_DRYRUN: Show commands without executing
|
||||||
|
# - Call this early in script execution
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
parse_dev_mode() {
|
||||||
|
local mode
|
||||||
|
# Initialize all flags to false
|
||||||
|
export DEV_MODE_MOTD=false
|
||||||
|
export DEV_MODE_KEEP=false
|
||||||
|
export DEV_MODE_TRACE=false
|
||||||
|
export DEV_MODE_PAUSE=false
|
||||||
|
export DEV_MODE_BREAKPOINT=false
|
||||||
|
export DEV_MODE_LOGS=false
|
||||||
|
export DEV_MODE_DRYRUN=false
|
||||||
|
|
||||||
|
# Parse comma-separated modes
|
||||||
|
if [[ -n "${dev_mode:-}" ]]; then
|
||||||
|
IFS=',' read -ra MODES <<<"$dev_mode"
|
||||||
|
for mode in "${MODES[@]}"; do
|
||||||
|
mode="$(echo "$mode" | xargs)" # Trim whitespace
|
||||||
|
case "$mode" in
|
||||||
|
motd) export DEV_MODE_MOTD=true ;;
|
||||||
|
keep) export DEV_MODE_KEEP=true ;;
|
||||||
|
trace) export DEV_MODE_TRACE=true ;;
|
||||||
|
pause) export DEV_MODE_PAUSE=true ;;
|
||||||
|
breakpoint) export DEV_MODE_BREAKPOINT=true ;;
|
||||||
|
logs) export DEV_MODE_LOGS=true ;;
|
||||||
|
dryrun) export DEV_MODE_DRYRUN=true ;;
|
||||||
|
*)
|
||||||
|
if declare -f msg_warn >/dev/null 2>&1; then
|
||||||
|
msg_warn "Unknown dev_mode: '$mode' (ignored)"
|
||||||
|
else
|
||||||
|
echo "[WARN] Unknown dev_mode: '$mode' (ignored)" >&2
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# Show active dev modes
|
||||||
|
local active_modes=()
|
||||||
|
[[ $DEV_MODE_MOTD == true ]] && active_modes+=("motd")
|
||||||
|
[[ $DEV_MODE_KEEP == true ]] && active_modes+=("keep")
|
||||||
|
[[ $DEV_MODE_TRACE == true ]] && active_modes+=("trace")
|
||||||
|
[[ $DEV_MODE_PAUSE == true ]] && active_modes+=("pause")
|
||||||
|
[[ $DEV_MODE_BREAKPOINT == true ]] && active_modes+=("breakpoint")
|
||||||
|
[[ $DEV_MODE_LOGS == true ]] && active_modes+=("logs")
|
||||||
|
[[ $DEV_MODE_DRYRUN == true ]] && active_modes+=("dryrun")
|
||||||
|
|
||||||
|
if [[ ${#active_modes[@]} -gt 0 ]]; then
|
||||||
|
if declare -f msg_custom >/dev/null 2>&1; then
|
||||||
|
msg_custom "🔧" "${YWB}" "Dev modes active: ${active_modes[*]}"
|
||||||
|
else
|
||||||
|
echo "[DEV] Active modes: ${active_modes[*]}" >&2
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
is_alpine() {
|
# ==============================================================================
|
||||||
local os_id="${var_os:-${PCT_OSTYPE:-}}"
|
# SECTION 2: VALIDATION CHECKS
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
if [[ -z "$os_id" && -f /etc/os-release ]]; then
|
# ------------------------------------------------------------------------------
|
||||||
os_id="$(
|
# shell_check()
|
||||||
. /etc/os-release 2>/dev/null
|
#
|
||||||
echo "${ID:-}"
|
# - Verifies that the script is running under Bash shell
|
||||||
)"
|
# - Exits with error message if different shell is detected
|
||||||
|
# - Required because scripts use Bash-specific features
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
shell_check() {
|
||||||
|
if [[ "$(ps -p $$ -o comm=)" != "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
|
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.
|
# root_check()
|
||||||
|
#
|
||||||
|
# - Verifies script is running with root privileges
|
||||||
|
# - Detects if executed via sudo (which can cause issues)
|
||||||
|
# - Exits with error if not running as root directly
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
__curl_err_handler() {
|
root_check() {
|
||||||
local exit_code="$1"
|
if [[ "$(id -u)" -ne 0 || $(ps -o comm= -p $PPID) == "sudo" ]]; then
|
||||||
local target="$2"
|
clear
|
||||||
local curl_msg="$3"
|
msg_error "Please run this script as root."
|
||||||
|
echo -e "\nExiting..."
|
||||||
|
sleep 2
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
case $exit_code in
|
# ------------------------------------------------------------------------------
|
||||||
1) msg_error "Unsupported protocol: $target" ;;
|
# pve_check()
|
||||||
2) msg_error "Curl init failed: $target" ;;
|
#
|
||||||
3) msg_error "Malformed URL: $target" ;;
|
# - Validates Proxmox VE version compatibility
|
||||||
5) msg_error "Proxy resolution failed: $target" ;;
|
# - Supported: PVE 8.0-8.9 and PVE 9.0-9.1
|
||||||
6) msg_error "Host resolution failed: $target" ;;
|
# - Exits with error message if unsupported version detected
|
||||||
7) msg_error "Connection failed: $target" ;;
|
# ------------------------------------------------------------------------------
|
||||||
9) msg_error "Access denied: $target" ;;
|
pve_check() {
|
||||||
18) msg_error "Partial file transfer: $target" ;;
|
local PVE_VER
|
||||||
22) msg_error "HTTP error (e.g. 400/404): $target" ;;
|
PVE_VER="$(pveversion | awk -F'/' '{print $2}' | awk -F'-' '{print $1}')"
|
||||||
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
|
# 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 9.0–9.1
|
||||||
|
if [[ "$PVE_VER" =~ ^9\.([0-9]+) ]]; then
|
||||||
|
local MINOR="${BASH_REMATCH[1]}"
|
||||||
|
if ((MINOR < 0 || MINOR > 1)); then
|
||||||
|
msg_error "This version of Proxmox VE is not yet supported."
|
||||||
|
msg_error "Supported: Proxmox VE version 9.0 – 9.1"
|
||||||
|
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.9 or 9.0 – 9.1"
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
fatal() {
|
# ------------------------------------------------------------------------------
|
||||||
msg_error "$1"
|
# arch_check()
|
||||||
kill -INT $$
|
#
|
||||||
|
# - Validates system architecture is amd64/x86_64
|
||||||
|
# - Exits with error message for unsupported architectures (e.g., ARM/PiMox)
|
||||||
|
# - Provides link to ARM64-compatible scripts
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# ssh_check()
|
||||||
|
#
|
||||||
|
# - Detects if script is running over SSH connection
|
||||||
|
# - Warns user for external SSH connections (recommends Proxmox shell)
|
||||||
|
# - Skips warning for local/same-subnet connections
|
||||||
|
# - Does not abort execution, only warns
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
ssh_check() {
|
||||||
|
if [ -n "$SSH_CLIENT" ]; then
|
||||||
|
local client_ip=$(awk '{print $1}' <<<"$SSH_CLIENT")
|
||||||
|
local host_ip=$(hostname -I | awk '{print $1}')
|
||||||
|
|
||||||
|
# Check if connection is local (Proxmox WebUI or same machine)
|
||||||
|
# - localhost (127.0.0.1, ::1)
|
||||||
|
# - same IP as host
|
||||||
|
# - local network range (10.x, 172.16-31.x, 192.168.x)
|
||||||
|
if [[ "$client_ip" == "127.0.0.1" || "$client_ip" == "::1" || "$client_ip" == "$host_ip" ]]; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if client is in same local network (optional, safer approach)
|
||||||
|
local host_subnet=$(echo "$host_ip" | cut -d. -f1-3)
|
||||||
|
local client_subnet=$(echo "$client_ip" | cut -d. -f1-3)
|
||||||
|
if [[ "$host_subnet" == "$client_subnet" ]]; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Only warn for truly external connections
|
||||||
|
msg_warn "Running via external SSH (client: $client_ip)."
|
||||||
|
msg_warn "For better stability, consider using the Proxmox Shell (Console) instead."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# SECTION 3: EXECUTION HELPERS
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# get_active_logfile()
|
||||||
|
#
|
||||||
|
# - Returns the appropriate log file based on execution context
|
||||||
|
# - BUILD_LOG: Host operations (container creation)
|
||||||
|
# - INSTALL_LOG: Container operations (application installation)
|
||||||
|
# - Fallback to BUILD_LOG if neither is set
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
get_active_logfile() {
|
||||||
|
if [[ -n "${INSTALL_LOG:-}" ]]; then
|
||||||
|
echo "$INSTALL_LOG"
|
||||||
|
elif [[ -n "${BUILD_LOG:-}" ]]; then
|
||||||
|
echo "$BUILD_LOG"
|
||||||
|
else
|
||||||
|
# Fallback for legacy scripts
|
||||||
|
echo "/tmp/build-$(date +%Y%m%d_%H%M%S).log"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Legacy compatibility: SILENT_LOGFILE points to active log
|
||||||
|
SILENT_LOGFILE="$(get_active_logfile)"
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# silent()
|
||||||
|
#
|
||||||
|
# - Executes command with output redirected to active log file
|
||||||
|
# - On error: displays last 10 lines of log and exits with original exit code
|
||||||
|
# - Temporarily disables error trap to capture exit code correctly
|
||||||
|
# - Sources explain_exit_code() for detailed error messages
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
silent() {
|
||||||
|
local cmd="$*"
|
||||||
|
local caller_line="${BASH_LINENO[0]:-unknown}"
|
||||||
|
local logfile="$(get_active_logfile)"
|
||||||
|
|
||||||
|
# Dryrun mode: Show command without executing
|
||||||
|
if [[ "${DEV_MODE_DRYRUN:-false}" == "true" ]]; then
|
||||||
|
if declare -f msg_custom >/dev/null 2>&1; then
|
||||||
|
msg_custom "🔍" "${BL}" "[DRYRUN] $cmd"
|
||||||
|
else
|
||||||
|
echo "[DRYRUN] $cmd" >&2
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
set +Eeuo pipefail
|
||||||
|
trap - ERR
|
||||||
|
|
||||||
|
"$@" >>"$logfile" 2>&1
|
||||||
|
local rc=$?
|
||||||
|
|
||||||
|
set -Eeuo pipefail
|
||||||
|
trap 'error_handler' ERR
|
||||||
|
|
||||||
|
if [[ $rc -ne 0 ]]; then
|
||||||
|
# Source explain_exit_code if needed
|
||||||
|
if ! declare -f explain_exit_code >/dev/null 2>&1; then
|
||||||
|
source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/error_handler.func)
|
||||||
|
fi
|
||||||
|
|
||||||
|
local explanation
|
||||||
|
explanation="$(explain_exit_code "$rc")"
|
||||||
|
|
||||||
|
printf "\e[?25h"
|
||||||
|
msg_error "in line ${caller_line}: exit code ${rc} (${explanation})"
|
||||||
|
msg_custom "→" "${YWB}" "${cmd}"
|
||||||
|
|
||||||
|
if [[ -s "$logfile" ]]; then
|
||||||
|
local log_lines=$(wc -l <"$logfile")
|
||||||
|
echo "--- Last 10 lines of silent log ---"
|
||||||
|
tail -n 10 "$logfile"
|
||||||
|
echo "-----------------------------------"
|
||||||
|
|
||||||
|
# Show how to view full log if there are more lines
|
||||||
|
if [[ $log_lines -gt 10 ]]; then
|
||||||
|
msg_custom "📋" "${YW}" "View full log (${log_lines} lines): ${logfile}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit "$rc"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# spinner()
|
||||||
|
#
|
||||||
|
# - Displays animated spinner with rotating characters (⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏)
|
||||||
|
# - Shows SPINNER_MSG alongside animation
|
||||||
|
# - Runs in infinite loop until killed by stop_spinner()
|
||||||
|
# - Uses color_spinner() colors for output
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
spinner() {
|
spinner() {
|
||||||
local chars=(⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏)
|
local chars=(⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏)
|
||||||
|
local msg="${SPINNER_MSG:-Processing...}"
|
||||||
local i=0
|
local i=0
|
||||||
while true; do
|
while true; do
|
||||||
local index=$((i++ % ${#chars[@]}))
|
local index=$((i++ % ${#chars[@]}))
|
||||||
printf "\r\033[2K%s %b" "${CS_YWB}${chars[$index]}${CS_CL}" "${CS_YWB}${SPINNER_MSG:-}${CS_CL}"
|
printf "\r\033[2K%s %b" "${CS_YWB}${chars[$index]}${CS_CL}" "${CS_YWB}${msg}${CS_CL}"
|
||||||
sleep 0.1
|
sleep 0.1
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# clear_line()
|
||||||
|
#
|
||||||
|
# - Clears current terminal line using tput or ANSI escape codes
|
||||||
|
# - Moves cursor to beginning of line (carriage return)
|
||||||
|
# - Erases from cursor to end of line
|
||||||
|
# - Fallback to ANSI codes if tput not available
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
clear_line() {
|
clear_line() {
|
||||||
tput cr 2>/dev/null || echo -en "\r"
|
tput cr 2>/dev/null || echo -en "\r"
|
||||||
tput el 2>/dev/null || echo -en "\033[K"
|
tput el 2>/dev/null || echo -en "\033[K"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# stop_spinner()
|
||||||
|
#
|
||||||
|
# - Stops running spinner process by PID
|
||||||
|
# - Reads PID from SPINNER_PID variable or /tmp/.spinner.pid file
|
||||||
|
# - Attempts graceful kill, then forced kill if needed
|
||||||
|
# - Cleans up temp file and resets terminal state
|
||||||
|
# - Unsets SPINNER_PID and SPINNER_MSG variables
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
stop_spinner() {
|
stop_spinner() {
|
||||||
local pid="${SPINNER_PID:-}"
|
local pid="${SPINNER_PID:-}"
|
||||||
[[ -z "$pid" && -f /tmp/.spinner.pid ]] && pid=$(</tmp/.spinner.pid)
|
[[ -z "$pid" && -f /tmp/.spinner.pid ]] && pid=$(</tmp/.spinner.pid)
|
||||||
@ -301,6 +503,19 @@ stop_spinner() {
|
|||||||
stty sane 2>/dev/null || true
|
stty sane 2>/dev/null || true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# SECTION 4: MESSAGE OUTPUT
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# msg_info()
|
||||||
|
#
|
||||||
|
# - Displays informational message with spinner animation
|
||||||
|
# - Shows each unique message only once (tracked via MSG_INFO_SHOWN)
|
||||||
|
# - In verbose/Alpine mode: shows hourglass icon instead of spinner
|
||||||
|
# - Stops any existing spinner before starting new one
|
||||||
|
# - Backgrounds spinner process and stores PID for later cleanup
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
msg_info() {
|
msg_info() {
|
||||||
local msg="$1"
|
local msg="$1"
|
||||||
[[ -z "$msg" ]] && return
|
[[ -z "$msg" ]] && return
|
||||||
@ -317,6 +532,12 @@ msg_info() {
|
|||||||
if is_verbose_mode || is_alpine; then
|
if is_verbose_mode || is_alpine; then
|
||||||
local HOURGLASS="${TAB}⏳${TAB}"
|
local HOURGLASS="${TAB}⏳${TAB}"
|
||||||
printf "\r\e[2K%s %b" "$HOURGLASS" "${YW}${msg}${CL}" >&2
|
printf "\r\e[2K%s %b" "$HOURGLASS" "${YW}${msg}${CL}" >&2
|
||||||
|
|
||||||
|
# Pause mode: Wait for Enter after each step
|
||||||
|
if [[ "${DEV_MODE_PAUSE:-false}" == "true" ]]; then
|
||||||
|
echo -en "\n${YWB}[PAUSE]${CL} Press Enter to continue..." >&2
|
||||||
|
read -r
|
||||||
|
fi
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@ -325,29 +546,68 @@ msg_info() {
|
|||||||
SPINNER_PID=$!
|
SPINNER_PID=$!
|
||||||
echo "$SPINNER_PID" >/tmp/.spinner.pid
|
echo "$SPINNER_PID" >/tmp/.spinner.pid
|
||||||
disown "$SPINNER_PID" 2>/dev/null || true
|
disown "$SPINNER_PID" 2>/dev/null || true
|
||||||
|
|
||||||
|
# Pause mode: Stop spinner and wait
|
||||||
|
if [[ "${DEV_MODE_PAUSE:-false}" == "true" ]]; then
|
||||||
|
stop_spinner
|
||||||
|
echo -en "\n${YWB}[PAUSE]${CL} Press Enter to continue..." >&2
|
||||||
|
read -r
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# msg_ok()
|
||||||
|
#
|
||||||
|
# - Displays success message with checkmark icon
|
||||||
|
# - Stops spinner and clears line before output
|
||||||
|
# - Removes message from MSG_INFO_SHOWN to allow re-display
|
||||||
|
# - Uses green color for success indication
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
msg_ok() {
|
msg_ok() {
|
||||||
local msg="$1"
|
local msg="$1"
|
||||||
[[ -z "$msg" ]] && return
|
[[ -z "$msg" ]] && return
|
||||||
stop_spinner
|
stop_spinner
|
||||||
clear_line
|
clear_line
|
||||||
printf "%s %b\n" "$CM" "${GN}${msg}${CL}" >&2
|
echo -e "$CM ${GN}${msg}${CL}"
|
||||||
unset MSG_INFO_SHOWN["$msg"]
|
unset MSG_INFO_SHOWN["$msg"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# msg_error()
|
||||||
|
#
|
||||||
|
# - Displays error message with cross/X icon
|
||||||
|
# - Stops spinner before output
|
||||||
|
# - Uses red color for error indication
|
||||||
|
# - Outputs to stderr
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
msg_error() {
|
msg_error() {
|
||||||
stop_spinner
|
stop_spinner
|
||||||
local msg="$1"
|
local msg="$1"
|
||||||
echo -e "${BFR:-} ${CROSS:-✖️} ${RD}${msg}${CL}"
|
echo -e "${BFR:-}${CROSS:-✖️} ${RD}${msg}${CL}" >&2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# msg_warn()
|
||||||
|
#
|
||||||
|
# - Displays warning message with info/lightbulb icon
|
||||||
|
# - Stops spinner before output
|
||||||
|
# - Uses bright yellow color for warning indication
|
||||||
|
# - Outputs to stderr
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
msg_warn() {
|
msg_warn() {
|
||||||
stop_spinner
|
stop_spinner
|
||||||
local msg="$1"
|
local msg="$1"
|
||||||
echo -e "${BFR:-} ${INFO:-ℹ️} ${YWB}${msg}${CL}"
|
echo -e "${BFR:-}${INFO:-ℹ️} ${YWB}${msg}${CL}" >&2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# msg_custom()
|
||||||
|
#
|
||||||
|
# - Displays custom message with user-defined symbol and color
|
||||||
|
# - Arguments: symbol, color code, message text
|
||||||
|
# - Stops spinner before output
|
||||||
|
# - Useful for specialized status messages
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
msg_custom() {
|
msg_custom() {
|
||||||
local symbol="${1:-"[*]"}"
|
local symbol="${1:-"[*]"}"
|
||||||
local color="${2:-"\e[36m"}"
|
local color="${2:-"\e[36m"}"
|
||||||
@ -357,17 +617,181 @@ msg_custom() {
|
|||||||
echo -e "${BFR:-} ${symbol} ${color}${msg}${CL:-\e[0m}"
|
echo -e "${BFR:-} ${symbol} ${color}${msg}${CL:-\e[0m}"
|
||||||
}
|
}
|
||||||
|
|
||||||
run_container_safe() {
|
# ------------------------------------------------------------------------------
|
||||||
local ct="$1"
|
# msg_debug()
|
||||||
shift
|
#
|
||||||
local cmd="$*"
|
# - Displays debug message with timestamp when var_full_verbose=1
|
||||||
|
# - Automatically enables var_verbose if not already set
|
||||||
lxc-attach -n "$ct" -- bash -euo pipefail -c "
|
# - Shows date/time prefix for log correlation
|
||||||
trap 'echo Aborted in container; exit 130' SIGINT SIGTERM
|
# - Uses bright yellow color for debug output
|
||||||
$cmd
|
# ------------------------------------------------------------------------------
|
||||||
" || __handle_general_error "lxc-attach to CT $ct"
|
msg_debug() {
|
||||||
|
if [[ "${var_full_verbose:-0}" == "1" ]]; then
|
||||||
|
[[ "${var_verbose:-0}" != "1" ]] && var_verbose=1
|
||||||
|
echo -e "${YWB}[$(date '+%F %T')] [DEBUG]${CL} $*"
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# msg_dev()
|
||||||
|
#
|
||||||
|
# - Display development mode messages with 🔧 icon
|
||||||
|
# - Only shown when dev_mode is active
|
||||||
|
# - Useful for debugging and development-specific output
|
||||||
|
# - Format: [DEV] message with distinct formatting
|
||||||
|
# - Usage: msg_dev "Container ready for debugging"
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
msg_dev() {
|
||||||
|
if [[ -n "${dev_mode:-}" ]]; then
|
||||||
|
echo -e "${SEARCH}${BOLD}${DGN}🔧 [DEV]${CL} $*"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
#
|
||||||
|
# - Displays error message and immediately terminates script
|
||||||
|
# - Sends SIGINT to current process to trigger error handler
|
||||||
|
# - Use for unrecoverable errors that require immediate exit
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
fatal() {
|
||||||
|
msg_error "$1"
|
||||||
|
kill -INT $$
|
||||||
|
}
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# SECTION 5: UTILITY FUNCTIONS
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# exit_script()
|
||||||
|
#
|
||||||
|
# - Called when user cancels an action
|
||||||
|
# - Clears screen and displays exit message
|
||||||
|
# - Exits with default exit code
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
exit_script() {
|
||||||
|
clear
|
||||||
|
echo -e "\n${CROSS}${RD}User exited script${CL}\n"
|
||||||
|
exit
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# get_header()
|
||||||
|
#
|
||||||
|
# - Downloads and caches application header ASCII art
|
||||||
|
# - Falls back to local cache if already downloaded
|
||||||
|
# - Determines app type (ct/vm) from APP_TYPE variable
|
||||||
|
# - Returns header content or empty string on failure
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
get_header() {
|
||||||
|
local app_name=$(echo "${APP,,}" | tr -d ' ')
|
||||||
|
local app_type=${APP_TYPE:-ct} # Default to 'ct' if not set
|
||||||
|
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()
|
||||||
|
#
|
||||||
|
# - Displays application header ASCII art at top of screen
|
||||||
|
# - Clears screen before displaying header
|
||||||
|
# - Detects terminal width for formatting
|
||||||
|
# - Returns silently if header not available
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
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()
|
||||||
|
#
|
||||||
|
# - Ensures tput command is available for terminal control
|
||||||
|
# - Installs ncurses-bin on Debian/Ubuntu or ncurses on Alpine
|
||||||
|
# - Required for clear_line() and terminal width detection
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
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()
|
||||||
|
#
|
||||||
|
# - Detects if running on Alpine Linux
|
||||||
|
# - Checks var_os, PCT_OSTYPE, or /etc/os-release
|
||||||
|
# - Returns 0 if Alpine, 1 otherwise
|
||||||
|
# - Used to adjust behavior for Alpine-specific commands
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
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()
|
||||||
|
#
|
||||||
|
# - Determines if script should run in verbose mode
|
||||||
|
# - Checks VERBOSE and var_verbose variables
|
||||||
|
# - Also returns true if not running in TTY (pipe/redirect scenario)
|
||||||
|
# - Used by msg_info() to decide between spinner and static output
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
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 ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# SECTION 6: CLEANUP & MAINTENANCE
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# cleanup_lxc()
|
||||||
|
#
|
||||||
|
# - Comprehensive cleanup of package managers, caches, and logs
|
||||||
|
# - Supports Alpine (apk), Debian/Ubuntu (apt), and language package managers
|
||||||
|
# - Cleans: Python (pip/uv), Node.js (npm/yarn/pnpm), Go, Rust, Ruby, PHP
|
||||||
|
# - Truncates log files and vacuums systemd journal
|
||||||
|
# - Run at end of container creation to minimize disk usage
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
cleanup_lxc() {
|
cleanup_lxc() {
|
||||||
msg_info "Cleaning up"
|
msg_info "Cleaning up"
|
||||||
|
|
||||||
@ -411,6 +835,16 @@ cleanup_lxc() {
|
|||||||
msg_ok "Cleaned"
|
msg_ok "Cleaned"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# check_or_create_swap()
|
||||||
|
#
|
||||||
|
# - Checks if swap is active on system
|
||||||
|
# - Offers to create swap file if none exists
|
||||||
|
# - Prompts user for swap size in MB
|
||||||
|
# - Creates /swapfile with specified size
|
||||||
|
# - Activates swap immediately
|
||||||
|
# - Returns 0 if swap active or successfully created, 1 if declined/failed
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
check_or_create_swap() {
|
check_or_create_swap() {
|
||||||
msg_info "Checking for active swap"
|
msg_info "Checking for active swap"
|
||||||
|
|
||||||
@ -449,4 +883,8 @@ check_or_create_swap() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# SIGNAL TRAPS
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
trap 'stop_spinner' EXIT INT TERM
|
trap 'stop_spinner' EXIT INT TERM
|
||||||
|
|||||||
@ -1,385 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
# Copyright (c) 2021-2025 tteck
|
|
||||||
# Author: tteck (tteckster)
|
|
||||||
# Co-Author: MickLesk
|
|
||||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
|
||||||
|
|
||||||
# This sets verbose mode if the global variable is set to "yes"
|
|
||||||
# if [ "$VERBOSE" == "yes" ]; then set -x; fi
|
|
||||||
|
|
||||||
if command -v curl >/dev/null 2>&1; then
|
|
||||||
source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/core.func)
|
|
||||||
load_functions
|
|
||||||
#echo "(create-lxc.sh) Loaded core.func via curl"
|
|
||||||
elif command -v wget >/dev/null 2>&1; then
|
|
||||||
source <(wget -qO- https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/core.func)
|
|
||||||
load_functions
|
|
||||||
#echo "(create-lxc.sh) Loaded core.func via wget"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# This sets error handling options and defines the error_handler function to handle errors
|
|
||||||
set -Eeuo pipefail
|
|
||||||
trap 'error_handler $LINENO "$BASH_COMMAND"' ERR
|
|
||||||
trap on_exit EXIT
|
|
||||||
trap on_interrupt INT
|
|
||||||
trap on_terminate TERM
|
|
||||||
|
|
||||||
function on_exit() {
|
|
||||||
local exit_code="$?"
|
|
||||||
[[ -n "${lockfile:-}" && -e "$lockfile" ]] && rm -f "$lockfile"
|
|
||||||
exit "$exit_code"
|
|
||||||
}
|
|
||||||
|
|
||||||
function error_handler() {
|
|
||||||
local exit_code="$?"
|
|
||||||
local line_number="$1"
|
|
||||||
local command="$2"
|
|
||||||
printf "\e[?25h"
|
|
||||||
echo -e "\n${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}\n"
|
|
||||||
exit "$exit_code"
|
|
||||||
}
|
|
||||||
|
|
||||||
function on_interrupt() {
|
|
||||||
echo -e "\n${RD}Interrupted by user (SIGINT)${CL}"
|
|
||||||
exit 130
|
|
||||||
}
|
|
||||||
|
|
||||||
function on_terminate() {
|
|
||||||
echo -e "\n${RD}Terminated by signal (SIGTERM)${CL}"
|
|
||||||
exit 143
|
|
||||||
}
|
|
||||||
|
|
||||||
function exit_script() {
|
|
||||||
clear
|
|
||||||
printf "\e[?25h"
|
|
||||||
echo -e "\n${CROSS}${RD}User exited script${CL}\n"
|
|
||||||
kill 0
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
function check_storage_support() {
|
|
||||||
local CONTENT="$1"
|
|
||||||
local -a VALID_STORAGES=()
|
|
||||||
while IFS= read -r line; do
|
|
||||||
local STORAGE_NAME
|
|
||||||
STORAGE_NAME=$(awk '{print $1}' <<<"$line")
|
|
||||||
[[ -z "$STORAGE_NAME" ]] && continue
|
|
||||||
VALID_STORAGES+=("$STORAGE_NAME")
|
|
||||||
done < <(pvesm status -content "$CONTENT" 2>/dev/null | awk 'NR>1')
|
|
||||||
|
|
||||||
[[ ${#VALID_STORAGES[@]} -gt 0 ]]
|
|
||||||
}
|
|
||||||
|
|
||||||
# This function selects a storage pool for a given content type (e.g., rootdir, vztmpl).
|
|
||||||
function select_storage() {
|
|
||||||
local CLASS=$1 CONTENT CONTENT_LABEL
|
|
||||||
|
|
||||||
case $CLASS in
|
|
||||||
container)
|
|
||||||
CONTENT='rootdir'
|
|
||||||
CONTENT_LABEL='Container'
|
|
||||||
;;
|
|
||||||
template)
|
|
||||||
CONTENT='vztmpl'
|
|
||||||
CONTENT_LABEL='Container template'
|
|
||||||
;;
|
|
||||||
iso)
|
|
||||||
CONTENT='iso'
|
|
||||||
CONTENT_LABEL='ISO image'
|
|
||||||
;;
|
|
||||||
images)
|
|
||||||
CONTENT='images'
|
|
||||||
CONTENT_LABEL='VM Disk image'
|
|
||||||
;;
|
|
||||||
backup)
|
|
||||||
CONTENT='backup'
|
|
||||||
CONTENT_LABEL='Backup'
|
|
||||||
;;
|
|
||||||
snippets)
|
|
||||||
CONTENT='snippets'
|
|
||||||
CONTENT_LABEL='Snippets'
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
msg_error "Invalid storage class '$CLASS'"
|
|
||||||
return 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
# Check for preset STORAGE variable
|
|
||||||
if [ "$CONTENT" = "rootdir" ] && [ -n "${STORAGE:-}" ]; then
|
|
||||||
if pvesm status -content "$CONTENT" | awk 'NR>1 {print $1}' | grep -qx "$STORAGE"; then
|
|
||||||
STORAGE_RESULT="$STORAGE"
|
|
||||||
msg_info "Using preset storage: $STORAGE_RESULT for $CONTENT_LABEL"
|
|
||||||
return 0
|
|
||||||
else
|
|
||||||
msg_error "Preset storage '$STORAGE' is not valid for content type '$CONTENT'."
|
|
||||||
return 2
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
local -A STORAGE_MAP
|
|
||||||
local -a MENU
|
|
||||||
local COL_WIDTH=0
|
|
||||||
|
|
||||||
while read -r TAG TYPE _ TOTAL USED FREE _; do
|
|
||||||
[[ -n "$TAG" && -n "$TYPE" ]] || continue
|
|
||||||
local STORAGE_NAME="$TAG"
|
|
||||||
local DISPLAY="${STORAGE_NAME} (${TYPE})"
|
|
||||||
local USED_FMT=$(numfmt --to=iec --from-unit=K --format %.1f <<<"$USED")
|
|
||||||
local FREE_FMT=$(numfmt --to=iec --from-unit=K --format %.1f <<<"$FREE")
|
|
||||||
local INFO="Free: ${FREE_FMT}B Used: ${USED_FMT}B"
|
|
||||||
STORAGE_MAP["$DISPLAY"]="$STORAGE_NAME"
|
|
||||||
MENU+=("$DISPLAY" "$INFO" "OFF")
|
|
||||||
((${#DISPLAY} > COL_WIDTH)) && COL_WIDTH=${#DISPLAY}
|
|
||||||
done < <(pvesm status -content "$CONTENT" | awk 'NR>1')
|
|
||||||
|
|
||||||
if [ ${#MENU[@]} -eq 0 ]; then
|
|
||||||
msg_error "No storage found for content type '$CONTENT'."
|
|
||||||
return 2
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ $((${#MENU[@]} / 3)) -eq 1 ]; then
|
|
||||||
STORAGE_RESULT="${STORAGE_MAP[${MENU[0]}]}"
|
|
||||||
STORAGE_INFO="${MENU[1]}"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
local WIDTH=$((COL_WIDTH + 42))
|
|
||||||
while true; do
|
|
||||||
local DISPLAY_SELECTED
|
|
||||||
DISPLAY_SELECTED=$(whiptail --backtitle "Proxmox VE Helper Scripts" \
|
|
||||||
--title "Storage Pools" \
|
|
||||||
--radiolist "Which storage pool for ${CONTENT_LABEL,,}?\n(Spacebar to select)" \
|
|
||||||
16 "$WIDTH" 6 "${MENU[@]}" 3>&1 1>&2 2>&3)
|
|
||||||
|
|
||||||
# Cancel or ESC
|
|
||||||
[[ $? -ne 0 ]] && exit_script
|
|
||||||
|
|
||||||
# Strip trailing whitespace or newline (important for storages like "storage (dir)")
|
|
||||||
DISPLAY_SELECTED=$(sed 's/[[:space:]]*$//' <<<"$DISPLAY_SELECTED")
|
|
||||||
|
|
||||||
if [[ -z "$DISPLAY_SELECTED" || -z "${STORAGE_MAP[$DISPLAY_SELECTED]+_}" ]]; then
|
|
||||||
whiptail --msgbox "No valid storage selected. Please try again." 8 58
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
STORAGE_RESULT="${STORAGE_MAP[$DISPLAY_SELECTED]}"
|
|
||||||
for ((i = 0; i < ${#MENU[@]}; i += 3)); do
|
|
||||||
if [[ "${MENU[$i]}" == "$DISPLAY_SELECTED" ]]; then
|
|
||||||
STORAGE_INFO="${MENU[$i + 1]}"
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
return 0
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
# Test if required variables are set
|
|
||||||
[[ "${CTID:-}" ]] || {
|
|
||||||
msg_error "You need to set 'CTID' variable."
|
|
||||||
exit 203
|
|
||||||
}
|
|
||||||
[[ "${PCT_OSTYPE:-}" ]] || {
|
|
||||||
msg_error "You need to set 'PCT_OSTYPE' variable."
|
|
||||||
exit 204
|
|
||||||
}
|
|
||||||
|
|
||||||
# Test if ID is valid
|
|
||||||
[ "$CTID" -ge "100" ] || {
|
|
||||||
msg_error "ID cannot be less than 100."
|
|
||||||
exit 205
|
|
||||||
}
|
|
||||||
|
|
||||||
# Test if ID is in use
|
|
||||||
if qm status "$CTID" &>/dev/null || pct status "$CTID" &>/dev/null; then
|
|
||||||
echo -e "ID '$CTID' is already in use."
|
|
||||||
unset CTID
|
|
||||||
msg_error "Cannot use ID that is already in use."
|
|
||||||
exit 206
|
|
||||||
fi
|
|
||||||
|
|
||||||
# This checks for the presence of valid Container Storage and Template Storage locations
|
|
||||||
if ! check_storage_support "rootdir"; then
|
|
||||||
msg_error "No valid storage found for 'rootdir' [Container]"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if ! check_storage_support "vztmpl"; then
|
|
||||||
msg_error "No valid storage found for 'vztmpl' [Template]"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
while true; do
|
|
||||||
if select_storage template; then
|
|
||||||
TEMPLATE_STORAGE="$STORAGE_RESULT"
|
|
||||||
TEMPLATE_STORAGE_INFO="$STORAGE_INFO"
|
|
||||||
msg_ok "Storage ${BL}$TEMPLATE_STORAGE${CL} ($TEMPLATE_STORAGE_INFO) [Template]"
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
while true; do
|
|
||||||
if select_storage container; then
|
|
||||||
CONTAINER_STORAGE="$STORAGE_RESULT"
|
|
||||||
CONTAINER_STORAGE_INFO="$STORAGE_INFO"
|
|
||||||
msg_ok "Storage ${BL}$CONTAINER_STORAGE${CL} ($CONTAINER_STORAGE_INFO) [Container]"
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
# Check free space on selected container storage
|
|
||||||
STORAGE_FREE=$(pvesm status | awk -v s="$CONTAINER_STORAGE" '$1 == s { print $6 }')
|
|
||||||
REQUIRED_KB=$((${PCT_DISK_SIZE:-8} * 1024 * 1024))
|
|
||||||
if [ "$STORAGE_FREE" -lt "$REQUIRED_KB" ]; then
|
|
||||||
msg_error "Not enough space on '$CONTAINER_STORAGE'. Needed: ${PCT_DISK_SIZE:-8}G."
|
|
||||||
exit 214
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check Cluster Quorum if in Cluster
|
|
||||||
if [ -f /etc/pve/corosync.conf ]; then
|
|
||||||
msg_info "Checking cluster quorum"
|
|
||||||
if ! pvecm status | awk -F':' '/^Quorate/ { exit ($2 ~ /Yes/) ? 0 : 1 }'; then
|
|
||||||
|
|
||||||
msg_error "Cluster is not quorate. Start all nodes or configure quorum device (QDevice)."
|
|
||||||
exit 210
|
|
||||||
fi
|
|
||||||
msg_ok "Cluster is quorate"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Update LXC template list
|
|
||||||
TEMPLATE_SEARCH="${PCT_OSTYPE}-${PCT_OSVERSION:-}"
|
|
||||||
case "$PCT_OSTYPE" in
|
|
||||||
debian | ubuntu)
|
|
||||||
TEMPLATE_PATTERN="-standard_"
|
|
||||||
;;
|
|
||||||
alpine | fedora | rocky | centos)
|
|
||||||
TEMPLATE_PATTERN="-default_"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
TEMPLATE_PATTERN=""
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
# 1. Check local templates first
|
|
||||||
msg_info "Searching for template '$TEMPLATE_SEARCH'"
|
|
||||||
mapfile -t TEMPLATES < <(
|
|
||||||
pveam list "$TEMPLATE_STORAGE" |
|
|
||||||
awk -v s="$TEMPLATE_SEARCH" -v p="$TEMPLATE_PATTERN" '$1 ~ s && $1 ~ p {print $1}' |
|
|
||||||
sed 's/.*\///' | sort -t - -k 2 -V
|
|
||||||
)
|
|
||||||
|
|
||||||
if [ ${#TEMPLATES[@]} -gt 0 ]; then
|
|
||||||
TEMPLATE_SOURCE="local"
|
|
||||||
else
|
|
||||||
msg_info "No local template found, checking online repository"
|
|
||||||
pveam update >/dev/null 2>&1
|
|
||||||
mapfile -t TEMPLATES < <(
|
|
||||||
pveam update >/dev/null 2>&1 &&
|
|
||||||
pveam available -section system |
|
|
||||||
sed -n "s/.*\($TEMPLATE_SEARCH.*$TEMPLATE_PATTERN.*\)/\1/p" |
|
|
||||||
sort -t - -k 2 -V
|
|
||||||
)
|
|
||||||
TEMPLATE_SOURCE="online"
|
|
||||||
fi
|
|
||||||
|
|
||||||
TEMPLATE="${TEMPLATES[-1]}"
|
|
||||||
TEMPLATE_PATH="$(pvesm path $TEMPLATE_STORAGE:vztmpl/$TEMPLATE 2>/dev/null ||
|
|
||||||
echo "/var/lib/vz/template/cache/$TEMPLATE")"
|
|
||||||
msg_ok "Template ${BL}$TEMPLATE${CL} [$TEMPLATE_SOURCE]"
|
|
||||||
|
|
||||||
# 4. Validate template (exists & not corrupted)
|
|
||||||
TEMPLATE_VALID=1
|
|
||||||
|
|
||||||
if [ ! -s "$TEMPLATE_PATH" ]; then
|
|
||||||
TEMPLATE_VALID=0
|
|
||||||
elif ! tar --use-compress-program=zstdcat -tf "$TEMPLATE_PATH" >/dev/null 2>&1; then
|
|
||||||
TEMPLATE_VALID=0
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$TEMPLATE_VALID" -eq 0 ]; then
|
|
||||||
msg_warn "Template $TEMPLATE is missing or corrupted. Re-downloading."
|
|
||||||
[[ -f "$TEMPLATE_PATH" ]] && rm -f "$TEMPLATE_PATH"
|
|
||||||
for attempt in {1..3}; do
|
|
||||||
msg_info "Attempt $attempt: Downloading LXC template..."
|
|
||||||
if pveam download "$TEMPLATE_STORAGE" "$TEMPLATE" >/dev/null 2>&1; then
|
|
||||||
msg_ok "Template download successful."
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
if [ $attempt -eq 3 ]; then
|
|
||||||
msg_error "Failed after 3 attempts. Please check network access or manually run:\n pveam download $TEMPLATE_STORAGE $TEMPLATE"
|
|
||||||
exit 208
|
|
||||||
fi
|
|
||||||
sleep $((attempt * 5))
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
msg_info "Creating LXC Container"
|
|
||||||
# Check and fix subuid/subgid
|
|
||||||
grep -q "root:100000:65536" /etc/subuid || echo "root:100000:65536" >>/etc/subuid
|
|
||||||
grep -q "root:100000:65536" /etc/subgid || echo "root:100000:65536" >>/etc/subgid
|
|
||||||
|
|
||||||
# Combine all options
|
|
||||||
PCT_OPTIONS=(${PCT_OPTIONS[@]:-${DEFAULT_PCT_OPTIONS[@]}})
|
|
||||||
[[ " ${PCT_OPTIONS[@]} " =~ " -rootfs " ]] || PCT_OPTIONS+=(-rootfs "$CONTAINER_STORAGE:${PCT_DISK_SIZE:-8}")
|
|
||||||
|
|
||||||
# Secure creation of the LXC container with lock and template check
|
|
||||||
lockfile="/tmp/template.${TEMPLATE}.lock"
|
|
||||||
exec 9>"$lockfile" || {
|
|
||||||
msg_error "Failed to create lock file '$lockfile'."
|
|
||||||
exit 200
|
|
||||||
}
|
|
||||||
flock -w 60 9 || {
|
|
||||||
msg_error "Timeout while waiting for template lock"
|
|
||||||
exit 211
|
|
||||||
}
|
|
||||||
|
|
||||||
if ! pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" "${PCT_OPTIONS[@]}" &>/dev/null; then
|
|
||||||
msg_error "Container creation failed. Checking if template is corrupted or incomplete."
|
|
||||||
|
|
||||||
if [[ ! -s "$TEMPLATE_PATH" || "$(stat -c%s "$TEMPLATE_PATH")" -lt 1000000 ]]; then
|
|
||||||
msg_error "Template file too small or missing – re-downloading."
|
|
||||||
rm -f "$TEMPLATE_PATH"
|
|
||||||
elif ! zstdcat "$TEMPLATE_PATH" | tar -tf - &>/dev/null; then
|
|
||||||
msg_error "Template appears to be corrupted – re-downloading."
|
|
||||||
rm -f "$TEMPLATE_PATH"
|
|
||||||
else
|
|
||||||
msg_error "Template is valid, but container creation failed. Update your whole Proxmox System (pve-container) first or check https://github.com/community-scripts/ProxmoxVE/discussions/8126"
|
|
||||||
exit 209
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Retry download
|
|
||||||
for attempt in {1..3}; do
|
|
||||||
msg_info "Attempt $attempt: Re-downloading template..."
|
|
||||||
if timeout 120 pveam download "$TEMPLATE_STORAGE" "$TEMPLATE" >/dev/null; then
|
|
||||||
msg_ok "Template re-download successful."
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
if [ "$attempt" -eq 3 ]; then
|
|
||||||
msg_error "Three failed attempts. Aborting."
|
|
||||||
exit 208
|
|
||||||
fi
|
|
||||||
sleep $((attempt * 5))
|
|
||||||
done
|
|
||||||
|
|
||||||
sleep 1 # I/O-Sync-Delay
|
|
||||||
msg_ok "Re-downloaded LXC Template"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! pct list | awk '{print $1}' | grep -qx "$CTID"; then
|
|
||||||
msg_error "Container ID $CTID not listed in 'pct list' – unexpected failure."
|
|
||||||
exit 215
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! grep -q '^rootfs:' "/etc/pve/lxc/$CTID.conf"; then
|
|
||||||
msg_error "RootFS entry missing in container config – storage not correctly assigned."
|
|
||||||
exit 216
|
|
||||||
fi
|
|
||||||
|
|
||||||
if grep -q '^hostname:' "/etc/pve/lxc/$CTID.conf"; then
|
|
||||||
CT_HOSTNAME=$(grep '^hostname:' "/etc/pve/lxc/$CTID.conf" | awk '{print $2}')
|
|
||||||
if [[ ! "$CT_HOSTNAME" =~ ^[a-z0-9-]+$ ]]; then
|
|
||||||
msg_warn "Hostname '$CT_HOSTNAME' contains invalid characters – may cause issues with networking or DNS."
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
msg_ok "LXC Container ${BL}$CTID${CL} ${GN}was successfully created."
|
|
||||||
317
misc/error_handler.func
Normal file
317
misc/error_handler.func
Normal file
@ -0,0 +1,317 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# ERROR HANDLER - ERROR & SIGNAL MANAGEMENT
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Copyright (c) 2021-2025 community-scripts ORG
|
||||||
|
# Author: MickLesk (CanbiZ)
|
||||||
|
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
#
|
||||||
|
# Provides comprehensive error handling and signal management for all scripts.
|
||||||
|
# Includes:
|
||||||
|
# - Exit code explanations (shell, package managers, databases, custom codes)
|
||||||
|
# - Error handler with detailed logging
|
||||||
|
# - Signal handlers (EXIT, INT, TERM)
|
||||||
|
# - Initialization function for trap setup
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# source <(curl -fsSL .../error_handler.func)
|
||||||
|
# catch_errors
|
||||||
|
#
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# SECTION 1: EXIT CODE EXPLANATIONS
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# explain_exit_code()
|
||||||
|
#
|
||||||
|
# - Maps numeric exit codes to human-readable error descriptions
|
||||||
|
# - Supports:
|
||||||
|
# * Generic/Shell errors (1, 2, 126, 127, 128, 130, 137, 139, 143)
|
||||||
|
# * Package manager errors (APT, DPKG: 100, 101, 255)
|
||||||
|
# * Node.js/npm errors (243-249, 254)
|
||||||
|
# * Python/pip/uv errors (210-212)
|
||||||
|
# * PostgreSQL errors (231-234)
|
||||||
|
# * MySQL/MariaDB errors (260-263)
|
||||||
|
# * MongoDB errors (251-253)
|
||||||
|
# * Proxmox custom codes (200-209, 213-223, 225)
|
||||||
|
# - Returns description string for given exit code
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
explain_exit_code() {
|
||||||
|
local code="$1"
|
||||||
|
case "$code" in
|
||||||
|
# --- Generic / Shell ---
|
||||||
|
1) echo "General error / Operation not permitted" ;;
|
||||||
|
2) echo "Misuse of shell builtins (e.g. syntax error)" ;;
|
||||||
|
126) echo "Command invoked cannot execute (permission problem?)" ;;
|
||||||
|
127) echo "Command not found" ;;
|
||||||
|
128) echo "Invalid argument to exit" ;;
|
||||||
|
130) echo "Terminated by Ctrl+C (SIGINT)" ;;
|
||||||
|
137) echo "Killed (SIGKILL / Out of memory?)" ;;
|
||||||
|
139) echo "Segmentation fault (core dumped)" ;;
|
||||||
|
143) echo "Terminated (SIGTERM)" ;;
|
||||||
|
|
||||||
|
# --- Package manager / APT / DPKG ---
|
||||||
|
100) echo "APT: Package manager error (broken packages / dependency problems)" ;;
|
||||||
|
101) echo "APT: Configuration error (bad sources.list, malformed config)" ;;
|
||||||
|
255) echo "DPKG: Fatal internal error" ;;
|
||||||
|
|
||||||
|
# --- Node.js / npm / pnpm / yarn ---
|
||||||
|
243) echo "Node.js: Out of memory (JavaScript heap out of memory)" ;;
|
||||||
|
245) echo "Node.js: Invalid command-line option" ;;
|
||||||
|
246) echo "Node.js: Internal JavaScript Parse Error" ;;
|
||||||
|
247) echo "Node.js: Fatal internal error" ;;
|
||||||
|
248) echo "Node.js: Invalid C++ addon / N-API failure" ;;
|
||||||
|
249) echo "Node.js: Inspector error" ;;
|
||||||
|
254) echo "npm/pnpm/yarn: Unknown fatal error" ;;
|
||||||
|
|
||||||
|
# --- Python / pip / uv ---
|
||||||
|
210) echo "Python: Virtualenv / uv environment missing or broken" ;;
|
||||||
|
211) echo "Python: Dependency resolution failed" ;;
|
||||||
|
212) echo "Python: Installation aborted (permissions or EXTERNALLY-MANAGED)" ;;
|
||||||
|
|
||||||
|
# --- PostgreSQL ---
|
||||||
|
231) echo "PostgreSQL: Connection failed (server not running / wrong socket)" ;;
|
||||||
|
232) echo "PostgreSQL: Authentication failed (bad user/password)" ;;
|
||||||
|
233) echo "PostgreSQL: Database does not exist" ;;
|
||||||
|
234) echo "PostgreSQL: Fatal error in query / syntax" ;;
|
||||||
|
|
||||||
|
# --- MySQL / MariaDB ---
|
||||||
|
260) echo "MySQL/MariaDB: Connection failed (server not running / wrong socket)" ;;
|
||||||
|
261) echo "MySQL/MariaDB: Authentication failed (bad user/password)" ;;
|
||||||
|
262) echo "MySQL/MariaDB: Database does not exist" ;;
|
||||||
|
263) echo "MySQL/MariaDB: Fatal error in query / syntax" ;;
|
||||||
|
|
||||||
|
# --- MongoDB ---
|
||||||
|
251) echo "MongoDB: Connection failed (server not running)" ;;
|
||||||
|
252) echo "MongoDB: Authentication failed (bad user/password)" ;;
|
||||||
|
253) echo "MongoDB: Database not found" ;;
|
||||||
|
|
||||||
|
# --- Proxmox Custom Codes ---
|
||||||
|
200) echo "Custom: Failed to create lock file" ;;
|
||||||
|
201) echo "Custom: Cluster not quorate" ;;
|
||||||
|
202) echo "Custom: Timeout waiting for template lock (concurrent download in progress)" ;;
|
||||||
|
203) echo "Custom: Missing CTID variable" ;;
|
||||||
|
204) echo "Custom: Missing PCT_OSTYPE variable" ;;
|
||||||
|
205) echo "Custom: Invalid CTID (<100)" ;;
|
||||||
|
206) echo "Custom: CTID already in use (check 'pct list' and /etc/pve/lxc/)" ;;
|
||||||
|
207) echo "Custom: Password contains unescaped special characters (-, /, \\, *, etc.)" ;;
|
||||||
|
208) echo "Custom: Invalid configuration (DNS/MAC/Network format error)" ;;
|
||||||
|
209) echo "Custom: Container creation failed (check logs for pct create output)" ;;
|
||||||
|
213) echo "Custom: LXC stack upgrade/retry failed (outdated pve-container - check https://github.com/community-scripts/ProxmoxVE/discussions/8126)" ;;
|
||||||
|
214) echo "Custom: Not enough storage space" ;;
|
||||||
|
215) echo "Custom: Container created but not listed (ghost state - check /etc/pve/lxc/)" ;;
|
||||||
|
216) echo "Custom: RootFS entry missing in config (incomplete creation)" ;;
|
||||||
|
217) echo "Custom: Storage does not support rootdir (check storage capabilities)" ;;
|
||||||
|
218) echo "Custom: Template file corrupted or incomplete download (size <1MB or invalid archive)" ;;
|
||||||
|
220) echo "Custom: Unable to resolve template path" ;;
|
||||||
|
221) echo "Custom: Template file exists but not readable (check file permissions)" ;;
|
||||||
|
222) echo "Custom: Template download failed after 3 attempts (network/storage issue)" ;;
|
||||||
|
223) echo "Custom: Template not available after download (storage sync issue)" ;;
|
||||||
|
225) echo "Custom: No template available for OS/Version (check 'pveam available')" ;;
|
||||||
|
|
||||||
|
# --- Default ---
|
||||||
|
*) echo "Unknown error" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# SECTION 2: ERROR HANDLERS
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# error_handler()
|
||||||
|
#
|
||||||
|
# - Main error handler triggered by ERR trap
|
||||||
|
# - Arguments: exit_code, command, line_number
|
||||||
|
# - Behavior:
|
||||||
|
# * Returns silently if exit_code is 0 (success)
|
||||||
|
# * Sources explain_exit_code() for detailed error description
|
||||||
|
# * Displays error message with:
|
||||||
|
# - Line number where error occurred
|
||||||
|
# - Exit code with explanation
|
||||||
|
# - Command that failed
|
||||||
|
# * Shows last 20 lines of SILENT_LOGFILE if available
|
||||||
|
# * Copies log to container /root for later inspection
|
||||||
|
# * Exits with original exit code
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
error_handler() {
|
||||||
|
local exit_code=${1:-$?}
|
||||||
|
local command=${2:-${BASH_COMMAND:-unknown}}
|
||||||
|
local line_number=${BASH_LINENO[0]:-unknown}
|
||||||
|
|
||||||
|
command="${command//\$STD/}"
|
||||||
|
|
||||||
|
if [[ "$exit_code" -eq 0 ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local explanation
|
||||||
|
explanation="$(explain_exit_code "$exit_code")"
|
||||||
|
|
||||||
|
printf "\e[?25h"
|
||||||
|
|
||||||
|
# Use msg_error if available, fallback to echo
|
||||||
|
if declare -f msg_error >/dev/null 2>&1; then
|
||||||
|
msg_error "in line ${line_number}: exit code ${exit_code} (${explanation}): while executing command ${command}"
|
||||||
|
else
|
||||||
|
echo -e "\n${RD}[ERROR]${CL} in line ${RD}${line_number}${CL}: exit code ${RD}${exit_code}${CL} (${explanation}): while executing command ${YWB}${command}${CL}\n"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "${DEBUG_LOGFILE:-}" ]]; then
|
||||||
|
{
|
||||||
|
echo "------ ERROR ------"
|
||||||
|
echo "Timestamp : $(date '+%Y-%m-%d %H:%M:%S')"
|
||||||
|
echo "Exit Code : $exit_code ($explanation)"
|
||||||
|
echo "Line : $line_number"
|
||||||
|
echo "Command : $command"
|
||||||
|
echo "-------------------"
|
||||||
|
} >>"$DEBUG_LOGFILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get active log file (BUILD_LOG or INSTALL_LOG)
|
||||||
|
local active_log=""
|
||||||
|
if declare -f get_active_logfile >/dev/null 2>&1; then
|
||||||
|
active_log="$(get_active_logfile)"
|
||||||
|
elif [[ -n "${SILENT_LOGFILE:-}" ]]; then
|
||||||
|
active_log="$SILENT_LOGFILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$active_log" && -s "$active_log" ]]; then
|
||||||
|
echo "--- Last 20 lines of silent log ---"
|
||||||
|
tail -n 20 "$active_log"
|
||||||
|
echo "-----------------------------------"
|
||||||
|
|
||||||
|
# Detect context: Container (INSTALL_LOG set + /root exists) vs Host (BUILD_LOG)
|
||||||
|
if [[ -n "${INSTALL_LOG:-}" && -d /root ]]; then
|
||||||
|
# CONTAINER CONTEXT: Copy log and create flag file for host
|
||||||
|
local container_log="/root/.install-${SESSION_ID:-error}.log"
|
||||||
|
cp "$active_log" "$container_log" 2>/dev/null || true
|
||||||
|
|
||||||
|
# Create error flag file with exit code for host detection
|
||||||
|
echo "$exit_code" >"/root/.install-${SESSION_ID:-error}.failed" 2>/dev/null || true
|
||||||
|
|
||||||
|
if declare -f msg_custom >/dev/null 2>&1; then
|
||||||
|
msg_custom "📋" "${YW}" "Log saved to: ${container_log}"
|
||||||
|
else
|
||||||
|
echo -e "${YW}Log saved to:${CL} ${BL}${container_log}${CL}"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# HOST CONTEXT: Show local log path and offer container cleanup
|
||||||
|
if declare -f msg_custom >/dev/null 2>&1; then
|
||||||
|
msg_custom "📋" "${YW}" "Full log: ${active_log}"
|
||||||
|
else
|
||||||
|
echo -e "${YW}Full log:${CL} ${BL}${active_log}${CL}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Offer to remove container if it exists (build errors after container creation)
|
||||||
|
if [[ -n "${CTID:-}" ]] && command -v pct &>/dev/null && pct status "$CTID" &>/dev/null; then
|
||||||
|
echo ""
|
||||||
|
echo -en "${YW}Remove broken container ${CTID}? (Y/n) [auto-remove in 60s]: ${CL}"
|
||||||
|
|
||||||
|
if read -t 60 -r response; then
|
||||||
|
if [[ -z "$response" || "$response" =~ ^[Yy]$ ]]; then
|
||||||
|
echo -e "\n${YW}Removing container ${CTID}${CL}"
|
||||||
|
pct stop "$CTID" &>/dev/null || true
|
||||||
|
pct destroy "$CTID" &>/dev/null || true
|
||||||
|
echo -e "${GN}✔${CL} Container ${CTID} removed"
|
||||||
|
elif [[ "$response" =~ ^[Nn]$ ]]; then
|
||||||
|
echo -e "\n${YW}Container ${CTID} kept for debugging${CL}"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# Timeout - auto-remove
|
||||||
|
echo -e "\n${YW}No response - auto-removing container${CL}"
|
||||||
|
pct stop "$CTID" &>/dev/null || true
|
||||||
|
pct destroy "$CTID" &>/dev/null || true
|
||||||
|
echo -e "${GN}✔${CL} Container ${CTID} removed"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit "$exit_code"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# SECTION 3: SIGNAL HANDLERS
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# on_exit()
|
||||||
|
#
|
||||||
|
# - EXIT trap handler
|
||||||
|
# - Cleans up lock files if lockfile variable is set
|
||||||
|
# - Exits with captured exit code
|
||||||
|
# - Always runs on script termination (success or failure)
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
on_exit() {
|
||||||
|
local exit_code=$?
|
||||||
|
[[ -n "${lockfile:-}" && -e "$lockfile" ]] && rm -f "$lockfile"
|
||||||
|
exit "$exit_code"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# on_interrupt()
|
||||||
|
#
|
||||||
|
# - SIGINT (Ctrl+C) trap handler
|
||||||
|
# - Displays "Interrupted by user" message
|
||||||
|
# - Exits with code 130 (128 + SIGINT=2)
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
on_interrupt() {
|
||||||
|
if declare -f msg_error >/dev/null 2>&1; then
|
||||||
|
msg_error "Interrupted by user (SIGINT)"
|
||||||
|
else
|
||||||
|
echo -e "\n${RD}Interrupted by user (SIGINT)${CL}"
|
||||||
|
fi
|
||||||
|
exit 130
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# on_terminate()
|
||||||
|
#
|
||||||
|
# - SIGTERM trap handler
|
||||||
|
# - Displays "Terminated by signal" message
|
||||||
|
# - Exits with code 143 (128 + SIGTERM=15)
|
||||||
|
# - Triggered by external process termination
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
on_terminate() {
|
||||||
|
if declare -f msg_error >/dev/null 2>&1; then
|
||||||
|
msg_error "Terminated by signal (SIGTERM)"
|
||||||
|
else
|
||||||
|
echo -e "\n${RD}Terminated by signal (SIGTERM)${CL}"
|
||||||
|
fi
|
||||||
|
exit 143
|
||||||
|
}
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# SECTION 4: INITIALIZATION
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# catch_errors()
|
||||||
|
#
|
||||||
|
# - Initializes error handling and signal traps
|
||||||
|
# - Enables strict error handling:
|
||||||
|
# * set -Ee: Exit on error, inherit ERR trap in functions
|
||||||
|
# * set -o pipefail: Pipeline fails if any command fails
|
||||||
|
# * set -u: (optional) Exit on undefined variable (if STRICT_UNSET=1)
|
||||||
|
# - Sets up traps:
|
||||||
|
# * ERR → error_handler
|
||||||
|
# * EXIT → on_exit
|
||||||
|
# * INT → on_interrupt
|
||||||
|
# * TERM → on_terminate
|
||||||
|
# - Call this function early in every script
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
catch_errors() {
|
||||||
|
set -Ee -o pipefail
|
||||||
|
if [ "${STRICT_UNSET:-0}" = "1" ]; then
|
||||||
|
set -u
|
||||||
|
fi
|
||||||
|
|
||||||
|
trap 'error_handler' ERR
|
||||||
|
trap on_exit EXIT
|
||||||
|
trap on_interrupt INT
|
||||||
|
trap on_terminate TERM
|
||||||
|
}
|
||||||
@ -1,21 +1,57 @@
|
|||||||
# Copyright (c) 2021-2025 tteck
|
# Copyright (c) 2021-2025 community-scripts ORG
|
||||||
# Author: tteck (tteckster)
|
# Author: tteck (tteckster)
|
||||||
# Co-Author: MickLesk
|
# Co-Author: MickLesk
|
||||||
# License: MIT
|
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||||
# https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
|
||||||
|
# ==============================================================================
|
||||||
|
# INSTALL.FUNC - CONTAINER INSTALLATION & SETUP
|
||||||
|
# ==============================================================================
|
||||||
|
#
|
||||||
|
# This file provides installation functions executed inside LXC containers
|
||||||
|
# after creation. Handles:
|
||||||
|
#
|
||||||
|
# - Network connectivity verification (IPv4/IPv6)
|
||||||
|
# - OS updates and package installation
|
||||||
|
# - DNS resolution checks
|
||||||
|
# - MOTD and SSH configuration
|
||||||
|
# - Container customization and auto-login
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# - Sourced by <app>-install.sh scripts
|
||||||
|
# - Executes via pct exec inside container
|
||||||
|
# - Requires internet connectivity
|
||||||
|
#
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# SECTION 1: INITIALIZATION
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
if ! command -v curl >/dev/null 2>&1; then
|
if ! command -v curl >/dev/null 2>&1; then
|
||||||
printf "\r\e[2K%b" '\033[93m Setup Source \033[m' >&2
|
printf "\r\e[2K%b" '\033[93m Setup Source \033[m' >&2
|
||||||
apt-get update >/dev/null 2>&1
|
apt update >/dev/null 2>&1
|
||||||
apt-get install -y curl >/dev/null 2>&1
|
apt install -y curl >/dev/null 2>&1
|
||||||
fi
|
fi
|
||||||
source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/core.func)
|
source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/core.func)
|
||||||
|
source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/error_handler.func)
|
||||||
load_functions
|
load_functions
|
||||||
# This function enables IPv6 if it's not disabled and sets verbose mode
|
catch_errors
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# SECTION 2: NETWORK & CONNECTIVITY
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# verb_ip6()
|
||||||
|
#
|
||||||
|
# - Configures IPv6 based on DISABLEIPV6 variable
|
||||||
|
# - If DISABLEIPV6=yes: disables IPv6 via sysctl
|
||||||
|
# - Sets verbose mode via set_std_mode()
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
verb_ip6() {
|
verb_ip6() {
|
||||||
set_std_mode # Set STD mode based on VERBOSE
|
set_std_mode # Set STD mode based on VERBOSE
|
||||||
|
|
||||||
if [ "$IPV6_METHOD" == "disable" ]; then
|
if [ "${IPV6_METHOD:-}" = "disable" ]; then
|
||||||
msg_info "Disabling IPv6 (this may affect some services)"
|
msg_info "Disabling IPv6 (this may affect some services)"
|
||||||
mkdir -p /etc/sysctl.d
|
mkdir -p /etc/sysctl.d
|
||||||
$STD tee /etc/sysctl.d/99-disable-ipv6.conf >/dev/null <<EOF
|
$STD tee /etc/sysctl.d/99-disable-ipv6.conf >/dev/null <<EOF
|
||||||
@ -29,30 +65,15 @@ EOF
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# This function sets error handling options and defines the error_handler function to handle errors
|
# ------------------------------------------------------------------------------
|
||||||
catch_errors() {
|
# setting_up_container()
|
||||||
set -Eeuo pipefail
|
#
|
||||||
trap 'error_handler $LINENO "$BASH_COMMAND"' ERR
|
# - Verifies network connectivity via hostname -I
|
||||||
}
|
# - Retries up to RETRY_NUM times with RETRY_EVERY seconds delay
|
||||||
|
# - Removes Python EXTERNALLY-MANAGED restrictions
|
||||||
# This function handles errors
|
# - Disables systemd-networkd-wait-online.service for faster boot
|
||||||
error_handler() {
|
# - Exits with error if network unavailable after retries
|
||||||
source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/api.func)
|
# ------------------------------------------------------------------------------
|
||||||
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() {
|
setting_up_container() {
|
||||||
msg_info "Setting up Container OS"
|
msg_info "Setting up Container OS"
|
||||||
for ((i = RETRY_NUM; i > 0; i--)); do
|
for ((i = RETRY_NUM; i > 0; i--)); do
|
||||||
@ -74,8 +95,17 @@ setting_up_container() {
|
|||||||
msg_ok "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()
|
||||||
|
#
|
||||||
|
# - Comprehensive network connectivity check for IPv4 and IPv6
|
||||||
|
# - Tests connectivity to multiple DNS servers:
|
||||||
|
# * IPv4: 1.1.1.1 (Cloudflare), 8.8.8.8 (Google), 9.9.9.9 (Quad9)
|
||||||
|
# * IPv6: 2606:4700:4700::1111, 2001:4860:4860::8888, 2620:fe::fe
|
||||||
|
# - Verifies DNS resolution for GitHub and Community-Scripts domains
|
||||||
|
# - Prompts user to continue if no internet detected
|
||||||
|
# - Uses fatal() on DNS resolution failure for critical hosts
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
network_check() {
|
network_check() {
|
||||||
set +e
|
set +e
|
||||||
trap - ERR
|
trap - ERR
|
||||||
@ -135,7 +165,19 @@ network_check() {
|
|||||||
trap 'error_handler $LINENO "$BASH_COMMAND"' ERR
|
trap 'error_handler $LINENO "$BASH_COMMAND"' ERR
|
||||||
}
|
}
|
||||||
|
|
||||||
# This function updates the Container OS by running apt-get update and upgrade
|
# ==============================================================================
|
||||||
|
# SECTION 3: OS UPDATE & PACKAGE MANAGEMENT
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# update_os()
|
||||||
|
#
|
||||||
|
# - Updates container OS via apt-get update and dist-upgrade
|
||||||
|
# - Configures APT cacher proxy if CACHER=yes (accelerates package downloads)
|
||||||
|
# - Removes Python EXTERNALLY-MANAGED restrictions for pip
|
||||||
|
# - Sources tools.func for additional setup functions after update
|
||||||
|
# - Uses $STD wrapper to suppress output unless VERBOSE=yes
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
update_os() {
|
update_os() {
|
||||||
msg_info "Updating Container OS"
|
msg_info "Updating Container OS"
|
||||||
if [[ "$CACHER" == "yes" ]]; then
|
if [[ "$CACHER" == "yes" ]]; then
|
||||||
@ -158,7 +200,24 @@ EOF
|
|||||||
source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/tools.func)
|
source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/tools.func)
|
||||||
}
|
}
|
||||||
|
|
||||||
# This function modifies the message of the day (motd) and SSH settings
|
# ==============================================================================
|
||||||
|
# SECTION 4: MOTD & SSH CONFIGURATION
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# motd_ssh()
|
||||||
|
#
|
||||||
|
# - Configures Message of the Day (MOTD) with container information
|
||||||
|
# - Creates /etc/profile.d/00_lxc-details.sh with:
|
||||||
|
# * Application name
|
||||||
|
# * Warning banner (DEV repository)
|
||||||
|
# * OS name and version
|
||||||
|
# * Hostname and IP address
|
||||||
|
# * GitHub repository link
|
||||||
|
# - Disables executable flag on /etc/update-motd.d/* scripts
|
||||||
|
# - Enables root SSH access if SSH_ROOT=yes
|
||||||
|
# - Configures TERM environment variable for better terminal support
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
motd_ssh() {
|
motd_ssh() {
|
||||||
# Set terminal to 256-color mode
|
# Set terminal to 256-color mode
|
||||||
grep -qxF "export TERM='xterm-256color'" /root/.bashrc || echo "export TERM='xterm-256color'" >>/root/.bashrc
|
grep -qxF "export TERM='xterm-256color'" /root/.bashrc || echo "export TERM='xterm-256color'" >>/root/.bashrc
|
||||||
@ -190,7 +249,19 @@ motd_ssh() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# This function customizes the container by modifying the getty service and enabling auto-login for the root user
|
# ==============================================================================
|
||||||
|
# SECTION 5: CONTAINER CUSTOMIZATION
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# customize()
|
||||||
|
#
|
||||||
|
# - Customizes container for passwordless root login if PASSWORD is empty
|
||||||
|
# - Configures getty for auto-login via /etc/systemd/system/container-getty@1.service.d/override.conf
|
||||||
|
# - Creates /usr/bin/update script for easy application updates
|
||||||
|
# - Injects SSH authorized keys if SSH_AUTHORIZED_KEY variable is set
|
||||||
|
# - Sets proper permissions on SSH directories and key files
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
customize() {
|
customize() {
|
||||||
if [[ "$PASSWORD" == "" ]]; then
|
if [[ "$PASSWORD" == "" ]]; then
|
||||||
msg_info "Customizing Container"
|
msg_info "Customizing Container"
|
||||||
|
|||||||
316
misc/tools.func
316
misc/tools.func
@ -72,23 +72,19 @@ stop_all_services() {
|
|||||||
local service_patterns=("$@")
|
local service_patterns=("$@")
|
||||||
|
|
||||||
for pattern in "${service_patterns[@]}"; do
|
for pattern in "${service_patterns[@]}"; do
|
||||||
# Find all matching services (use || true to avoid pipeline failures)
|
# Find all matching services
|
||||||
local services
|
|
||||||
services=$(systemctl list-units --type=service --all 2>/dev/null |
|
systemctl list-units --type=service --all 2>/dev/null |
|
||||||
grep -oE "${pattern}[^ ]*\.service" 2>/dev/null |
|
grep -oE "${pattern}[^ ]*\.service" |
|
||||||
sort -u 2>/dev/null || true)
|
sort -u |
|
||||||
|
while read -r service; do
|
||||||
|
|
||||||
# Only process if we found any services
|
|
||||||
if [[ -n "$services" ]]; then
|
|
||||||
while IFS= read -r service; do
|
|
||||||
[[ -z "$service" ]] && continue
|
|
||||||
$STD systemctl stop "$service" 2>/dev/null || true
|
$STD systemctl stop "$service" 2>/dev/null || true
|
||||||
$STD systemctl disable "$service" 2>/dev/null || true
|
$STD systemctl disable "$service" 2>/dev/null || true
|
||||||
done <<<"$services"
|
done
|
||||||
fi
|
|
||||||
done
|
done
|
||||||
|
|
||||||
return 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
@ -1214,7 +1210,7 @@ setup_deb822_repo() {
|
|||||||
local gpg_url="$2"
|
local gpg_url="$2"
|
||||||
local repo_url="$3"
|
local repo_url="$3"
|
||||||
local suite="$4"
|
local suite="$4"
|
||||||
local component="${5-main}"
|
local component="${5:-main}"
|
||||||
local architectures="${6-}" # optional
|
local architectures="${6-}" # optional
|
||||||
|
|
||||||
# Validate required parameters
|
# Validate required parameters
|
||||||
@ -3242,7 +3238,6 @@ function setup_mongodb() {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# Verify MongoDB was installed correctly
|
|
||||||
if ! command -v mongod >/dev/null 2>&1; then
|
if ! command -v mongod >/dev/null 2>&1; then
|
||||||
msg_error "MongoDB binary not found after installation"
|
msg_error "MongoDB binary not found after installation"
|
||||||
return 1
|
return 1
|
||||||
@ -3418,12 +3413,12 @@ EOF
|
|||||||
# - Optionally installs or updates global npm modules
|
# - Optionally installs or updates global npm modules
|
||||||
#
|
#
|
||||||
# Variables:
|
# Variables:
|
||||||
# NODE_VERSION - Node.js version to install (default: 22)
|
# NODE_VERSION - Node.js version to install (default: 24 LTS)
|
||||||
# NODE_MODULE - Comma-separated list of global modules (e.g. "yarn,@vue/cli@5.0.0")
|
# NODE_MODULE - Comma-separated list of global modules (e.g. "yarn,@vue/cli@5.0.0")
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
function setup_nodejs() {
|
function setup_nodejs() {
|
||||||
local NODE_VERSION="${NODE_VERSION:-22}"
|
local NODE_VERSION="${NODE_VERSION:-24}"
|
||||||
local NODE_MODULE="${NODE_MODULE:-}"
|
local NODE_MODULE="${NODE_MODULE:-}"
|
||||||
|
|
||||||
# ALWAYS clean up legacy installations first (nvm, etc.) to prevent conflicts
|
# ALWAYS clean up legacy installations first (nvm, etc.) to prevent conflicts
|
||||||
@ -3485,14 +3480,11 @@ function setup_nodejs() {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# CRITICAL: Force APT cache refresh AFTER repository setup
|
# Force APT cache refresh after repository setup
|
||||||
# This ensures NodeSource is the only nodejs source in APT cache
|
|
||||||
$STD apt update
|
$STD apt update
|
||||||
|
|
||||||
# Install dependencies (NodeSource is now the only nodejs source)
|
|
||||||
ensure_dependencies curl ca-certificates gnupg
|
ensure_dependencies curl ca-certificates gnupg
|
||||||
|
|
||||||
# Install Node.js from NodeSource
|
|
||||||
install_packages_with_retry "nodejs" || {
|
install_packages_with_retry "nodejs" || {
|
||||||
msg_error "Failed to install Node.js ${NODE_VERSION} from NodeSource"
|
msg_error "Failed to install Node.js ${NODE_VERSION} from NodeSource"
|
||||||
return 1
|
return 1
|
||||||
@ -3643,12 +3635,12 @@ function setup_php() {
|
|||||||
local CURRENT_PHP=""
|
local CURRENT_PHP=""
|
||||||
CURRENT_PHP=$(is_tool_installed "php" 2>/dev/null) || true
|
CURRENT_PHP=$(is_tool_installed "php" 2>/dev/null) || true
|
||||||
|
|
||||||
# CRITICAL: If wrong version is installed, remove it FIRST before any pinning
|
# Remove conflicting PHP version before pinning
|
||||||
if [[ -n "$CURRENT_PHP" && "$CURRENT_PHP" != "$PHP_VERSION" ]]; then
|
if [[ -n "$CURRENT_PHP" && "$CURRENT_PHP" != "$PHP_VERSION" ]]; then
|
||||||
msg_info "Removing conflicting PHP ${CURRENT_PHP} (need ${PHP_VERSION})"
|
msg_info "Removing conflicting PHP ${CURRENT_PHP} (need ${PHP_VERSION})"
|
||||||
stop_all_services "php.*-fpm"
|
stop_all_services "php.*-fpm"
|
||||||
$STD apt-get purge -y "php*" 2>/dev/null || true
|
$STD apt purge -y "php*" 2>/dev/null || true
|
||||||
$STD apt-get autoremove -y 2>/dev/null || true
|
$STD apt autoremove -y 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# NOW create pinning for the desired version
|
# NOW create pinning for the desired version
|
||||||
@ -3675,7 +3667,7 @@ EOF
|
|||||||
}
|
}
|
||||||
|
|
||||||
ensure_apt_working || return 1
|
ensure_apt_working || return 1
|
||||||
$STD apt-get update
|
$STD apt update
|
||||||
|
|
||||||
# Get available PHP version from repository
|
# Get available PHP version from repository
|
||||||
local AVAILABLE_PHP_VERSION=""
|
local AVAILABLE_PHP_VERSION=""
|
||||||
@ -3790,7 +3782,6 @@ EOF
|
|||||||
|
|
||||||
local INSTALLED_VERSION=$(php -v 2>/dev/null | awk '/^PHP/{print $2}' | cut -d. -f1,2)
|
local INSTALLED_VERSION=$(php -v 2>/dev/null | awk '/^PHP/{print $2}' | cut -d. -f1,2)
|
||||||
|
|
||||||
# Critical: if major.minor doesn't match, fail and cleanup
|
|
||||||
if [[ "$INSTALLED_VERSION" != "$PHP_VERSION" ]]; then
|
if [[ "$INSTALLED_VERSION" != "$PHP_VERSION" ]]; then
|
||||||
msg_error "PHP version mismatch: requested ${PHP_VERSION} but got ${INSTALLED_VERSION}"
|
msg_error "PHP version mismatch: requested ${PHP_VERSION} but got ${INSTALLED_VERSION}"
|
||||||
msg_error "This indicates a critical package installation issue"
|
msg_error "This indicates a critical package installation issue"
|
||||||
@ -3870,74 +3861,14 @@ function setup_postgresql() {
|
|||||||
local SUITE
|
local SUITE
|
||||||
case "$DISTRO_CODENAME" in
|
case "$DISTRO_CODENAME" in
|
||||||
trixie | forky | sid)
|
trixie | forky | sid)
|
||||||
# For Debian Testing/Unstable, try PostgreSQL repo first, fallback to native packages
|
|
||||||
if verify_repo_available "https://apt.postgresql.org/pub/repos/apt" "trixie-pgdg"; then
|
if verify_repo_available "https://apt.postgresql.org/pub/repos/apt" "trixie-pgdg"; then
|
||||||
SUITE="trixie-pgdg"
|
SUITE="trixie-pgdg"
|
||||||
|
|
||||||
setup_deb822_repo \
|
|
||||||
"pgdg" \
|
|
||||||
"https://www.postgresql.org/media/keys/ACCC4CF8.asc" \
|
|
||||||
"https://apt.postgresql.org/pub/repos/apt" \
|
|
||||||
"$SUITE" \
|
|
||||||
"main"
|
|
||||||
|
|
||||||
if ! $STD apt update; then
|
|
||||||
msg_warn "Failed to update PostgreSQL repository, falling back to native packages"
|
|
||||||
SUITE=""
|
|
||||||
fi
|
|
||||||
else
|
else
|
||||||
SUITE=""
|
SUITE="bookworm-pgdg"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# If no repo or packages not installable, use native Debian packages
|
|
||||||
if [[ -z "$SUITE" ]] || ! apt-cache show "postgresql-${PG_VERSION}" 2>/dev/null | grep -q "Version:"; then
|
|
||||||
msg_info "Using native Debian packages for $DISTRO_CODENAME"
|
|
||||||
|
|
||||||
# Install ssl-cert dependency if available
|
|
||||||
if apt-cache search "^ssl-cert$" 2>/dev/null | grep -q .; then
|
|
||||||
$STD apt install -y ssl-cert 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! $STD apt install -y postgresql postgresql-client 2>/dev/null; then
|
|
||||||
msg_error "Failed to install native PostgreSQL packages"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! command -v psql >/dev/null 2>&1; then
|
|
||||||
msg_error "PostgreSQL installed but psql command not found"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Restore database backup if we upgraded from previous version
|
|
||||||
if [[ -n "$CURRENT_PG_VERSION" ]]; then
|
|
||||||
msg_info "Restoring PostgreSQL databases from backup..."
|
|
||||||
$STD runuser -u postgres -- psql </var/lib/postgresql/backup_$(date +%F)_v${CURRENT_PG_VERSION}.sql 2>/dev/null || {
|
|
||||||
msg_warn "Failed to restore database backup - this may be expected for major version upgrades"
|
|
||||||
}
|
|
||||||
fi
|
|
||||||
|
|
||||||
$STD systemctl enable --now postgresql 2>/dev/null || true
|
|
||||||
|
|
||||||
# Get actual installed version
|
|
||||||
INSTALLED_VERSION="$(psql -V 2>/dev/null | awk '{print $3}' | cut -d. -f1)"
|
|
||||||
|
|
||||||
# Add PostgreSQL binaries to PATH
|
|
||||||
if ! grep -q '/usr/lib/postgresql' /etc/environment 2>/dev/null; then
|
|
||||||
echo 'PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/lib/postgresql/'"${INSTALLED_VERSION}"'/bin"' >/etc/environment
|
|
||||||
fi
|
|
||||||
|
|
||||||
cache_installed_version "postgresql" "$INSTALLED_VERSION"
|
|
||||||
msg_ok "Setup PostgreSQL $INSTALLED_VERSION (native)"
|
|
||||||
|
|
||||||
# Install optional modules if specified
|
|
||||||
if [[ -n "$PG_MODULES" ]]; then
|
|
||||||
IFS=',' read -ra MODULES <<<"$PG_MODULES"
|
|
||||||
for module in "${MODULES[@]}"; do
|
|
||||||
$STD apt install -y "postgresql-${INSTALLED_VERSION}-${module}" 2>/dev/null || true
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
SUITE=$(get_fallback_suite "$DISTRO_ID" "$DISTRO_CODENAME" "https://apt.postgresql.org/pub/repos/apt")
|
SUITE=$(get_fallback_suite "$DISTRO_ID" "$DISTRO_CODENAME" "https://apt.postgresql.org/pub/repos/apt")
|
||||||
@ -4831,3 +4762,214 @@ function setup_yq() {
|
|||||||
cache_installed_version "yq" "$FINAL_VERSION"
|
cache_installed_version "yq" "$FINAL_VERSION"
|
||||||
msg_ok "Setup yq $FINAL_VERSION"
|
msg_ok "Setup yq $FINAL_VERSION"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Docker Engine Installation and Management (All-In-One)
|
||||||
|
#
|
||||||
|
# Description:
|
||||||
|
# - Detects and migrates old Docker installations
|
||||||
|
# - Installs/Updates Docker Engine via official repository
|
||||||
|
# - Optional: Installs/Updates Portainer CE
|
||||||
|
# - Updates running containers interactively
|
||||||
|
# - Cleans up legacy repository files
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# setup_docker
|
||||||
|
# DOCKER_PORTAINER="true" setup_docker
|
||||||
|
# DOCKER_LOG_DRIVER="json-file" setup_docker
|
||||||
|
#
|
||||||
|
# Variables:
|
||||||
|
# DOCKER_PORTAINER - Install Portainer CE (optional, "true" to enable)
|
||||||
|
# DOCKER_LOG_DRIVER - Log driver (optional, default: "journald")
|
||||||
|
# DOCKER_SKIP_UPDATES - Skip container update check (optional, "true" to skip)
|
||||||
|
#
|
||||||
|
# Features:
|
||||||
|
# - Migrates from get.docker.com to repository-based installation
|
||||||
|
# - Updates Docker Engine if newer version available
|
||||||
|
# - Interactive container update with multi-select
|
||||||
|
# - Portainer installation and update support
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
function setup_docker() {
|
||||||
|
local docker_installed=false
|
||||||
|
local portainer_installed=false
|
||||||
|
|
||||||
|
# Check if Docker is already installed
|
||||||
|
if command -v docker &>/dev/null; then
|
||||||
|
docker_installed=true
|
||||||
|
DOCKER_CURRENT_VERSION=$(docker --version | grep -oP '\d+\.\d+\.\d+' | head -1)
|
||||||
|
msg_info "Docker $DOCKER_CURRENT_VERSION detected"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if Portainer is running
|
||||||
|
if docker ps --format '{{.Names}}' 2>/dev/null | grep -q '^portainer$'; then
|
||||||
|
portainer_installed=true
|
||||||
|
msg_info "Portainer container detected"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Cleanup old repository configurations
|
||||||
|
if [ -f /etc/apt/sources.list.d/docker.list ]; then
|
||||||
|
msg_info "Migrating from old Docker repository format"
|
||||||
|
rm -f /etc/apt/sources.list.d/docker.list
|
||||||
|
rm -f /etc/apt/keyrings/docker.asc
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Setup/Update Docker repository
|
||||||
|
msg_info "Setting up Docker Repository"
|
||||||
|
setup_deb822_repo \
|
||||||
|
"docker" \
|
||||||
|
"https://download.docker.com/linux/$(get_os_info id)/gpg" \
|
||||||
|
"https://download.docker.com/linux/$(get_os_info id)" \
|
||||||
|
"$(get_os_info codename)" \
|
||||||
|
"stable" \
|
||||||
|
"$(dpkg --print-architecture)"
|
||||||
|
|
||||||
|
# Install or upgrade Docker
|
||||||
|
if [ "$docker_installed" = true ]; then
|
||||||
|
msg_info "Checking for Docker updates"
|
||||||
|
DOCKER_LATEST_VERSION=$(apt-cache policy docker-ce | grep Candidate | awk '{print $2}' | cut -d':' -f2 | cut -d'-' -f1)
|
||||||
|
|
||||||
|
if [ "$DOCKER_CURRENT_VERSION" != "$DOCKER_LATEST_VERSION" ]; then
|
||||||
|
msg_info "Updating Docker $DOCKER_CURRENT_VERSION → $DOCKER_LATEST_VERSION"
|
||||||
|
$STD apt install -y --only-upgrade \
|
||||||
|
docker-ce \
|
||||||
|
docker-ce-cli \
|
||||||
|
containerd.io \
|
||||||
|
docker-buildx-plugin \
|
||||||
|
docker-compose-plugin
|
||||||
|
msg_ok "Updated Docker to $DOCKER_LATEST_VERSION"
|
||||||
|
else
|
||||||
|
msg_ok "Docker is up-to-date ($DOCKER_CURRENT_VERSION)"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
msg_info "Installing Docker"
|
||||||
|
$STD apt install -y \
|
||||||
|
docker-ce \
|
||||||
|
docker-ce-cli \
|
||||||
|
containerd.io \
|
||||||
|
docker-buildx-plugin \
|
||||||
|
docker-compose-plugin
|
||||||
|
|
||||||
|
DOCKER_CURRENT_VERSION=$(docker --version | grep -oP '\d+\.\d+\.\d+' | head -1)
|
||||||
|
msg_ok "Installed Docker $DOCKER_CURRENT_VERSION"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Configure daemon.json
|
||||||
|
local log_driver="${DOCKER_LOG_DRIVER:-journald}"
|
||||||
|
mkdir -p /etc/docker
|
||||||
|
if [ ! -f /etc/docker/daemon.json ]; then
|
||||||
|
cat <<EOF >/etc/docker/daemon.json
|
||||||
|
{
|
||||||
|
"log-driver": "$log_driver"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Enable and start Docker
|
||||||
|
systemctl enable -q --now docker
|
||||||
|
|
||||||
|
# Portainer Management
|
||||||
|
if [[ "${DOCKER_PORTAINER:-}" == "true" ]]; then
|
||||||
|
if [ "$portainer_installed" = true ]; then
|
||||||
|
msg_info "Checking for Portainer updates"
|
||||||
|
PORTAINER_CURRENT=$(docker inspect portainer --format='{{.Config.Image}}' 2>/dev/null | cut -d':' -f2)
|
||||||
|
PORTAINER_LATEST=$(curl -fsSL https://registry.hub.docker.com/v2/repositories/portainer/portainer-ce/tags?page_size=100 | grep -oP '"name":"\K[0-9]+\.[0-9]+\.[0-9]+"' | head -1 | tr -d '"')
|
||||||
|
|
||||||
|
if [ "$PORTAINER_CURRENT" != "$PORTAINER_LATEST" ]; then
|
||||||
|
read -r -p "${TAB3}Update Portainer $PORTAINER_CURRENT → $PORTAINER_LATEST? <y/N> " prompt
|
||||||
|
if [[ ${prompt,,} =~ ^(y|yes)$ ]]; then
|
||||||
|
msg_info "Updating Portainer"
|
||||||
|
docker stop portainer
|
||||||
|
docker rm portainer
|
||||||
|
docker pull portainer/portainer-ce:latest
|
||||||
|
docker run -d \
|
||||||
|
-p 9000:9000 \
|
||||||
|
-p 9443:9443 \
|
||||||
|
--name=portainer \
|
||||||
|
--restart=always \
|
||||||
|
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||||
|
-v portainer_data:/data \
|
||||||
|
portainer/portainer-ce:latest
|
||||||
|
msg_ok "Updated Portainer to $PORTAINER_LATEST"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
msg_ok "Portainer is up-to-date ($PORTAINER_CURRENT)"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
msg_info "Installing Portainer"
|
||||||
|
docker volume create portainer_data
|
||||||
|
docker run -d \
|
||||||
|
-p 9000:9000 \
|
||||||
|
-p 9443:9443 \
|
||||||
|
--name=portainer \
|
||||||
|
--restart=always \
|
||||||
|
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||||
|
-v portainer_data:/data \
|
||||||
|
portainer/portainer-ce:latest
|
||||||
|
|
||||||
|
LOCAL_IP=$(hostname -I | awk '{print $1}')
|
||||||
|
msg_ok "Installed Portainer (http://${LOCAL_IP}:9000)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Interactive Container Update Check
|
||||||
|
if [[ "${DOCKER_SKIP_UPDATES:-}" != "true" ]] && [ "$docker_installed" = true ]; then
|
||||||
|
msg_info "Checking for container updates"
|
||||||
|
|
||||||
|
# Get list of running containers with update status
|
||||||
|
local containers_with_updates=()
|
||||||
|
local container_info=()
|
||||||
|
local index=1
|
||||||
|
|
||||||
|
while IFS= read -r container; do
|
||||||
|
local name=$(echo "$container" | awk '{print $1}')
|
||||||
|
local image=$(echo "$container" | awk '{print $2}')
|
||||||
|
local current_digest=$(docker inspect "$name" --format='{{.Image}}' 2>/dev/null | cut -d':' -f2 | cut -c1-12)
|
||||||
|
|
||||||
|
# Pull latest image digest
|
||||||
|
docker pull "$image" >/dev/null 2>&1
|
||||||
|
local latest_digest=$(docker inspect "$image" --format='{{.Id}}' 2>/dev/null | cut -d':' -f2 | cut -c1-12)
|
||||||
|
|
||||||
|
if [ "$current_digest" != "$latest_digest" ]; then
|
||||||
|
containers_with_updates+=("$name")
|
||||||
|
container_info+=("${index}) ${name} (${image})")
|
||||||
|
((index++))
|
||||||
|
fi
|
||||||
|
done < <(docker ps --format '{{.Names}} {{.Image}}')
|
||||||
|
|
||||||
|
if [ ${#containers_with_updates[@]} -gt 0 ]; then
|
||||||
|
echo ""
|
||||||
|
echo "${TAB3}Container updates available:"
|
||||||
|
for info in "${container_info[@]}"; do
|
||||||
|
echo "${TAB3} $info"
|
||||||
|
done
|
||||||
|
echo ""
|
||||||
|
read -r -p "${TAB3}Select containers to update (e.g., 1,3,5 or 'all' or 'none'): " selection
|
||||||
|
|
||||||
|
if [[ ${selection,,} == "all" ]]; then
|
||||||
|
for container in "${containers_with_updates[@]}"; do
|
||||||
|
msg_info "Updating container: $container"
|
||||||
|
docker stop "$container"
|
||||||
|
docker rm "$container"
|
||||||
|
# Note: This requires the original docker run command - best to recreate via compose
|
||||||
|
msg_ok "Stopped and removed $container (please recreate with updated image)"
|
||||||
|
done
|
||||||
|
elif [[ ${selection,,} != "none" ]]; then
|
||||||
|
IFS=',' read -ra SELECTED <<<"$selection"
|
||||||
|
for num in "${SELECTED[@]}"; do
|
||||||
|
num=$(echo "$num" | xargs) # trim whitespace
|
||||||
|
if [[ "$num" =~ ^[0-9]+$ ]] && [ "$num" -ge 1 ] && [ "$num" -le "${#containers_with_updates[@]}" ]; then
|
||||||
|
container="${containers_with_updates[$((num - 1))]}"
|
||||||
|
msg_info "Updating container: $container"
|
||||||
|
docker stop "$container"
|
||||||
|
docker rm "$container"
|
||||||
|
msg_ok "Stopped and removed $container (please recreate with updated image)"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
msg_ok "All containers are up-to-date"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
msg_ok "Docker setup completed"
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user