2026-02-06 08:54:21 +01:00
#!/usr/bin/env bash
# Copyright (c) 2021-2026 community-scripts ORG
# Author: BvdBerg01 | Co-Author: remz1337
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
2026-08-25 11:04:15 +02:00
_CS_CORE_URL = " ${ COMMUNITY_SCRIPTS_CORE_URL :- https ://raw.githubusercontent.com/community-scripts/core/main } "
source <( curl -fsSL " ${ _CS_CORE_URL } /core/core.func" )
source <( curl -fsSL " ${ _CS_CORE_URL } /api/api.func" ) 2>/dev/null || true
2026-02-18 10:24:06 +01:00
declare -f init_tool_telemetry & >/dev/null && init_tool_telemetry "update-apps" "pve"
2026-02-06 08:54:21 +01:00
# =============================================================================
# CONFIGURATION VARIABLES
# Set these variables to skip interactive prompts (Whiptail dialogs)
# =============================================================================
# var_backup: Enable/disable backup before update
# Options: "yes" | "no" | "" (empty = interactive prompt)
var_backup = " ${ var_backup :- } "
# var_backup_storage: Storage location for backups (only used if var_backup=yes)
# Options: Storage name from /etc/pve/storage.cfg (e.g., "local", "nas-backup")
# Leave empty for interactive selection
var_backup_storage = " ${ var_backup_storage :- } "
# var_container: Which containers to update
# Options:
# - "all" : All containers with community-scripts tags
# - "all_running" : Only running containers with community-scripts tags
# - "all_stopped" : Only stopped containers with community-scripts tags
# - "101,102,109" : Comma-separated list of specific container IDs
# - "" : Interactive selection via Whiptail
var_container = " ${ var_container :- } "
# var_unattended: Run updates without user interaction inside containers
# Options: "yes" | "no" | "" (empty = interactive prompt)
var_unattended = " ${ var_unattended :- } "
# var_skip_confirm: Skip initial confirmation dialog
# Options: "yes" | "no" (default: no)
var_skip_confirm = " ${ var_skip_confirm :- no } "
# var_auto_reboot: Automatically reboot containers that require it after update
# Options: "yes" | "no" | "" (empty = interactive prompt)
var_auto_reboot = " ${ var_auto_reboot :- } "
2026-05-07 15:53:22 +02:00
# var_continue_on_error: Continue updating remaining containers if one update fails
# Options: "yes" | "no" (default: no = stop on first error)
# Note: containers with backups always attempt restore on failure regardless of this setting
var_continue_on_error = " ${ var_continue_on_error :- no } "
# var_dry_run: Check for available updates without applying them
# Options: "yes" | "no" (default: no)
# Output: lists each container with current vs. latest version
# Note: requires the container to be running; does not modify any container
var_dry_run = " ${ var_dry_run :- no } "
2026-02-06 08:54:21 +01:00
# var_tags: Optionally override the tags used for auto-detection
# Options: "community-script|proxmox-helper-scripts" (default)
var_tags = " ${ var_tags :- community -script|proxmox-helper-scripts } "
# =============================================================================
# JSON CONFIG EXPORT
# Run with --export-config to output current configuration as JSON
# =============================================================================
function export_config_json() {
cat <<EOF
{
"var_backup": "${var_backup}",
"var_backup_storage": "${var_backup_storage}",
"var_container": "${var_container}",
"var_unattended": "${var_unattended}",
"var_skip_confirm": "${var_skip_confirm}",
"var_auto_reboot": "${var_auto_reboot}",
2026-05-07 15:53:22 +02:00
"var_continue_on_error": "${var_continue_on_error}",
"var_dry_run": "${var_dry_run}",
2026-02-06 08:54:21 +01:00
"var_tags": "${var_tags}"
}
EOF
}
function print_usage() {
cat <<EOF
Usage: $(basename "$0") [OPTIONS]
Update LXC containers created with community-scripts.
Options:
--help Show this help message
--export-config Export current configuration as JSON
Environment Variables:
var_backup Enable backup before update (yes/no)
var_backup_storage Storage location for backups
var_container Container selection (all/all_running/all_stopped/101,102,...)
2026-05-07 15:53:22 +02:00
var_unattended Run updates unattended (yes/no)
var_skip_confirm Skip initial confirmation (yes/no)
var_auto_reboot Auto-reboot containers if required (yes/no)
var_continue_on_error Continue to next container on update failure (yes/no)
var_dry_run Check for updates without applying them (yes/no)
var_tags Optionally override auto-detection tags ("prod|smb|community-script")
2026-02-06 08:54:21 +01:00
Examples:
# Run interactively
$(basename "$0")
# Update all running containers unattended with backup
var_backup=yes var_backup_storage=local var_container=all_running var_unattended=yes var_skip_confirm=yes $(basename "$0")
# Update specific containers without backup
var_backup=no var_container=101,102,105 var_unattended=yes var_skip_confirm=yes $(basename "$0")
2026-05-07 15:53:22 +02:00
# Unattended cron-style: skip confirm, continue on error, no backup
var_backup=no var_container=all_running var_unattended=yes var_skip_confirm=yes var_continue_on_error=yes $(basename "$0")
# Dry-run: show available updates for all running containers without applying
var_container=all_running var_skip_confirm=yes var_dry_run=yes $(basename "$0")
2026-02-06 08:54:21 +01:00
# Export current configuration
$(basename "$0") --export-config
EOF
}
# Handle command line arguments
case " ${ 1 :- } " in
2026-02-17 16:36:20 +01:00
--help | -h)
print_usage
exit 0
;;
--export-config)
export_config_json
exit 0
;;
2026-02-06 08:54:21 +01:00
esac
# =============================================================================
function header_info {
clear
cat <<"EOF"
__ _ ________ __ __ __ __
/ / | | / / ____/ / / / /___ ____/ /___ _/ /____
/ / | / / / / / / __ \/ __ / __ ` / __/ _ \
/ /___/ / /___ / /_/ / /_/ / /_/ / /_/ / /_/ __/
/_____/_/| _\_ ___/ \_ ___/ .___/\_ _,_/\_ _,_/\_ _/\_ __/
/_/
EOF
}
2026-06-22 21:59:43 +02:00
function sanitize_service_name() {
local name = " ${ 1 // $'\r' / } "
name = " ${ name // $'\n' / } "
[[ -z " $name " ]] && return 1
[[ " $name " == *'#!' * ]] && return 1
[[ ! " $name " = ~ ^[ a-zA-Z0-9._-] +$ ]] && return 1
return 0
}
2026-09-05 05:04:27 +02:00
function script_exists() {
2026-06-22 21:59:43 +02:00
local name = " $1 "
sanitize_service_name " $name " || return 1
curl -fsSL --max-time 10 -o /dev/null \
"https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/ ${ name } .sh" 2>/dev/null
}
2026-09-05 05:04:27 +02:00
# A container keeps the slug it was built with, so a renamed ct/ script leaves it
# pointing at a name that no longer exists. Try the successors, but only accept one
# that is really there -- guessing wrong would run a foreign app's updater.
function resolve_service_script() {
local n = " $1 " c
script_exists " $n " && { printf '%s' " $n " ; return 0; }
for c in " ${ n #alpine- } " " $( printf '%s' " $n " | sed -E 's/-v[0-9]+$//' ) " ; do
[[ -n " $c " && " $c " != " $n " ]] || continue
script_exists " $c " && { printf '%s' " $c " ; return 0; }
done
case " $n " in
pbs) script_exists proxmox-backup-server && { printf '%s' proxmox-backup-server; return 0; } ;;
esac
return 1
}
2026-09-11 07:40:57 +02:00
# The retired Gitea mirror. The old entrypoint pulls ct/<app>.sh straight from
# it and never reaches the update helper that would repair itself, so rewrite it
# here. Only host and /raw/<kind>/ change; owner, repo and ref are kept.
function repair_update_url() {
local container = " $1 "
pct exec " $container " -- sh -c '
[ -f /usr/bin/update ] || exit 1
grep -q git.community-scripts.org /usr/bin/update || exit 1
sed -i \
-e "s|https://git[.]community-scripts[.]org/|https://raw.githubusercontent.com/|g" \
-e "s|/raw/branch/|/|g" -e "s|/raw/tag/|/|g" -e "s|/raw/commit/|/|g" \
/usr/bin/update
' >/dev/null 2>& 1 || return 1
echo -e " ${ BL } [INFO] ${ CL } Repaired update URL (Gitea -> GitHub) in container $container "
log_write "Container $container : rewrote the retired Gitea base in /usr/bin/update"
}
2026-02-06 08:54:21 +01:00
function detect_service() {
2026-06-22 21:59:43 +02:00
local container = " $1 "
local tmpdir update_file
service = ""
tmpdir = $( mktemp -d)
update_file = " $tmpdir /update"
pct pull " $container " /usr/bin/update " $update_file " 2>/dev/null || true
if [[ ! -s " $update_file " ]] ; then
rm -rf " $tmpdir "
return 1
fi
2026-08-27 10:05:08 +02:00
service = $( sed -n -E 's/^[[:space:]]*export[[:space:]]+UPDATE_SCRIPT_NAME=["' "'" ']?([a-zA-Z0-9._-]+).*/\1/p' " $update_file " | head -n1)
[[ -z " $service " ]] && service = $( sed -n -E 's/^[[:space:]]*export[[:space:]]+SCRIPT_SLUG=["' "'" ']?([a-zA-Z0-9._-]+).*/\1/p' " $update_file " | head -n1)
[[ -z " $service " ]] && service = $( grep -oE '/ct/[a-zA-Z0-9._-]+\.sh' " $update_file " 2>/dev/null | head -n1 | sed 's|.*/ct/||; s|\.sh$||' )
2026-06-22 21:59:43 +02:00
rm -rf " $tmpdir "
2026-02-06 08:54:21 +01:00
}
2026-05-07 15:53:22 +02:00
function dry_run_container() {
local container = " $1 "
local service = " $2 "
# Extract app name and source repo directly from check_for_gh_release call in the ct script
# Pattern: check_for_gh_release "appname" "owner/repo"
local check_line app_name app_lc source_repo
check_line = $( echo " $script " | grep -m1 'check_for_gh_release' )
if [[ -z " $check_line " ]] ; then
echo -e " ${ YW } [DRY-RUN] ${ CL } Container $container ( $service ): no check_for_gh_release found — skipping"
DRY_RUN_RESULT = "no check_for_gh_release found — skipping"
return
fi
app_name = $( echo " $check_line " | cut -d'"' -f2)
source_repo = $( echo " $check_line " | cut -d'"' -f4)
app_lc = $( echo " ${ app_name ,, } " | tr -d ' ' )
if [[ -z " $source_repo " || " $source_repo " != *"/" * ]] ; then
echo -e " ${ YW } [DRY-RUN] ${ CL } Container $container ( $service ): cannot parse source repo — skipping"
DRY_RUN_RESULT = "cannot parse source repo — skipping"
return
fi
# Read installed version from container (stored by check_for_gh_release as ~/.<appname>)
local current_version
current_version = $( pct exec " $container " -- bash -c "cat \$HOME/. ${ app_lc } 2>/dev/null" 2>/dev/null || true )
current_version = " ${ current_version #v } "
# Query latest release from GitHub API
local latest_version
latest_version = $( curl -sSL --max-time 10 \
-H 'Accept: application/vnd.github+json' \
-H 'X-GitHub-Api-Version: 2022-11-28' \
"https://api.github.com/repos/ ${ source_repo } /releases/latest" 2>/dev/null |
grep '"tag_name"' | head -1 | cut -d'"' -f4 | sed 's/^v//' )
if [[ -z " $latest_version " ]] ; then
echo -e " ${ YW } [DRY-RUN] ${ CL } Container $container ( $service ): cannot fetch latest version from $source_repo "
DRY_RUN_RESULT = "cannot fetch latest version from $source_repo "
return
fi
if [[ -z " $current_version " ]] ; then
echo -e " ${ BL } [DRY-RUN] ${ CL } Container $container ( $service ): installed version unknown, latest: ${ latest_version } ( ${ source_repo } )"
DRY_RUN_RESULT = "version unknown — latest: ${ latest_version } "
elif [[ " $current_version " == " $latest_version " ]] ; then
echo -e " ${ GN } [DRY-RUN] ${ CL } Container $container ( $service ): up to date ( ${ current_version } )"
DRY_RUN_RESULT = "up to date ( ${ current_version } )"
else
echo -e " ${ YW } [DRY-RUN] ${ CL } Container $container ( $service ): update available ${ current_version } → ${ latest_version } "
DRY_RUN_RESULT = "update available ${ current_version } → ${ latest_version } "
fi
}
2026-02-06 08:54:21 +01:00
function backup_container() {
msg_info "Creating backup for container $1 "
2026-02-10 19:30:54 +01:00
vzdump $1 --compress zstd --storage $STORAGE_CHOICE -notes-template "{{guestname}} - community-scripts backup updater" >/dev/null 2>& 1
2026-02-06 08:54:21 +01:00
status = $?
if [ $status -eq 0 ] ; then
msg_ok "Backup created"
else
msg_error "Backup failed for container $1 "
2026-03-02 10:55:20 +01:00
exit 235
2026-02-06 08:54:21 +01:00
fi
}
function get_backup_storages() {
STORAGES = $( awk '
/^[a-z]+:/ {
if (name != "") {
if (has_backup || (!has_content && type == "dir")) print name
}
split($0, a, ":")
type = a[1]
name = a[2]
2026-02-10 19:30:54 +01:00
gsub(/^[ \t]+|[ \t]+$/, "", name)
2026-02-06 08:54:21 +01:00
has_content = 0
has_backup = 0
}
2026-02-10 19:30:54 +01:00
/^[ \t]*content/ {
2026-02-06 08:54:21 +01:00
has_content = 1
if ($0 ~ /backup/) has_backup = 1
}
END {
if (name != "") {
if (has_backup || (!has_content && type == "dir")) print name
}
}
' /etc/pve/storage.cfg)
}
2026-05-07 15:53:22 +02:00
# Structured result tracking for the final summary report
# Each entry: "CTID|service|STATUS|details"
declare -a UPDATE_RESULTS =()
function log_result() {
# log_result <ctid> <service> <STATUS> <details>
UPDATE_RESULTS +=( " ${ 1 } | ${ 2 } | ${ 3 } | ${ 4 } " )
}
2026-02-06 08:54:21 +01:00
header_info
2026-05-07 15:53:22 +02:00
# =============================================================================
# LOGGING SETUP
# Key events are written directly to a timestamped log file under
# /usr/local/community-scripts/update_apps/ — this avoids any stdout
# redirection that would break interactive spinners or whiptail dialogs.
# The full summary table is appended at the end of the run.
# =============================================================================
LOG_DIR = "/usr/local/community-scripts/update_apps"
mkdir -p " $LOG_DIR "
LOG_FILE = " ${ LOG_DIR } / $( date '+%Y%m%d_%H%M%S' ) .log"
echo "Update started: $( date '+%Y-%m-%d %H:%M:%S' ) " >" $LOG_FILE "
function log_write() {
echo "[ $( date '+%H:%M:%S' ) ] $* " >>" $LOG_FILE "
}
2026-02-06 08:54:21 +01:00
# Skip confirmation if var_skip_confirm is set to yes
if [[ " $var_skip_confirm " != "yes" ]] ; then
whiptail --backtitle "Proxmox VE Helper Scripts" --title "LXC App Update" --yesno "This will update apps in LXCs installed by Helper-Scripts. Proceed?" 10 58 || exit
fi
tags_formatted = " ${ var_tags //|/, } "
msg_info "Loading all possible LXC containers from Proxmox VE with tags: ${ tags_formatted } . This may take a few seconds..."
NODE = $( hostname)
containers = $( pct list | tail -n +2 | awk '{print $0 " " $4}' )
if [ -z " $containers " ] ; then
whiptail --title "LXC Container Update" --msgbox "No LXC containers available!" 10 60
2026-03-02 10:55:20 +01:00
exit 234
2026-02-06 08:54:21 +01:00
fi
menu_items =()
FORMAT = "%-10s %-15s %-10s"
TAGS = " ${ var_tags :- community -script|proxmox-helper-scripts } "
while read -r container; do
container_id = $( echo $container | awk '{print $1}' )
container_name = $( echo $container | awk '{print $2}' )
container_status = $( echo $container | awk '{print $3}' )
formatted_line = $( printf " $FORMAT " " $container_name " " $container_status " )
if pct config " $container_id " | grep -qE "[^-][; ]( ${ TAGS } ).*" ; then
menu_items +=( " $container_id " " $formatted_line " "OFF" )
fi
done <<< " $containers "
2026-05-05 16:05:31 -03:00
msg_ok "Loaded $(( ${# menu_items [@] } / 3 )) containers"
2026-02-06 08:54:21 +01:00
# Determine container selection based on var_container
if [[ -n " $var_container " ]] ; then
case " $var_container " in
2026-02-17 16:36:20 +01:00
all)
# Select all containers with matching tags
CHOICE = ""
for (( i = 0; i < ${# menu_items [@] } ; i += 3)) ; do
CHOICE = " $CHOICE ${ menu_items [ $i ] } "
done
CHOICE = $( echo " $CHOICE " | xargs)
;;
all_running)
# Select only running containers with matching tags
CHOICE = ""
for (( i = 0; i < ${# menu_items [@] } ; i += 3)) ; do
cid = " ${ menu_items [ $i ] } "
if pct status " $cid " 2>/dev/null | grep -q "running" ; then
CHOICE = " $CHOICE $cid "
fi
done
CHOICE = $( echo " $CHOICE " | xargs)
;;
all_stopped)
# Select only stopped containers with matching tags
CHOICE = ""
for (( i = 0; i < ${# menu_items [@] } ; i += 3)) ; do
cid = " ${ menu_items [ $i ] } "
if pct status " $cid " 2>/dev/null | grep -q "stopped" ; then
CHOICE = " $CHOICE $cid "
fi
done
CHOICE = $( echo " $CHOICE " | xargs)
;;
*)
# Assume comma-separated list of container IDs
CHOICE = $( echo " $var_container " | tr ',' ' ' )
;;
2026-02-06 08:54:21 +01:00
esac
if [[ -z " $CHOICE " ]] ; then
msg_error "No containers matched the selection criteria: $var_container ${ var_tags :- community -script|proxmox-helper-scripts } "
2026-03-02 10:55:20 +01:00
exit 234
2026-02-06 08:54:21 +01:00
fi
msg_ok "Selected containers: $CHOICE "
else
CHOICE = $( whiptail --title "LXC Container Update" \
--checklist "Select LXC containers to update:" 25 60 13 \
" ${ menu_items [@] } " 3>& 2 2>& 1 1>& 3 | tr -d '"' )
if [ -z " $CHOICE " ] ; then
whiptail --title "LXC Container Update" \
--msgbox "No containers selected!" 10 60
2026-03-02 10:55:20 +01:00
exit 0
2026-02-06 08:54:21 +01:00
fi
fi
header_info
# Determine backup choice based on var_backup
2026-05-07 15:53:22 +02:00
# Dry-run never needs a backup — skip the prompt entirely
if [[ " $var_dry_run " == "yes" ]] ; then
BACKUP_CHOICE = "no"
elif [[ -n " $var_backup " ]] ; then
2026-02-06 08:54:21 +01:00
BACKUP_CHOICE = " $var_backup "
else
BACKUP_CHOICE = "no"
if ( whiptail --backtitle "Proxmox VE Helper Scripts" --title "LXC Container Update" --yesno "Do you want to backup your containers before update?" 10 58) ; then
BACKUP_CHOICE = "yes"
fi
fi
# Determine unattended update based on var_unattended
2026-05-07 15:53:22 +02:00
# Dry-run never executes updates — skip the prompt entirely
if [[ " $var_dry_run " == "yes" ]] ; then
UNATTENDED_UPDATE = "no"
elif [[ -n " $var_unattended " ]] ; then
2026-02-06 08:54:21 +01:00
UNATTENDED_UPDATE = " $var_unattended "
else
UNATTENDED_UPDATE = "no"
if ( whiptail --backtitle "Proxmox VE Helper Scripts" --title "LXC Container Update" --yesno "Run updates unattended?" 10 58) ; then
UNATTENDED_UPDATE = "yes"
fi
fi
if [ " $BACKUP_CHOICE " == "yes" ] ; then
get_backup_storages
if [ -z " $STORAGES " ] ; then
msg_error "No storage with 'backup' support found!"
2026-03-02 10:55:20 +01:00
exit 119
2026-02-06 08:54:21 +01:00
fi
# Determine storage based on var_backup_storage
if [[ -n " $var_backup_storage " ]] ; then
# Validate that the specified storage exists and supports backups
if echo " $STORAGES " | grep -qw " $var_backup_storage " ; then
STORAGE_CHOICE = " $var_backup_storage "
msg_ok "Using backup storage: $STORAGE_CHOICE "
else
msg_error "Specified backup storage ' $var_backup_storage ' not found or doesn't support backups!"
msg_info "Available storages: $( echo $STORAGES | tr '\n' ' ' ) "
2026-03-02 10:55:20 +01:00
exit 119
2026-02-06 08:54:21 +01:00
fi
else
MENU_ITEMS =()
for STORAGE in $STORAGES ; do
MENU_ITEMS +=( " $STORAGE " "" )
done
STORAGE_CHOICE = $( whiptail --title "Select storage device" --menu "Select a storage device (Only storage devices with 'backup' support are listed):" 15 50 5 " ${ MENU_ITEMS [@] } " 3>& 1 1>& 2 2>& 3)
if [ -z " $STORAGE_CHOICE " ] ; then
msg_error "No storage selected!"
2026-03-02 10:55:20 +01:00
exit 0
2026-02-06 08:54:21 +01:00
fi
fi
fi
UPDATE_CMD = "update;"
if [ " $UNATTENDED_UPDATE " == "yes" ] ; then
UPDATE_CMD = "export PHS_SILENT=1;update;"
fi
containers_needing_reboot =()
for container in $CHOICE ; do
echo -e " ${ BL } [INFO] ${ CL } Updating container $container "
2026-05-07 15:53:22 +02:00
log_write "Container $container : starting"
2026-02-06 08:54:21 +01:00
if [ " $BACKUP_CHOICE " == "yes" ] ; then
backup_container $container
fi
os = $( pct config " $container " | awk '/^ostype/ {print $2}' )
status = $( pct status $container )
template = $( pct config $container | grep -q "template:" && echo "true" || echo "false" )
if [ " $template " == "false" ] && [ " $status " == "status: stopped" ] ; then
echo -e " ${ BL } [Info] ${ GN } Starting ${ BL } $container ${ CL } \n"
pct start $container
echo -e " ${ BL } [Info] ${ GN } Waiting For ${ BL } $container ${ CL }${ GN } To Start ${ CL } \n"
sleep 5
fi
2026-09-11 07:40:57 +02:00
#0.5) Rewrite a retired Gitea base before anything reads it.
repair_update_url " $container "
2026-02-06 08:54:21 +01:00
#1) Detect service using the service name in the update command
detect_service $container
2026-06-22 21:59:43 +02:00
#1.1) If update script not detected or service name is invalid, skip
if [ -z " ${ service } " ] || ! sanitize_service_name " ${ service } " ; then
echo -e " ${ RD } [ERROR] ${ CL } Could not detect a valid service name for container $container "
log_result " $container " "(unknown)" "ERROR" "Invalid or missing service name in /usr/bin/update"
log_write "Container $container : ERROR — invalid or missing service name"
continue
fi
2026-09-05 05:04:27 +02:00
resolved_service = " $( resolve_service_script " ${ service } " ) "
if [ -z " ${ resolved_service } " ] ; then
2026-06-22 21:59:43 +02:00
echo -e " ${ RD } [ERROR] ${ CL } Service ' ${ service } ' does not resolve to ct/ ${ service } .sh"
log_result " $container " " ${ service } " "ERROR" "No matching ct/ ${ service } .sh script found"
log_write "Container $container : ERROR — ct/ ${ service } .sh not found"
2026-02-06 08:54:21 +01:00
continue
fi
2026-09-05 05:04:27 +02:00
if [ " ${ resolved_service } " != " ${ service } " ] ; then
echo -e " ${ BL } [INFO] ${ CL } Script was renamed: ${ service } -> ${ GN }${ resolved_service }${ CL } "
log_write "Container $container : slug ${ service } resolved to ${ resolved_service } "
service = " ${ resolved_service } "
fi
2026-02-06 08:54:21 +01:00
2026-06-22 21:59:43 +02:00
echo -e " ${ BL } [INFO] ${ CL } Detected service: ${ GN }${ service }${ CL } "
log_write "Container $container : detected service ' ${ service } '"
2026-02-06 08:54:21 +01:00
#2) Extract service build/update resource requirements from config/installation file
2026-06-22 21:59:43 +02:00
script = $( curl -fsSL "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/ ${ service } .sh" )
2026-02-06 08:54:21 +01:00
#2.1) Check if the script downloaded successfully
2026-06-22 21:59:43 +02:00
if [ $? -ne 0 ] || [ -z " ${ script } " ] ; then
echo -e " ${ RD } [ERROR] ${ CL } Failed to download ct/ ${ service } .sh"
log_result " $container " " ${ service } " "ERROR" "Failed to download ct/ ${ service } .sh"
log_write "Container $container ( ${ service } ): ERROR — failed to download install script"
continue
2026-02-06 08:54:21 +01:00
fi
config = $( pct config " $container " )
build_cpu = $( echo " $script " | { grep -m 1 "var_cpu" || test $? = 1; } | sed 's|.*=||g' | sed 's|"||g' | sed 's|.*var_cpu:-||g' | sed 's|}||g' )
build_ram = $( echo " $script " | { grep -m 1 "var_ram" || test $? = 1; } | sed 's|.*=||g' | sed 's|"||g' | sed 's|.*var_ram:-||g' | sed 's|}||g' )
run_cpu = $( echo " $script " | { grep -m 1 "pct set \$CTID -cores" || test $? = 1; } | sed 's|.*cores ||g' )
run_ram = $( echo " $script " | { grep -m 1 "pct set \$CTID -memory" || test $? = 1; } | sed 's|.*memory ||g' )
current_cpu = $( echo " $config " | grep -m 1 "cores:" | sed 's|cores: ||g' )
current_ram = $( echo " $config " | grep -m 1 "memory:" | sed 's|memory: ||g' )
#Test if all values are valid (>0)
if [ -z " ${ run_cpu } " ] || [ " $run_cpu " -le 0 ] ; then
#echo "No valid value found for run_cpu. Assuming same as current configuration."
run_cpu = $current_cpu
fi
if [ -z " ${ run_ram } " ] || [ " $run_ram " -le 0 ] ; then
#echo "No valid value found for run_ram. Assuming same as current configuration."
run_ram = $current_ram
fi
if [ -z " ${ build_cpu } " ] || [ " $build_cpu " -le 0 ] ; then
#echo "No valid value found for build_cpu. Assuming same as current configuration."
build_cpu = $current_cpu
fi
if [ -z " ${ build_ram } " ] || [ " $build_ram " -le 0 ] ; then
#echo "No valid value found for build_ram. Assuming same as current configuration."
build_ram = $current_ram
fi
UPDATE_BUILD_RESOURCES = 0
if [ " $build_cpu " -gt " $run_cpu " ] || [ " $build_ram " -gt " $run_ram " ] ; then
UPDATE_BUILD_RESOURCES = 1
fi
#3) if build resources are different than run resources, then:
2026-05-07 15:53:22 +02:00
if [ " $UPDATE_BUILD_RESOURCES " -eq "1" ] && [[ " $var_dry_run " != "yes" ]] ; then
2026-02-06 08:54:21 +01:00
pct set " $container " --cores " $build_cpu " --memory " $build_ram "
fi
2026-05-07 15:53:22 +02:00
#3.5) Dry-run: report update availability without applying
if [[ " $var_dry_run " == "yes" ]] ; then
DRY_RUN_RESULT = ""
dry_run_container " $container " " $service "
log_result " $container " " $service " "DRY-RUN" " ${ DRY_RUN_RESULT :- version check only } "
log_write "Container $container ( $service ): DRY-RUN — ${ DRY_RUN_RESULT :- version check only } "
continue
fi
2026-02-06 08:54:21 +01:00
#4) Update service, using the update command
2026-05-07 15:53:22 +02:00
# Prepend a no-op 'clear' wrapper to PATH so update scripts calling clear
# don't fail without a TTY — works for all shells incl. ash (no export -f)
SETUP_CMD = "mkdir -p /tmp/.nc; printf '#!/bin/sh\n:\n' > /tmp/.nc/clear; chmod +x /tmp/.nc/clear; export PATH=/tmp/.nc:\$PATH; export TERM=dumb; "
2026-02-06 08:54:21 +01:00
case " $os " in
2026-05-07 15:53:22 +02:00
alpine) pct exec " $container " -- ash -c " ${ SETUP_CMD }${ UPDATE_CMD } " ;;
archlinux) pct exec " $container " -- bash -c " ${ SETUP_CMD }${ UPDATE_CMD } " ;;
fedora | rocky | centos | alma) pct exec " $container " -- bash -c " ${ SETUP_CMD }${ UPDATE_CMD } " ;;
ubuntu | debian | devuan) pct exec " $container " -- bash -c " ${ SETUP_CMD }${ UPDATE_CMD } " ;;
opensuse) pct exec " $container " -- bash -c " ${ SETUP_CMD }${ UPDATE_CMD } " ;;
2026-02-06 08:54:21 +01:00
esac
exit_code = $?
#5) if build resources are different than run resources, then:
if [ " $UPDATE_BUILD_RESOURCES " -eq "1" ] ; then
pct set " $container " --cores " $run_cpu " --memory " $run_ram "
fi
if pct exec " $container " -- [ -e "/var/run/reboot-required" ] ; then
# Get the container's hostname and add it to the list
container_hostname = $( pct exec " $container " hostname)
containers_needing_reboot +=( " $container ( $container_hostname )" )
fi
2026-04-27 13:30:20 +02:00
if [ " $template " == "false" ] && [ " $status " == "status: stopped" ] ; then
echo -e " ${ BL } [Info] ${ GN } Shutting down ${ BL } $container ${ CL } \n"
pct shutdown $container & >/dev/null &
fi
2026-02-06 08:54:21 +01:00
if [ $exit_code -eq 0 ] ; then
msg_ok "Updated container $container "
2026-05-07 15:53:22 +02:00
log_result " $container " " $service " "OK" "Updated successfully"
log_write "Container $container ( $service ): OK"
2026-02-09 11:05:31 +01:00
elif [ $exit_code -eq 75 ] ; then
echo -e " ${ YW } [WARN] ${ CL } Container $container skipped (requires interactive mode)"
2026-05-07 15:53:22 +02:00
log_result " $container " " $service " "SKIPPED" "Requires interactive mode (exit 75)"
log_write "Container $container ( $service ): SKIPPED — requires interactive mode"
elif [ $exit_code -eq 113 ] ; then
echo -e " ${ YW } [WARN] ${ CL } Container $container skipped (under-provisioned: increase CPU/RAM to match template)"
log_result " $container " " $service " "SKIPPED" "Under-provisioned — increase CPU/RAM to match template"
log_write "Container $container ( $service ): SKIPPED — under-provisioned"
elif [ $exit_code -eq 114 ] ; then
echo -e " ${ YW } [WARN] ${ CL } Container $container skipped (storage critically low on /boot)"
log_result " $container " " $service " "SKIPPED" "Storage critically low on /boot (>80%)"
log_write "Container $container ( $service ): SKIPPED — storage critically low on /boot"
2026-02-06 08:54:21 +01:00
elif [ " $BACKUP_CHOICE " == "yes" ] ; then
2026-03-13 23:54:40 +03:00
msg_error "Update failed for container $container (exit code: $exit_code ) — attempting restore"
2026-05-07 15:53:22 +02:00
log_write "Container $container ( $service ): FAILED (exit $exit_code ) — attempting restore"
2026-03-13 23:54:40 +03:00
msg_info "Restoring LXC $container from backup ( $STORAGE_CHOICE )"
2026-02-06 08:54:21 +01:00
pct stop $container
LXC_STORAGE = $( pct config $container | awk -F '[:,]' '/rootfs/ {print $2}' )
2026-03-13 23:54:40 +03:00
BACKUP_ENTRY = $( pvesm list " $STORAGE_CHOICE " 2>/dev/null | awk -v ctid = " $container " '$1 ~ "vzdump-lxc-"ctid"-" || $1 ~ "/ct/"ctid"/" {print $1}' | sort -r | head -n1)
if [ -z " $BACKUP_ENTRY " ] ; then
msg_error "No backup found in storage $STORAGE_CHOICE for container $container "
2026-05-07 15:53:22 +02:00
log_result " $container " " $service " "FAILED" "Update failed (exit $exit_code ) — no backup found for restore"
log_write "Container $container ( $service ): FAILED — no backup found for restore"
2026-03-13 23:54:40 +03:00
exit 235
fi
pct restore $container " $BACKUP_ENTRY " --storage $LXC_STORAGE --force >/dev/null 2>& 1
2026-02-06 08:54:21 +01:00
restorestatus = $?
if [ $restorestatus -eq 0 ] ; then
2026-03-13 23:54:40 +03:00
pct start $container
2026-10-03 08:19:10 +02:00
msg_ok "Container $container restored from ${ BACKUP_ENTRY } "
2026-05-07 15:53:22 +02:00
log_result " $container " " $service " "RESTORED" "Update failed (exit $exit_code ) — restored from backup"
log_write "Container $container ( $service ): RESTORED from $BACKUP_ENTRY "
2026-02-06 08:54:21 +01:00
else
2026-03-13 23:54:40 +03:00
msg_error "Restore failed for container $container "
2026-05-07 15:53:22 +02:00
log_result " $container " " $service " "FAILED" "Update failed (exit $exit_code ) — restore also failed"
log_write "Container $container ( $service ): FAILED — restore also failed"
2026-03-02 10:55:20 +01:00
exit 235
2026-02-06 08:54:21 +01:00
fi
else
2026-05-07 15:53:22 +02:00
msg_error "Update failed for container $container (exit code: $exit_code )"
log_result " $container " " $service " "FAILED" "Exit code $exit_code "
log_write "Container $container ( $service ): FAILED (exit $exit_code )"
if [[ " $var_continue_on_error " == "yes" ]] ; then
echo -e " ${ YW } [WARN] ${ CL } Continuing to next container (var_continue_on_error=yes)"
continue
else
exit " $exit_code "
fi
2026-02-06 08:54:21 +01:00
fi
done
wait
header_info
2026-05-07 15:53:22 +02:00
if [[ " $var_dry_run " == "yes" ]] ; then
echo -e " ${ GN } Dry-run complete. No containers were modified. ${ CL } \n"
else
echo -e " ${ GN } The process is complete, and the containers have been successfully updated. ${ CL } \n"
fi
# =============================================================================
# SUMMARY REPORT
# =============================================================================
if [ " ${# UPDATE_RESULTS [@] } " -gt 0 ] ; then
SEPARATOR = "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
HEADER = $( printf " %-8s %-22s %-10s %s" "CTID" "Service" "Status" "Details" )
# terminal output (with colours)
echo ""
echo " $SEPARATOR "
echo " $HEADER "
echo " $SEPARATOR "
for entry in " ${ UPDATE_RESULTS [@] } " ; do
IFS = '|' read -r _ctid _svc _status _details <<< " $entry "
case " $_status " in
OK) _color = " ${ GN } " ;;
FAILED) _color = " ${ RD } " ;;
RESTORED) _color = " ${ YW } " ;;
*) _color = " ${ YW } " ;;
esac
printf " %-8s %-22s ${ _color } %-10s ${ CL } %s\n" " $_ctid " " $_svc " " $_status " " $_details "
done
echo " $SEPARATOR "
echo ""
echo "Full log: $LOG_FILE "
echo ""
# append plain-text summary to log file
{
echo ""
echo "Update finished: $( date '+%Y-%m-%d %H:%M:%S' ) "
echo " $SEPARATOR "
echo " $HEADER "
echo " $SEPARATOR "
for entry in " ${ UPDATE_RESULTS [@] } " ; do
IFS = '|' read -r _ctid _svc _status _details <<< " $entry "
printf " %-8s %-22s %-10s %s\n" " $_ctid " " $_svc " " $_status " " $_details "
done
echo " $SEPARATOR "
} >>" $LOG_FILE "
fi
2026-02-06 08:54:21 +01:00
if [ " ${# containers_needing_reboot [@] } " -gt 0 ] ; then
echo -e " ${ RD } The following containers require a reboot: ${ CL } "
for container_name in " ${ containers_needing_reboot [@] } " ; do
echo " $container_name "
done
# Determine reboot choice based on var_auto_reboot
REBOOT_CHOICE = "no"
if [[ -n " $var_auto_reboot " ]] ; then
REBOOT_CHOICE = " $var_auto_reboot "
else
echo -ne " ${ INFO } Do you wish to reboot these containers? <yes/No> "
read -r prompt
if [[ ${ prompt ,, } = ~ ^( yes) $ ]] ; then
REBOOT_CHOICE = "yes"
fi
fi
if [[ " $REBOOT_CHOICE " == "yes" ]] ; then
echo -e " ${ CROSS }${ HOLD } ${ YWB } Rebooting containers. ${ CL } "
for container_name in " ${ containers_needing_reboot [@] } " ; do
container = $( echo $container_name | cut -d " " -f 1)
pct reboot ${ container }
done
fi
fi