Compare commits

...

8 Commits

Author SHA1 Message Date
CanbiZ (MickLesk)
290bef96df fix(ui): improve error output formatting and remove redundant log paths
- error_handler: Use msg_info/msg_ok/msg_warn for container cleanup
  instead of raw echo with manual ANSI codes
- error_handler: Add  icon before 'Remove broken container?' prompt
- error_handler: Indent log output with TAB for visual consistency
- build.func: Use msg_custom for installation log path display
- build.func: Use msg_info → msg_ok for container removal flow
- build.func: Use msg_warn for 'kept for debugging' message
- core.func/vm-core.func: Remove redundant container-internal log
  path display (📋 View full log) since combined log on host is
  the canonical location shown after failure
2026-02-14 14:33:21 +01:00
CanbiZ (MickLesk)
58f8158c47 fix(telemetry): improve error reporting with structured error strings and better categorization
- Add build_error_string() that creates structured format:
  'exit_code=N | description\n---\n<last 20 log lines>'
- Fix categorize_error() to map ALL known exit codes:
  - Added: shell(1,2), proxmox(200-231), service(150-154),
    database(170-193), runtime(243-249), signal(139,141,143)
  - Split timeout from network (28 was in both)
  - Added DPKG(255) to dependency category
- Update all API functions to use build_error_string():
  post_update_to_api, post_update_to_api_extended,
  post_tool_to_api, post_addon_to_api
- Add ensure_log_on_host() calls to on_exit, on_interrupt,
  on_terminate handlers to prevent race condition where
  telemetry reports before container log is pulled to host
2026-02-14 14:29:43 +01:00
CanbiZ (MickLesk)
f762155870 feat(core): merge ProxmoxVED core.func with prompt utilities and unattended mode 2026-02-14 13:48:06 +01:00
community-scripts-pr-app[bot]
867d02d969 Update CHANGELOG.md (#11903)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-02-14 12:41:34 +00:00
CanbiZ (MickLesk)
1a9bbdd6e7 core: unified logging system with combined logs (#11761)
* refactor(logging): unified logging system with combined logs

- Add log_msg(), log_section(), strip_ansi() helper functions to core.func
- Extend msg_ok, msg_error, msg_warn, msg_info, msg_custom to write to log file
- Log container settings (default and advanced) to log file
- Combine host creation log and container installation log on failure
- Use app-specific log path: /tmp/{app}-{ctid}-{session}.log
- Add timestamps and section headers in log files
- Improve get_error_text() with combined log fallback chain
- Add ensure_log_on_host() for trap handlers to pull logs before API reporting

* linting
2026-02-14 13:41:09 +01:00
community-scripts-pr-app[bot]
3e0db150bd chore: update github-versions.json (#11902)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-02-14 12:08:49 +00:00
community-scripts-pr-app[bot]
6b3653627c Update CHANGELOG.md (#11899)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-02-14 10:39:34 +00:00
Copilot
733ad75dc1 Disable UniFi script - APT packages no longer available (#11898)
* Initial plan

* Add disabled flag and description to unifi.json

Co-authored-by: MickLesk <47820557+MickLesk@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: MickLesk <47820557+MickLesk@users.noreply.github.com>
2026-02-14 11:39:11 +01:00
8 changed files with 1063 additions and 222 deletions

View File

@@ -403,6 +403,16 @@ Exercise vigilance regarding copycat or coat-tailing sites that seek to exploit
## 2026-02-14
### 💾 Core
- #### ✨ New Features
- core: unified logging system with combined logs [@MickLesk](https://github.com/MickLesk) ([#11761](https://github.com/community-scripts/ProxmoxVE/pull/11761))
### ❔ Uncategorized
- Disable UniFi script - APT packages no longer available [@Copilot](https://github.com/Copilot) ([#11898](https://github.com/community-scripts/ProxmoxVE/pull/11898))
## 2026-02-13
### 🚀 Updated Scripts

View File

@@ -1,5 +1,5 @@
{
"generated": "2026-02-14T06:14:47Z",
"generated": "2026-02-14T12:08:41Z",
"versions": [
{
"slug": "2fauth",
@@ -284,9 +284,9 @@
{
"slug": "domain-locker",
"repo": "Lissy93/domain-locker",
"version": "v0.1.3",
"version": "v0.1.4",
"pinned": false,
"date": "2026-02-11T10:03:32Z"
"date": "2026-02-14T07:41:29Z"
},
{
"slug": "domain-monitor",
@@ -354,9 +354,9 @@
{
"slug": "firefly",
"repo": "firefly-iii/firefly-iii",
"version": "v6.4.18",
"version": "v6.4.19",
"pinned": false,
"date": "2026-02-08T07:28:00Z"
"date": "2026-02-14T11:55:40Z"
},
{
"slug": "fladder",
@@ -823,9 +823,9 @@
{
"slug": "metube",
"repo": "alexta69/metube",
"version": "2026.02.13",
"version": "2026.02.14",
"pinned": false,
"date": "2026-02-13T15:18:17Z"
"date": "2026-02-14T07:49:11Z"
},
{
"slug": "miniflux",

View File

@@ -14,6 +14,8 @@
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/ubiquiti-unifi.webp",
"config_path": "",
"description": "UniFi Network Server is a software that helps manage and monitor UniFi networks (Wi-Fi, Ethernet, etc.) by providing an intuitive user interface and advanced features. It allows network administrators to configure, monitor, and upgrade network devices, as well as view network statistics, client devices, and historical events. The aim of the application is to make the management of UniFi networks easier and more efficient.",
"disable": true,
"disable_description": "This script is disabled because UniFi no longer delivers APT packages for Debian systems. The installation relies on APT repositories that are no longer maintained or available. For more details, see: https://github.com/community-scripts/ProxmoxVE/issues/11876",
"install_methods": [
{
"type": "default",

View File

@@ -242,7 +242,7 @@ explain_exit_code() {
json_escape() {
local s="$1"
s=${s//\\/\\\\}
s=${s//"/\\"}
s=${s//"/\\"/}
s=${s//$'\n'/\\n}
s=${s//$'\r'/}
s=${s//$'\t'/\\t}
@@ -253,7 +253,8 @@ json_escape() {
# get_error_text()
#
# - Returns last 20 lines of the active log (INSTALL_LOG or BUILD_LOG)
# - Falls back to empty string if no log is available
# - Falls back to combined log or BUILD_LOG if primary is not accessible
# - Handles container paths that don't exist on the host
# ------------------------------------------------------------------------------
get_error_text() {
local logfile=""
@@ -265,11 +266,53 @@ get_error_text() {
logfile="$BUILD_LOG"
fi
# If logfile is inside container (e.g. /root/.install-*), try the host copy
if [[ -n "$logfile" && ! -s "$logfile" ]]; then
# Try combined log: /tmp/<app>-<CTID>-<SESSION_ID>.log
if [[ -n "${CTID:-}" && -n "${SESSION_ID:-}" ]]; then
local combined_log="/tmp/${NSAPP:-lxc}-${CTID}-${SESSION_ID}.log"
if [[ -s "$combined_log" ]]; then
logfile="$combined_log"
fi
fi
fi
# Also try BUILD_LOG as fallback if primary log is empty/missing
if [[ -z "$logfile" || ! -s "$logfile" ]] && [[ -n "${BUILD_LOG:-}" && -s "${BUILD_LOG}" ]]; then
logfile="$BUILD_LOG"
fi
if [[ -n "$logfile" && -s "$logfile" ]]; then
tail -n 20 "$logfile" 2>/dev/null | sed 's/\r$//'
fi
}
# ------------------------------------------------------------------------------
# build_error_string()
#
# - Builds a structured error string for telemetry reporting
# - Format: "exit_code=<N> | <explanation>\n---\n<last 20 log lines>"
# - If no log lines available, returns just the explanation
# - Arguments:
# * $1: exit_code (numeric)
# * $2: log_text (optional, output from get_error_text)
# - Returns structured error string via stdout
# ------------------------------------------------------------------------------
build_error_string() {
local exit_code="${1:-1}"
local log_text="${2:-}"
local explanation
explanation=$(explain_exit_code "$exit_code")
if [[ -n "$log_text" ]]; then
# Structured format: header + separator + log lines
printf 'exit_code=%s | %s\n---\n%s' "$exit_code" "$explanation" "$log_text"
else
# No log available - just the explanation with exit code
printf 'exit_code=%s | %s' "$exit_code" "$explanation"
fi
}
# ==============================================================================
# SECTION 2: TELEMETRY FUNCTIONS
# ==============================================================================
@@ -648,13 +691,12 @@ post_update_to_api() {
else
exit_code=1
fi
# Get log lines and build structured error string
local error_text=""
error_text=$(get_error_text)
if [[ -n "$error_text" ]]; then
error=$(json_escape "$error_text")
else
error=$(json_escape "$(explain_exit_code "$exit_code")")
fi
local full_error
full_error=$(build_error_string "$exit_code" "$error_text")
error=$(json_escape "$full_error")
short_error=$(json_escape "$(explain_exit_code "$exit_code")")
error_category=$(categorize_error "$exit_code")
[[ -z "$error" ]] && error="Unknown error"
@@ -797,31 +839,52 @@ EOF
categorize_error() {
local code="$1"
case "$code" in
# Network errors
6 | 7 | 22 | 28 | 35) echo "network" ;;
# Network errors (curl/wget)
6 | 7 | 22 | 35) echo "network" ;;
# Storage errors
214 | 217 | 219) echo "storage" ;;
# Timeout errors
28 | 124 | 211) echo "timeout" ;;
# Dependency/Package errors
100 | 101 | 102 | 127 | 160 | 161 | 162) echo "dependency" ;;
# Storage errors (Proxmox storage)
214 | 217 | 219 | 224) echo "storage" ;;
# Dependency/Package errors (APT, DPKG, pip, commands)
100 | 101 | 102 | 127 | 160 | 161 | 162 | 255) echo "dependency" ;;
# Permission errors
126 | 152) echo "permission" ;;
# Timeout errors
124 | 28 | 211) echo "timeout" ;;
# Configuration errors (Proxmox config, invalid args)
128 | 203 | 204 | 205 | 206 | 207 | 208) echo "config" ;;
# Configuration errors
203 | 204 | 205 | 206 | 207 | 208) echo "config" ;;
# Proxmox container/template errors
200 | 209 | 210 | 212 | 213 | 215 | 216 | 218 | 220 | 221 | 222 | 223 | 225 | 231) echo "proxmox" ;;
# Service/Systemd errors
150 | 151 | 153 | 154) echo "service" ;;
# Database errors (PostgreSQL, MySQL, MongoDB)
170 | 171 | 172 | 173 | 180 | 181 | 182 | 183 | 190 | 191 | 192 | 193) echo "database" ;;
# Node.js / JavaScript runtime errors
243 | 245 | 246 | 247 | 248 | 249) echo "runtime" ;;
# Python environment errors
# (already covered: 160-162 under dependency)
# Aborted by user
130) echo "aborted" ;;
# Resource errors (OOM, etc)
137 | 134) echo "resource" ;;
# Resource errors (OOM, SIGKILL, SIGABRT)
134 | 137) echo "resource" ;;
# Default
# Signal/Process errors (SIGTERM, SIGPIPE, SIGSEGV)
139 | 141 | 143) echo "signal" ;;
# Shell errors (general error, syntax error)
1 | 2) echo "shell" ;;
# Default - truly unknown
*) echo "unknown" ;;
esac
}
@@ -884,11 +947,9 @@ post_tool_to_api() {
[[ ! "$exit_code" =~ ^[0-9]+$ ]] && exit_code=1
local error_text=""
error_text=$(get_error_text)
if [[ -n "$error_text" ]]; then
error=$(json_escape "$error_text")
else
error=$(json_escape "$(explain_exit_code "$exit_code")")
fi
local full_error
full_error=$(build_error_string "$exit_code" "$error_text")
error=$(json_escape "$full_error")
error_category=$(categorize_error "$exit_code")
fi
@@ -951,11 +1012,9 @@ post_addon_to_api() {
[[ ! "$exit_code" =~ ^[0-9]+$ ]] && exit_code=1
local error_text=""
error_text=$(get_error_text)
if [[ -n "$error_text" ]]; then
error=$(json_escape "$error_text")
else
error=$(json_escape "$(explain_exit_code "$exit_code")")
fi
local full_error
full_error=$(build_error_string "$exit_code" "$error_text")
error=$(json_escape "$full_error")
error_category=$(categorize_error "$exit_code")
fi
@@ -1050,11 +1109,9 @@ post_update_to_api_extended() {
fi
local error_text=""
error_text=$(get_error_text)
if [[ -n "$error_text" ]]; then
error=$(json_escape "$error_text")
else
error=$(json_escape "$(explain_exit_code "$exit_code")")
fi
local full_error
full_error=$(build_error_string "$exit_code" "$error_text")
error=$(json_escape "$full_error")
error_category=$(categorize_error "$exit_code")
[[ -z "$error" ]] && error="Unknown error"
fi

View File

@@ -38,15 +38,16 @@
# - Captures app-declared resource defaults (CPU, RAM, Disk)
# ------------------------------------------------------------------------------
variables() {
NSAPP=$(echo "${APP,,}" | tr -d ' ') # This function sets the NSAPP variable by converting the value of the APP variable to lowercase and removing any spaces.
var_install="${NSAPP}-install" # sets the var_install variable by appending "-install" to the value of NSAPP.
INTEGER='^[0-9]+([.][0-9]+)?$' # it defines the INTEGER regular expression pattern.
PVEHOST_NAME=$(hostname) # gets the Proxmox Hostname and sets it to Uppercase
DIAGNOSTICS="yes" # sets the DIAGNOSTICS variable to "yes", used for the API call.
METHOD="default" # sets the METHOD variable to "default", used for the API call.
RANDOM_UUID="$(cat /proc/sys/kernel/random/uuid)" # generates a random UUID and sets it to the RANDOM_UUID variable.
SESSION_ID="${RANDOM_UUID:0:8}" # Short session ID (first 8 chars of UUID) for log files
BUILD_LOG="/tmp/create-lxc-${SESSION_ID}.log" # Host-side container creation log
NSAPP=$(echo "${APP,,}" | tr -d ' ') # This function sets the NSAPP variable by converting the value of the APP variable to lowercase and removing any spaces.
var_install="${NSAPP}-install" # sets the var_install variable by appending "-install" to the value of NSAPP.
INTEGER='^[0-9]+([.][0-9]+)?$' # it defines the INTEGER regular expression pattern.
PVEHOST_NAME=$(hostname) # gets the Proxmox Hostname and sets it to Uppercase
DIAGNOSTICS="yes" # sets the DIAGNOSTICS variable to "yes", used for the API call.
METHOD="default" # sets the METHOD variable to "default", used for the API call.
RANDOM_UUID="$(cat /proc/sys/kernel/random/uuid)" # generates a random UUID and sets it to the RANDOM_UUID variable.
SESSION_ID="${RANDOM_UUID:0:8}" # Short session ID (first 8 chars of UUID) for log files
BUILD_LOG="/tmp/create-lxc-${SESSION_ID}.log" # Host-side container creation log
combined_log="/tmp/install-${SESSION_ID}-combined.log" # Combined log (build + install) for failed installations
CTTYPE="${CTTYPE:-${CT_TYPE:-1}}"
# Parse dev_mode early
@@ -217,7 +218,7 @@ update_motd_ip() {
local current_os="$(grep ^NAME /etc/os-release | cut -d= -f2 | tr -d '"') - Version: $(grep ^VERSION_ID /etc/os-release | cut -d= -f2 | tr -d '"')"
local current_hostname="$(hostname)"
local current_ip="$(hostname -I | awk '{print $1}')"
# Update only if values actually changed
if ! grep -q "OS:.*$current_os" "$PROFILE_FILE" 2>/dev/null; then
sed -i "s|OS:.*|OS: \${GN}$current_os\${CL}\\\"|" "$PROFILE_FILE"
@@ -385,7 +386,7 @@ validate_hostname() {
# Split by dots and validate each label
local IFS='.'
read -ra labels <<< "$hostname"
read -ra labels <<<"$hostname"
for label in "${labels[@]}"; do
# Each label: 1-63 chars, alphanumeric, hyphens allowed (not at start/end)
if [[ -z "$label" ]] || [[ ${#label} -gt 63 ]]; then
@@ -489,7 +490,7 @@ validate_ipv6_address() {
# Check that no segment exceeds 4 hex chars
local IFS=':'
local -a segments
read -ra segments <<< "$addr"
read -ra segments <<<"$addr"
for seg in "${segments[@]}"; do
if [[ ${#seg} -gt 4 ]]; then
return 1
@@ -539,14 +540,14 @@ validate_gateway_in_subnet() {
# Convert IPs to integers
local IFS='.'
read -r i1 i2 i3 i4 <<< "$ip"
read -r g1 g2 g3 g4 <<< "$gateway"
read -r i1 i2 i3 i4 <<<"$ip"
read -r g1 g2 g3 g4 <<<"$gateway"
local ip_int=$(( (i1 << 24) + (i2 << 16) + (i3 << 8) + i4 ))
local gw_int=$(( (g1 << 24) + (g2 << 16) + (g3 << 8) + g4 ))
local ip_int=$(((i1 << 24) + (i2 << 16) + (i3 << 8) + i4))
local gw_int=$(((g1 << 24) + (g2 << 16) + (g3 << 8) + g4))
# Check if both are in same network
if (( (ip_int & mask) != (gw_int & mask) )); then
if (((ip_int & mask) != (gw_int & mask))); then
return 1
fi
@@ -1079,117 +1080,117 @@ load_vars_file() {
# Validate values before setting (skip empty values - they use defaults)
if [[ -n "$var_val" ]]; then
case "$var_key" in
var_mac)
if ! validate_mac_address "$var_val"; then
msg_warn "Invalid MAC address '$var_val' in $file, ignoring"
var_mac)
if ! validate_mac_address "$var_val"; then
msg_warn "Invalid MAC address '$var_val' in $file, ignoring"
continue
fi
;;
var_vlan)
if ! validate_vlan_tag "$var_val"; then
msg_warn "Invalid VLAN tag '$var_val' in $file (must be 1-4094), ignoring"
continue
fi
;;
var_mtu)
if ! validate_mtu "$var_val"; then
msg_warn "Invalid MTU '$var_val' in $file (must be 576-65535), ignoring"
continue
fi
;;
var_tags)
if ! validate_tags "$var_val"; then
msg_warn "Invalid tags '$var_val' in $file (alphanumeric, -, _, ; only), ignoring"
continue
fi
;;
var_timezone)
if ! validate_timezone "$var_val"; then
msg_warn "Invalid timezone '$var_val' in $file, ignoring"
continue
fi
;;
var_brg)
if ! validate_bridge "$var_val"; then
msg_warn "Bridge '$var_val' not found in $file, ignoring"
continue
fi
;;
var_gateway)
if ! validate_gateway_ip "$var_val"; then
msg_warn "Invalid gateway IP '$var_val' in $file, ignoring"
continue
fi
;;
var_hostname)
if ! validate_hostname "$var_val"; then
msg_warn "Invalid hostname '$var_val' in $file, ignoring"
continue
fi
;;
var_cpu)
if ! [[ "$var_val" =~ ^[0-9]+$ ]] || ((var_val < 1 || var_val > 128)); then
msg_warn "Invalid CPU count '$var_val' in $file (must be 1-128), ignoring"
continue
fi
;;
var_ram)
if ! [[ "$var_val" =~ ^[0-9]+$ ]] || ((var_val < 256)); then
msg_warn "Invalid RAM '$var_val' in $file (must be >= 256 MiB), ignoring"
continue
fi
;;
var_disk)
if ! [[ "$var_val" =~ ^[0-9]+$ ]] || ((var_val < 1)); then
msg_warn "Invalid disk size '$var_val' in $file (must be >= 1 GB), ignoring"
continue
fi
;;
var_unprivileged)
if [[ "$var_val" != "0" && "$var_val" != "1" ]]; then
msg_warn "Invalid unprivileged value '$var_val' in $file (must be 0 or 1), ignoring"
continue
fi
;;
var_nesting)
if [[ "$var_val" != "0" && "$var_val" != "1" ]]; then
msg_warn "Invalid nesting value '$var_val' in $file (must be 0 or 1), ignoring"
continue
fi
# Warn about potential issues with systemd-based OS when nesting is disabled via vars file
if [[ "$var_val" == "0" && "${var_os:-debian}" != "alpine" ]]; then
msg_warn "Nesting disabled in $file - modern systemd-based distributions may require nesting for proper operation"
fi
;;
var_keyctl)
if [[ "$var_val" != "0" && "$var_val" != "1" ]]; then
msg_warn "Invalid keyctl value '$var_val' in $file (must be 0 or 1), ignoring"
continue
fi
;;
var_net)
# var_net can be: dhcp, static IP/CIDR, or IP range
if [[ "$var_val" != "dhcp" ]]; then
if is_ip_range "$var_val"; then
: # IP range is valid, will be resolved at runtime
elif ! validate_ip_address "$var_val"; then
msg_warn "Invalid network '$var_val' in $file (must be dhcp or IP/CIDR), ignoring"
continue
fi
;;
var_vlan)
if ! validate_vlan_tag "$var_val"; then
msg_warn "Invalid VLAN tag '$var_val' in $file (must be 1-4094), ignoring"
continue
fi
;;
var_mtu)
if ! validate_mtu "$var_val"; then
msg_warn "Invalid MTU '$var_val' in $file (must be 576-65535), ignoring"
continue
fi
;;
var_tags)
if ! validate_tags "$var_val"; then
msg_warn "Invalid tags '$var_val' in $file (alphanumeric, -, _, ; only), ignoring"
continue
fi
;;
var_timezone)
if ! validate_timezone "$var_val"; then
msg_warn "Invalid timezone '$var_val' in $file, ignoring"
continue
fi
;;
var_brg)
if ! validate_bridge "$var_val"; then
msg_warn "Bridge '$var_val' not found in $file, ignoring"
continue
fi
;;
var_gateway)
if ! validate_gateway_ip "$var_val"; then
msg_warn "Invalid gateway IP '$var_val' in $file, ignoring"
continue
fi
;;
var_hostname)
if ! validate_hostname "$var_val"; then
msg_warn "Invalid hostname '$var_val' in $file, ignoring"
continue
fi
;;
var_cpu)
if ! [[ "$var_val" =~ ^[0-9]+$ ]] || ((var_val < 1 || var_val > 128)); then
msg_warn "Invalid CPU count '$var_val' in $file (must be 1-128), ignoring"
continue
fi
;;
var_ram)
if ! [[ "$var_val" =~ ^[0-9]+$ ]] || ((var_val < 256)); then
msg_warn "Invalid RAM '$var_val' in $file (must be >= 256 MiB), ignoring"
continue
fi
;;
var_disk)
if ! [[ "$var_val" =~ ^[0-9]+$ ]] || ((var_val < 1)); then
msg_warn "Invalid disk size '$var_val' in $file (must be >= 1 GB), ignoring"
continue
fi
;;
var_unprivileged)
if [[ "$var_val" != "0" && "$var_val" != "1" ]]; then
msg_warn "Invalid unprivileged value '$var_val' in $file (must be 0 or 1), ignoring"
continue
fi
;;
var_nesting)
if [[ "$var_val" != "0" && "$var_val" != "1" ]]; then
msg_warn "Invalid nesting value '$var_val' in $file (must be 0 or 1), ignoring"
continue
fi
# Warn about potential issues with systemd-based OS when nesting is disabled via vars file
if [[ "$var_val" == "0" && "${var_os:-debian}" != "alpine" ]]; then
msg_warn "Nesting disabled in $file - modern systemd-based distributions may require nesting for proper operation"
fi
;;
var_keyctl)
if [[ "$var_val" != "0" && "$var_val" != "1" ]]; then
msg_warn "Invalid keyctl value '$var_val' in $file (must be 0 or 1), ignoring"
continue
fi
;;
var_net)
# var_net can be: dhcp, static IP/CIDR, or IP range
if [[ "$var_val" != "dhcp" ]]; then
if is_ip_range "$var_val"; then
: # IP range is valid, will be resolved at runtime
elif ! validate_ip_address "$var_val"; then
msg_warn "Invalid network '$var_val' in $file (must be dhcp or IP/CIDR), ignoring"
continue
fi
fi
;;
var_fuse|var_tun|var_gpu|var_ssh|var_verbose|var_protection)
if [[ "$var_val" != "yes" && "$var_val" != "no" ]]; then
msg_warn "Invalid boolean '$var_val' for $var_key in $file (must be yes/no), ignoring"
continue
fi
;;
var_ipv6_method)
if [[ "$var_val" != "auto" && "$var_val" != "dhcp" && "$var_val" != "static" && "$var_val" != "none" ]]; then
msg_warn "Invalid IPv6 method '$var_val' in $file (must be auto/dhcp/static/none), ignoring"
continue
fi
;;
fi
;;
var_fuse | var_tun | var_gpu | var_ssh | var_verbose | var_protection)
if [[ "$var_val" != "yes" && "$var_val" != "no" ]]; then
msg_warn "Invalid boolean '$var_val' for $var_key in $file (must be yes/no), ignoring"
continue
fi
;;
var_ipv6_method)
if [[ "$var_val" != "auto" && "$var_val" != "dhcp" && "$var_val" != "static" && "$var_val" != "none" ]]; then
msg_warn "Invalid IPv6 method '$var_val' in $file (must be auto/dhcp/static/none), ignoring"
continue
fi
;;
esac
fi
@@ -2764,6 +2765,26 @@ Advanced:
[[ "$APT_CACHER" == "yes" ]] && echo -e "${INFO}${BOLD}${DGN}APT Cacher: ${BGN}$APT_CACHER_IP${CL}"
echo -e "${SEARCH}${BOLD}${DGN}Verbose Mode: ${BGN}$VERBOSE${CL}"
echo -e "${CREATING}${BOLD}${RD}Creating an LXC of ${APP} using the above advanced settings${CL}"
# Log settings to file
log_section "CONTAINER SETTINGS (ADVANCED) - ${APP}"
log_msg "Application: ${APP}"
log_msg "PVE Version: ${PVEVERSION} (Kernel: ${KERNEL_VERSION})"
log_msg "Operating System: $var_os ($var_version)"
log_msg "Container Type: $([ "$CT_TYPE" == "1" ] && echo "Unprivileged" || echo "Privileged")"
log_msg "Container ID: $CT_ID"
log_msg "Hostname: $HN"
log_msg "Disk Size: ${DISK_SIZE} GB"
log_msg "CPU Cores: $CORE_COUNT"
log_msg "RAM Size: ${RAM_SIZE} MiB"
log_msg "Bridge: $BRG"
log_msg "IPv4: $NET"
log_msg "IPv6: $IPV6_METHOD"
log_msg "FUSE Support: ${ENABLE_FUSE:-no}"
log_msg "Nesting: $([ "${ENABLE_NESTING:-1}" == "1" ] && echo "Enabled" || echo "Disabled")"
log_msg "GPU Passthrough: ${ENABLE_GPU:-no}"
log_msg "Verbose Mode: $VERBOSE"
log_msg "Session ID: ${SESSION_ID}"
}
# ==============================================================================
@@ -2871,6 +2892,7 @@ diagnostics_menu() {
# - Prints summary of default values (ID, OS, type, disk, RAM, CPU, etc.)
# - Uses icons and formatting for readability
# - Convert CT_TYPE to description
# - Also logs settings to log file for debugging
# ------------------------------------------------------------------------------
echo_default() {
CT_TYPE_DESC="Unprivileged"
@@ -2892,6 +2914,20 @@ echo_default() {
fi
echo -e "${CREATING}${BOLD}${BL}Creating a ${APP} LXC using the above default settings${CL}"
echo -e " "
# Log settings to file
log_section "CONTAINER SETTINGS - ${APP}"
log_msg "Application: ${APP}"
log_msg "PVE Version: ${PVEVERSION} (Kernel: ${KERNEL_VERSION})"
log_msg "Container ID: ${CT_ID}"
log_msg "Operating System: $var_os ($var_version)"
log_msg "Container Type: $CT_TYPE_DESC"
log_msg "Disk Size: ${DISK_SIZE} GB"
log_msg "CPU Cores: ${CORE_COUNT}"
log_msg "RAM Size: ${RAM_SIZE} MiB"
[[ -n "${var_gpu:-}" && "${var_gpu}" == "yes" ]] && log_msg "GPU Passthrough: Enabled"
[[ "$VERBOSE" == "yes" ]] && log_msg "Verbose Mode: Enabled"
log_msg "Session ID: ${SESSION_ID}"
}
# ------------------------------------------------------------------------------
@@ -4049,30 +4085,55 @@ EOF'
# Copy install log from container BEFORE API call so get_error_text() can read it
local build_log_copied=false
local install_log_copied=false
local host_install_log="/tmp/install-lxc-${CTID}-${SESSION_ID}.log"
local combined_log="/tmp/${NSAPP:-lxc}-${CTID}-${SESSION_ID}.log"
if [[ -n "$CTID" && -n "${SESSION_ID:-}" ]]; then
# Copy BUILD_LOG (creation log) if it exists
# Create combined log with header
{
echo "================================================================================"
echo "COMBINED INSTALLATION LOG - ${APP:-LXC}"
echo "Container ID: ${CTID}"
echo "Session ID: ${SESSION_ID}"
echo "Timestamp: $(date '+%Y-%m-%d %H:%M:%S')"
echo "================================================================================"
echo ""
} >"$combined_log"
# Append BUILD_LOG (host-side creation log) if it exists
if [[ -f "${BUILD_LOG}" ]]; then
cp "${BUILD_LOG}" "/tmp/create-lxc-${CTID}-${SESSION_ID}.log" 2>/dev/null && build_log_copied=true
{
echo "================================================================================"
echo "PHASE 1: CONTAINER CREATION (Host)"
echo "================================================================================"
cat "${BUILD_LOG}"
echo ""
} >>"$combined_log"
build_log_copied=true
fi
# Copy INSTALL_LOG from container to host
if pct pull "$CTID" "/root/.install-${SESSION_ID}.log" "$host_install_log" 2>/dev/null; then
# Copy and append INSTALL_LOG from container
local temp_install_log="/tmp/.install-temp-${SESSION_ID}.log"
if pct pull "$CTID" "/root/.install-${SESSION_ID}.log" "$temp_install_log" 2>/dev/null; then
{
echo "================================================================================"
echo "PHASE 2: APPLICATION INSTALLATION (Container)"
echo "================================================================================"
cat "$temp_install_log"
echo ""
} >>"$combined_log"
rm -f "$temp_install_log"
install_log_copied=true
# Point INSTALL_LOG to host copy so get_error_text() finds it
INSTALL_LOG="$host_install_log"
# Point INSTALL_LOG to combined log so get_error_text() finds it
INSTALL_LOG="$combined_log"
fi
fi
# Report failure to telemetry API (now with log available on host)
post_update_to_api "failed" "$install_exit_code"
# Show available logs
# Show combined log location
if [[ -n "$CTID" && -n "${SESSION_ID:-}" ]]; then
echo ""
[[ "$build_log_copied" == true ]] && echo -e "${GN}${CL} Container creation log: ${BL}/tmp/create-lxc-${CTID}-${SESSION_ID}.log${CL}"
[[ "$install_log_copied" == true ]] && echo -e "${GN}${CL} Installation log: ${BL}${host_install_log}${CL}"
msg_custom "📋" "${YW}" "Installation log: ${combined_log}"
fi
# Dev mode: Keep container or open breakpoint shell
@@ -4095,19 +4156,21 @@ EOF'
exit $install_exit_code
fi
# Prompt user for cleanup with 60s timeout (plain echo - no msg_info to avoid spinner)
# Prompt user for cleanup with 60s timeout
echo ""
echo -en "${YW}Remove broken container ${CTID}? (Y/n) [auto-remove in 60s]: ${CL}"
echo -en "${TAB}${TAB}${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
# Remove container
echo -e "\n${TAB}${HOLD}${YW}Removing container ${CTID}${CL}"
echo ""
msg_info "Removing container ${CTID}"
pct stop "$CTID" &>/dev/null || true
pct destroy "$CTID" &>/dev/null || true
echo -e "${BFR}${CM}${GN}Container ${CTID} removed${CL}"
msg_ok "Container ${CTID} removed"
elif [[ "$response" =~ ^[Nn]$ ]]; then
echo -e "\n${TAB}${YW}Container ${CTID} kept for debugging${CL}"
echo ""
msg_warn "Container ${CTID} kept for debugging"
# Dev mode: Setup MOTD/SSH for debugging access to broken container
if [[ "${DEV_MODE_MOTD:-false}" == "true" ]]; then
@@ -4123,11 +4186,11 @@ EOF'
fi
else
# Timeout - auto-remove
echo -e "\n${YW}No response - auto-removing container${CL}"
echo -e "${TAB}${HOLD}${YW}Removing container ${CTID}${CL}"
echo ""
msg_info "No response - removing container ${CTID}"
pct stop "$CTID" &>/dev/null || true
pct destroy "$CTID" &>/dev/null || true
echo -e "${BFR}${CM}${GN}Container ${CTID} removed${CL}"
msg_ok "Container ${CTID} removed"
fi
# Force one final status update attempt after cleanup
@@ -5137,6 +5200,61 @@ EOF
# SECTION 10: ERROR HANDLING & EXIT TRAPS
# ==============================================================================
# ------------------------------------------------------------------------------
# ensure_log_on_host()
#
# - Ensures INSTALL_LOG points to a readable file on the host
# - If INSTALL_LOG points to a container path (e.g. /root/.install-*),
# tries to pull it from the container and create a combined log
# - This allows get_error_text() to find actual error output for telemetry
# ------------------------------------------------------------------------------
ensure_log_on_host() {
# Already readable on host? Nothing to do.
[[ -n "${INSTALL_LOG:-}" && -s "${INSTALL_LOG}" ]] && return 0
# Try pulling from container and creating combined log
if [[ -n "${CTID:-}" && -n "${SESSION_ID:-}" ]] && command -v pct &>/dev/null; then
local combined_log="/tmp/${NSAPP:-lxc}-${CTID}-${SESSION_ID}.log"
if [[ ! -s "$combined_log" ]]; then
# Create combined log
{
echo "================================================================================"
echo "COMBINED INSTALLATION LOG - ${APP:-LXC}"
echo "Container ID: ${CTID}"
echo "Session ID: ${SESSION_ID}"
echo "Timestamp: $(date '+%Y-%m-%d %H:%M:%S')"
echo "================================================================================"
echo ""
} >"$combined_log" 2>/dev/null || return 0
# Append BUILD_LOG if it exists
if [[ -f "${BUILD_LOG:-}" ]]; then
{
echo "================================================================================"
echo "PHASE 1: CONTAINER CREATION (Host)"
echo "================================================================================"
cat "${BUILD_LOG}"
echo ""
} >>"$combined_log"
fi
# Pull INSTALL_LOG from container
local temp_log="/tmp/.install-temp-${SESSION_ID}.log"
if pct pull "$CTID" "/root/.install-${SESSION_ID}.log" "$temp_log" 2>/dev/null; then
{
echo "================================================================================"
echo "PHASE 2: APPLICATION INSTALLATION (Container)"
echo "================================================================================"
cat "$temp_log"
echo ""
} >>"$combined_log"
rm -f "$temp_log"
fi
fi
if [[ -s "$combined_log" ]]; then
INSTALL_LOG="$combined_log"
fi
fi
}
# ------------------------------------------------------------------------------
# api_exit_script()
#
@@ -5149,6 +5267,7 @@ EOF
api_exit_script() {
exit_code=$?
if [ $exit_code -ne 0 ]; then
ensure_log_on_host
post_update_to_api "failed" "$exit_code"
fi
}
@@ -5156,6 +5275,6 @@ api_exit_script() {
if command -v pveversion >/dev/null 2>&1; then
trap 'api_exit_script' EXIT
fi
trap 'post_update_to_api "failed" "$?"' ERR
trap 'post_update_to_api "failed" "130"' SIGINT
trap 'post_update_to_api "failed" "143"' SIGTERM
trap 'ensure_log_on_host; post_update_to_api "failed" "$?"' ERR
trap 'ensure_log_on_host; post_update_to_api "failed" "130"' SIGINT
trap 'ensure_log_on_host; post_update_to_api "failed" "143"' SIGTERM

View File

@@ -115,7 +115,7 @@ icons() {
BRIDGE="${TAB}🌉${TAB}${CL}"
NETWORK="${TAB}📡${TAB}${CL}"
GATEWAY="${TAB}🌐${TAB}${CL}"
DISABLEIPV6="${TAB}🚫${TAB}${CL}"
ICON_DISABLEIPV6="${TAB}🚫${TAB}${CL}"
DEFAULT="${TAB}⚙️${TAB}${CL}"
MACADDRESS="${TAB}🔗${TAB}${CL}"
VLANTAG="${TAB}🏷️${TAB}${CL}"
@@ -413,6 +413,69 @@ get_active_logfile() {
# Legacy compatibility: SILENT_LOGFILE points to active log
SILENT_LOGFILE="$(get_active_logfile)"
# ------------------------------------------------------------------------------
# strip_ansi()
#
# - Removes ANSI escape sequences from input text
# - Used to clean colored output for log files
# - Handles both piped input and arguments
# ------------------------------------------------------------------------------
strip_ansi() {
if [[ $# -gt 0 ]]; then
echo -e "$*" | sed 's/\x1b\[[0-9;]*m//g; s/\x1b\[[0-9;]*[a-zA-Z]//g'
else
sed 's/\x1b\[[0-9;]*m//g; s/\x1b\[[0-9;]*[a-zA-Z]//g'
fi
}
# ------------------------------------------------------------------------------
# log_msg()
#
# - Writes message to active log file without ANSI codes
# - Adds timestamp prefix for log correlation
# - Creates log file if it doesn't exist
# - Arguments: message text (can include ANSI codes, will be stripped)
# ------------------------------------------------------------------------------
log_msg() {
local msg="$*"
local logfile
logfile="$(get_active_logfile)"
[[ -z "$msg" ]] && return
[[ -z "$logfile" ]] && return
# Ensure log directory exists
mkdir -p "$(dirname "$logfile")" 2>/dev/null || true
# Strip ANSI codes and write with timestamp
local clean_msg
clean_msg=$(strip_ansi "$msg")
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $clean_msg" >>"$logfile"
}
# ------------------------------------------------------------------------------
# log_section()
#
# - Writes a section header to the log file
# - Used for separating different phases of installation
# - Arguments: section name
# ------------------------------------------------------------------------------
log_section() {
local section="$1"
local logfile
logfile="$(get_active_logfile)"
[[ -z "$logfile" ]] && return
mkdir -p "$(dirname "$logfile")" 2>/dev/null || true
{
echo ""
echo "================================================================================"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $section"
echo "================================================================================"
} >>"$logfile"
}
# ------------------------------------------------------------------------------
# silent()
#
@@ -459,15 +522,9 @@ silent() {
msg_custom "→" "${YWB}" "${cmd}"
if [[ -s "$logfile" ]]; then
local log_lines=$(wc -l <"$logfile")
echo "--- Last 10 lines of silent log ---"
echo -e "\n${TAB}--- Last 10 lines of 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
echo -e "${TAB}-----------------------------------\n"
fi
exit "$rc"
@@ -488,7 +545,7 @@ spinner() {
local i=0
while true; do
local index=$((i++ % ${#chars[@]}))
printf "\r\033[2K%s %b" "${CS_YWB}${TAB}${chars[$index]}${TAB}${CS_CL}" "${CS_YWB}${msg}${CS_CL}"
printf "\r\033[2K%s %b" "${CS_YWB}${chars[$index]}${CS_CL}" "${CS_YWB}${msg}${CS_CL}"
sleep 0.1
done
}
@@ -555,6 +612,9 @@ msg_info() {
[[ -n "${MSG_INFO_SHOWN["$msg"]+x}" ]] && return
MSG_INFO_SHOWN["$msg"]=1
# Log to file
log_msg "[INFO] $msg"
stop_spinner
SPINNER_MSG="$msg"
@@ -598,7 +658,10 @@ msg_ok() {
stop_spinner
clear_line
echo -e "$CM ${GN}${msg}${CL}"
unset MSG_INFO_SHOWN["$msg"]
log_msg "[OK] $msg"
local sanitized_msg
sanitized_msg=$(printf '%s' "$msg" | sed 's/\x1b\[[0-9;]*m//g; s/[^a-zA-Z0-9_]/_/g')
unset 'MSG_INFO_SHOWN['"$sanitized_msg"']' 2>/dev/null || true
}
# ------------------------------------------------------------------------------
@@ -613,6 +676,7 @@ msg_error() {
stop_spinner
local msg="$1"
echo -e "${BFR:-}${CROSS:-✖️} ${RD}${msg}${CL}" >&2
log_msg "[ERROR] $msg"
}
# ------------------------------------------------------------------------------
@@ -627,6 +691,7 @@ msg_warn() {
stop_spinner
local msg="$1"
echo -e "${BFR:-}${INFO:-} ${YWB}${msg}${CL}" >&2
log_msg "[WARN] $msg"
}
# ------------------------------------------------------------------------------
@@ -644,6 +709,7 @@ msg_custom() {
[[ -z "$msg" ]] && return
stop_spinner
echo -e "${BFR:-} ${symbol} ${color}${msg}${CL:-\e[0m}"
log_msg "$msg"
}
# ------------------------------------------------------------------------------
@@ -808,6 +874,562 @@ is_verbose_mode() {
[[ "$verbose" != "no" || ! -t 2 ]]
}
# ------------------------------------------------------------------------------
# is_unattended()
#
# - Detects if script is running in unattended/non-interactive mode
# - Checks MODE variable first (primary method)
# - Falls back to legacy flags (PHS_SILENT, var_unattended)
# - Returns 0 (true) if unattended, 1 (false) otherwise
# - Used by prompt functions to auto-apply defaults
#
# Modes that are unattended:
# - default (1) : Use script defaults, no prompts
# - mydefaults (3) : Use user's default.vars, no prompts
# - appdefaults (4) : Use app-specific defaults, no prompts
#
# Modes that are interactive:
# - advanced (2) : Full wizard with all options
#
# Note: Even in advanced mode, install scripts run unattended because
# all values are already collected during the wizard phase.
# ------------------------------------------------------------------------------
is_unattended() {
# Primary: Check MODE variable (case-insensitive)
local mode="${MODE:-${mode:-}}"
mode="${mode,,}" # lowercase
case "$mode" in
default | 1)
return 0
;;
mydefaults | userdefaults | 3)
return 0
;;
appdefaults | 4)
return 0
;;
advanced | 2)
# Advanced mode is interactive ONLY during wizard
# Inside container (install scripts), it should be unattended
# Check if we're inside a container (no pveversion command)
if ! command -v pveversion &>/dev/null; then
# We're inside the container - all values already collected
return 0
fi
# On host during wizard - interactive
return 1
;;
esac
# Legacy fallbacks for compatibility
[[ "${PHS_SILENT:-0}" == "1" ]] && return 0
[[ "${var_unattended:-}" =~ ^(yes|true|1)$ ]] && return 0
[[ "${UNATTENDED:-}" =~ ^(yes|true|1)$ ]] && return 0
# No TTY available = unattended
[[ ! -t 0 ]] && return 0
# Default: interactive
return 1
}
# ------------------------------------------------------------------------------
# show_missing_values_warning()
#
# - Displays a summary of required values that used fallback defaults
# - Should be called at the end of install scripts
# - Only shows warning if MISSING_REQUIRED_VALUES array has entries
# - Provides clear guidance on what needs manual configuration
#
# Global:
# MISSING_REQUIRED_VALUES - Array of variable names that need configuration
#
# Example:
# # At end of install script:
# show_missing_values_warning
# ------------------------------------------------------------------------------
show_missing_values_warning() {
if [[ ${#MISSING_REQUIRED_VALUES[@]} -gt 0 ]]; then
echo ""
echo -e "${YW}╔════════════════════════════════════════════════════════════╗${CL}"
echo -e "${YW}║ ⚠️ MANUAL CONFIGURATION REQUIRED ║${CL}"
echo -e "${YW}╠════════════════════════════════════════════════════════════╣${CL}"
echo -e "${YW}║ The following values were not provided and need to be ║${CL}"
echo -e "${YW}║ configured manually for the service to work properly: ║${CL}"
echo -e "${YW}╟────────────────────────────────────────────────────────────╢${CL}"
for val in "${MISSING_REQUIRED_VALUES[@]}"; do
printf "${YW}${CL} • %-56s ${YW}${CL}\n" "$val"
done
echo -e "${YW}╟────────────────────────────────────────────────────────────╢${CL}"
echo -e "${YW}║ Check the service configuration files or environment ║${CL}"
echo -e "${YW}║ variables and update the placeholder values. ║${CL}"
echo -e "${YW}╚════════════════════════════════════════════════════════════╝${CL}"
echo ""
return 1
fi
return 0
}
# ------------------------------------------------------------------------------
# prompt_confirm()
#
# - Prompts user for yes/no confirmation with timeout and unattended support
# - In unattended mode: immediately returns default value
# - In interactive mode: waits for user input with configurable timeout
# - After timeout: auto-applies default value
#
# Arguments:
# $1 - Prompt message (required)
# $2 - Default value: "y" or "n" (optional, default: "n")
# $3 - Timeout in seconds (optional, default: 60)
#
# Returns:
# 0 - User confirmed (yes)
# 1 - User declined (no) or timeout with default "n"
#
# Example:
# if prompt_confirm "Proceed with installation?" "y" 30; then
# echo "Installing..."
# fi
#
# # Unattended: prompt_confirm will use default without waiting
# var_unattended=yes
# prompt_confirm "Delete files?" "n" && echo "Deleting" || echo "Skipped"
# ------------------------------------------------------------------------------
prompt_confirm() {
local message="${1:-Confirm?}"
local default="${2:-n}"
local timeout="${3:-60}"
local response
# Normalize default to lowercase
default="${default,,}"
[[ "$default" != "y" ]] && default="n"
# Build prompt hint
local hint
if [[ "$default" == "y" ]]; then
hint="[Y/n]"
else
hint="[y/N]"
fi
# Unattended mode: apply default immediately
if is_unattended; then
if [[ "$default" == "y" ]]; then
return 0
else
return 1
fi
fi
# Check if running in a TTY
if [[ ! -t 0 ]]; then
# Not a TTY, use default
if [[ "$default" == "y" ]]; then
return 0
else
return 1
fi
fi
# Interactive prompt with timeout
echo -en "${YW}${message} ${hint} (auto-${default} in ${timeout}s): ${CL}"
if read -t "$timeout" -r response; then
# User provided input
response="${response,,}" # lowercase
case "$response" in
y | yes)
return 0
;;
n | no)
return 1
;;
"")
# Empty response, use default
if [[ "$default" == "y" ]]; then
return 0
else
return 1
fi
;;
*)
# Invalid input, use default
echo -e "${YW}Invalid response, using default: ${default}${CL}"
if [[ "$default" == "y" ]]; then
return 0
else
return 1
fi
;;
esac
else
# Timeout occurred
echo "" # Newline after timeout
echo -e "${YW}Timeout - auto-selecting: ${default}${CL}"
if [[ "$default" == "y" ]]; then
return 0
else
return 1
fi
fi
}
# ------------------------------------------------------------------------------
# prompt_input()
#
# - Prompts user for text input with timeout and unattended support
# - In unattended mode: immediately returns default value
# - In interactive mode: waits for user input with configurable timeout
# - After timeout: auto-applies default value
#
# Arguments:
# $1 - Prompt message (required)
# $2 - Default value (optional, default: "")
# $3 - Timeout in seconds (optional, default: 60)
#
# Output:
# Prints the user input or default value to stdout
#
# Example:
# username=$(prompt_input "Enter username:" "admin" 30)
# echo "Using username: $username"
#
# # With validation
# while true; do
# port=$(prompt_input "Enter port:" "8080" 30)
# [[ "$port" =~ ^[0-9]+$ ]] && break
# echo "Invalid port number"
# done
# ------------------------------------------------------------------------------
prompt_input() {
local message="${1:-Enter value:}"
local default="${2:-}"
local timeout="${3:-60}"
local response
# Build display default hint
local hint=""
[[ -n "$default" ]] && hint=" (default: ${default})"
# Unattended mode: return default immediately
if is_unattended; then
echo "$default"
return 0
fi
# Check if running in a TTY
if [[ ! -t 0 ]]; then
# Not a TTY, use default
echo "$default"
return 0
fi
# Interactive prompt with timeout
echo -en "${YW}${message}${hint} (auto-default in ${timeout}s): ${CL}" >&2
if read -t "$timeout" -r response; then
# User provided input (or pressed Enter for empty)
if [[ -n "$response" ]]; then
echo "$response"
else
echo "$default"
fi
else
# Timeout occurred
echo "" >&2 # Newline after timeout
echo -e "${YW}Timeout - using default: ${default}${CL}" >&2
echo "$default"
fi
}
# ------------------------------------------------------------------------------
# prompt_input_required()
#
# - Prompts user for REQUIRED text input with fallback support
# - In unattended mode: Uses fallback value if no env var set (with warning)
# - In interactive mode: loops until user provides non-empty input
# - Tracks missing required values for end-of-script summary
#
# Arguments:
# $1 - Prompt message (required)
# $2 - Fallback/example value for unattended mode (optional)
# $3 - Timeout in seconds (optional, default: 120)
# $4 - Environment variable name hint for error messages (optional)
#
# Output:
# Prints the user input or fallback value to stdout
#
# Returns:
# 0 - Success (value provided or fallback used)
# 1 - Failed (interactive timeout without input)
#
# Global:
# MISSING_REQUIRED_VALUES - Array tracking fields that used fallbacks
#
# Example:
# # With fallback - script continues even in unattended mode
# token=$(prompt_input_required "Enter API Token:" "YOUR_TOKEN_HERE" 60 "var_api_token")
#
# # Check at end of script if any values need manual configuration
# if [[ ${#MISSING_REQUIRED_VALUES[@]} -gt 0 ]]; then
# msg_warn "Please configure: ${MISSING_REQUIRED_VALUES[*]}"
# fi
# ------------------------------------------------------------------------------
# Global array to track missing required values
declare -g -a MISSING_REQUIRED_VALUES=()
prompt_input_required() {
local message="${1:-Enter required value:}"
local fallback="${2:-CHANGE_ME}"
local timeout="${3:-120}"
local env_var_hint="${4:-}"
local response=""
# Check if value is already set via environment variable (if hint provided)
if [[ -n "$env_var_hint" ]]; then
local env_value="${!env_var_hint:-}"
if [[ -n "$env_value" ]]; then
echo "$env_value"
return 0
fi
fi
# Unattended mode: use fallback with warning
if is_unattended; then
if [[ -n "$env_var_hint" ]]; then
echo -e "${YW}⚠ Required value '${env_var_hint}' not set - using fallback: ${fallback}${CL}" >&2
MISSING_REQUIRED_VALUES+=("$env_var_hint")
else
echo -e "${YW}⚠ Required value not provided - using fallback: ${fallback}${CL}" >&2
MISSING_REQUIRED_VALUES+=("(unnamed)")
fi
echo "$fallback"
return 0
fi
# Check if running in a TTY
if [[ ! -t 0 ]]; then
echo -e "${YW}⚠ Not interactive - using fallback: ${fallback}${CL}" >&2
MISSING_REQUIRED_VALUES+=("${env_var_hint:-unnamed}")
echo "$fallback"
return 0
fi
# Interactive prompt - loop until non-empty input or use fallback on timeout
local attempts=0
while [[ -z "$response" ]]; do
attempts=$((attempts + 1))
if [[ $attempts -gt 3 ]]; then
echo -e "${YW}Too many empty inputs - using fallback: ${fallback}${CL}" >&2
MISSING_REQUIRED_VALUES+=("${env_var_hint:-manual_input}")
echo "$fallback"
return 0
fi
echo -en "${YW}${message} (required, timeout ${timeout}s): ${CL}" >&2
if read -t "$timeout" -r response; then
if [[ -z "$response" ]]; then
echo -e "${YW}This field is required. Please enter a value. (attempt ${attempts}/3)${CL}" >&2
fi
else
# Timeout occurred - use fallback
echo "" >&2
echo -e "${YW}Timeout - using fallback value: ${fallback}${CL}" >&2
MISSING_REQUIRED_VALUES+=("${env_var_hint:-timeout}")
echo "$fallback"
return 0
fi
done
echo "$response"
}
# ------------------------------------------------------------------------------
# prompt_select()
#
# - Prompts user to select from a list of options with timeout support
# - In unattended mode: immediately returns default selection
# - In interactive mode: displays numbered menu and waits for choice
# - After timeout: auto-applies default selection
#
# Arguments:
# $1 - Prompt message (required)
# $2 - Default option number, 1-based (optional, default: 1)
# $3 - Timeout in seconds (optional, default: 60)
# $4+ - Options to display (required, at least 2)
#
# Output:
# Prints the selected option value to stdout
#
# Returns:
# 0 - Success
# 1 - No options provided or invalid state
#
# Example:
# choice=$(prompt_select "Select database:" 1 30 "PostgreSQL" "MySQL" "SQLite")
# echo "Selected: $choice"
#
# # With array
# options=("Option A" "Option B" "Option C")
# selected=$(prompt_select "Choose:" 2 60 "${options[@]}")
# ------------------------------------------------------------------------------
prompt_select() {
local message="${1:-Select option:}"
local default="${2:-1}"
local timeout="${3:-60}"
shift 3
local options=("$@")
local num_options=${#options[@]}
# Validate options
if [[ $num_options -eq 0 ]]; then
echo "" >&2
return 1
fi
# Validate default
if [[ ! "$default" =~ ^[0-9]+$ ]] || [[ "$default" -lt 1 ]] || [[ "$default" -gt "$num_options" ]]; then
default=1
fi
# Unattended mode: return default immediately
if is_unattended; then
echo "${options[$((default - 1))]}"
return 0
fi
# Check if running in a TTY
if [[ ! -t 0 ]]; then
echo "${options[$((default - 1))]}"
return 0
fi
# Display menu
echo -e "${YW}${message}${CL}" >&2
local i
for i in "${!options[@]}"; do
local num=$((i + 1))
if [[ $num -eq $default ]]; then
echo -e " ${GN}${num})${CL} ${options[$i]} ${YW}(default)${CL}" >&2
else
echo -e " ${GN}${num})${CL} ${options[$i]}" >&2
fi
done
# Interactive prompt with timeout
echo -en "${YW}Select [1-${num_options}] (auto-select ${default} in ${timeout}s): ${CL}" >&2
local response
if read -t "$timeout" -r response; then
if [[ -z "$response" ]]; then
# Empty response, use default
echo "${options[$((default - 1))]}"
elif [[ "$response" =~ ^[0-9]+$ ]] && [[ "$response" -ge 1 ]] && [[ "$response" -le "$num_options" ]]; then
# Valid selection
echo "${options[$((response - 1))]}"
else
# Invalid input, use default
echo -e "${YW}Invalid selection, using default: ${options[$((default - 1))]}${CL}" >&2
echo "${options[$((default - 1))]}"
fi
else
# Timeout occurred
echo "" >&2 # Newline after timeout
echo -e "${YW}Timeout - auto-selecting: ${options[$((default - 1))]}${CL}" >&2
echo "${options[$((default - 1))]}"
fi
}
# ------------------------------------------------------------------------------
# prompt_password()
#
# - Prompts user for password input with hidden characters
# - In unattended mode: returns default or generates random password
# - Supports auto-generation of secure passwords
# - After timeout: generates random password if allowed
#
# Arguments:
# $1 - Prompt message (required)
# $2 - Default value or "generate" for auto-generation (optional)
# $3 - Timeout in seconds (optional, default: 60)
# $4 - Minimum length for validation (optional, default: 0 = no minimum)
#
# Output:
# Prints the password to stdout
#
# Example:
# password=$(prompt_password "Enter password:" "generate" 30 8)
# echo "Password set"
#
# # Require user input (no default)
# db_pass=$(prompt_password "Database password:" "" 60 12)
# ------------------------------------------------------------------------------
prompt_password() {
local message="${1:-Enter password:}"
local default="${2:-}"
local timeout="${3:-60}"
local min_length="${4:-0}"
local response
# Generate random password if requested
local generated=""
if [[ "$default" == "generate" ]]; then
generated=$(openssl rand -base64 16 2>/dev/null | tr -dc 'a-zA-Z0-9' | head -c 16)
[[ -z "$generated" ]] && generated=$(head /dev/urandom | tr -dc 'a-zA-Z0-9' | head -c 16)
default="$generated"
fi
# Unattended mode: return default immediately
if is_unattended; then
echo "$default"
return 0
fi
# Check if running in a TTY
if [[ ! -t 0 ]]; then
echo "$default"
return 0
fi
# Build hint
local hint=""
if [[ -n "$generated" ]]; then
hint=" (Enter for auto-generated)"
elif [[ -n "$default" ]]; then
hint=" (Enter for default)"
fi
[[ "$min_length" -gt 0 ]] && hint="${hint} [min ${min_length} chars]"
# Interactive prompt with timeout (silent input)
echo -en "${YW}${message}${hint} (timeout ${timeout}s): ${CL}" >&2
if read -t "$timeout" -rs response; then
echo "" >&2 # Newline after hidden input
if [[ -n "$response" ]]; then
# Validate minimum length
if [[ "$min_length" -gt 0 ]] && [[ ${#response} -lt "$min_length" ]]; then
echo -e "${YW}Password too short (min ${min_length}), using default${CL}" >&2
echo "$default"
else
echo "$response"
fi
else
echo "$default"
fi
else
# Timeout occurred
echo "" >&2 # Newline after timeout
echo -e "${YW}Timeout - using generated password${CL}" >&2
echo "$default"
fi
}
# ==============================================================================
# SECTION 6: CLEANUP & MAINTENANCE
# ==============================================================================
@@ -896,15 +1518,13 @@ check_or_create_swap() {
msg_error "No active swap detected"
read -p "Do you want to create a swap file? [y/N]: " create_swap
create_swap="${create_swap,,}" # to lowercase
if [[ "$create_swap" != "y" && "$create_swap" != "yes" ]]; then
if ! prompt_confirm "Do you want to create a swap file?" "n" 60; then
msg_info "Skipping swap file creation"
return 1
fi
read -p "Enter swap size in MB (e.g., 2048 for 2GB): " swap_size_mb
local swap_size_mb
swap_size_mb=$(prompt_input "Enter swap size in MB (e.g., 2048 for 2GB):" "2048" 60)
if ! [[ "$swap_size_mb" =~ ^[0-9]+$ ]]; then
msg_error "Invalid size input. Aborting."
return 1

View File

@@ -175,9 +175,9 @@ error_handler() {
fi
if [[ -n "$active_log" && -s "$active_log" ]]; then
echo "--- Last 20 lines of silent log ---"
echo -e "\n${TAB}--- Last 20 lines of log ---"
tail -n 20 "$active_log"
echo "-----------------------------------"
echo -e "${TAB}-----------------------------------\n"
# Detect context: Container (INSTALL_LOG set + /root exists) vs Host (BUILD_LOG)
if [[ -n "${INSTALL_LOG:-}" && -d /root ]]; then
@@ -204,23 +204,50 @@ error_handler() {
fi
echo ""
echo -en "${YW}Remove broken container ${CTID}? (Y/n) [auto-remove in 60s]: ${CL}"
if declare -f msg_custom >/dev/null 2>&1; then
echo -en "${TAB}${TAB}${YW}Remove broken container ${CTID}? (Y/n) [auto-remove in 60s]: ${CL}"
else
echo -en "${YW}Remove broken container ${CTID}? (Y/n) [auto-remove in 60s]: ${CL}"
fi
if read -t 60 -r response; then
if [[ -z "$response" || "$response" =~ ^[Yy]$ ]]; then
echo -e "\n${YW}Removing container ${CTID}${CL}"
echo ""
if declare -f msg_info >/dev/null 2>&1; then
msg_info "Removing container ${CTID}"
else
echo -e "${YW}Removing container ${CTID}${CL}"
fi
pct stop "$CTID" &>/dev/null || true
pct destroy "$CTID" &>/dev/null || true
echo -e "${GN}${CL} Container ${CTID} removed"
if declare -f msg_ok >/dev/null 2>&1; then
msg_ok "Container ${CTID} removed"
else
echo -e "${GN}${CL} Container ${CTID} removed"
fi
elif [[ "$response" =~ ^[Nn]$ ]]; then
echo -e "\n${YW}Container ${CTID} kept for debugging${CL}"
echo ""
if declare -f msg_warn >/dev/null 2>&1; then
msg_warn "Container ${CTID} kept for debugging"
else
echo -e "${YW}Container ${CTID} kept for debugging${CL}"
fi
fi
else
# Timeout - auto-remove
echo -e "\n${YW}No response - auto-removing container${CL}"
echo ""
if declare -f msg_info >/dev/null 2>&1; then
msg_info "No response - removing container ${CTID}"
else
echo -e "${YW}No response - removing container ${CTID}${CL}"
fi
pct stop "$CTID" &>/dev/null || true
pct destroy "$CTID" &>/dev/null || true
echo -e "${GN}${CL} Container ${CTID} removed"
if declare -f msg_ok >/dev/null 2>&1; then
msg_ok "Container ${CTID} removed"
else
echo -e "${GN}${CL} Container ${CTID} removed"
fi
fi
# Force one final status update attempt after cleanup
@@ -254,6 +281,10 @@ on_exit() {
# post_to_api was called ("installing" sent) but post_update_to_api was never called
if [[ "${POST_TO_API_DONE:-}" == "true" && "${POST_UPDATE_DONE:-}" != "true" ]]; then
if declare -f post_update_to_api >/dev/null 2>&1; then
# Ensure log is accessible on host before reporting
if declare -f ensure_log_on_host >/dev/null 2>&1; then
ensure_log_on_host
fi
if [[ $exit_code -ne 0 ]]; then
post_update_to_api "failed" "$exit_code"
else
@@ -273,6 +304,10 @@ on_exit() {
# - Exits with code 130 (128 + SIGINT=2)
# ------------------------------------------------------------------------------
on_interrupt() {
# Ensure log is accessible on host before reporting
if declare -f ensure_log_on_host >/dev/null 2>&1; then
ensure_log_on_host
fi
# Report interruption to telemetry API (prevents stuck "installing" records)
if declare -f post_update_to_api >/dev/null 2>&1; then
post_update_to_api "failed" "130"
@@ -294,6 +329,10 @@ on_interrupt() {
# - Triggered by external process termination
# ------------------------------------------------------------------------------
on_terminate() {
# Ensure log is accessible on host before reporting
if declare -f ensure_log_on_host >/dev/null 2>&1; then
ensure_log_on_host
fi
# Report termination to telemetry API (prevents stuck "installing" records)
if declare -f post_update_to_api >/dev/null 2>&1; then
post_update_to_api "failed" "143"

View File

@@ -207,15 +207,9 @@ silent() {
msg_custom "→" "${YWB}" "${cmd}"
if [[ -s "$logfile" ]]; then
local log_lines=$(wc -l <"$logfile")
echo "--- Last 10 lines of log ---"
echo -e "\n${TAB}--- Last 10 lines of 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
echo -e "${TAB}----------------------------\n"
fi
exit "$rc"