Display an installation summary (including downloaded script SHA256, source, install path, config, log file, and cron schedule) and make script review optional before install. Add an interactive View menu to inspect the installed worker script, cron configuration, or both; implement view_worker_script, view_cron_config (shows cron entry, human-readable schedule, config contents and excluded patterns, and log file size), and cron_to_human helper. Also update prompt wording and the View option description.
351 lines
12 KiB
Bash
351 lines
12 KiB
Bash
#!/usr/bin/env bash
|
|
|
|
# Copyright (c) 2021-2026 community-scripts ORG
|
|
# Author: MickLesk (CanbiZ)
|
|
# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE
|
|
#
|
|
# This script manages a local cron job for automatic LXC container OS updates.
|
|
# The update script is downloaded once, displayed for review, and installed
|
|
# locally. Cron runs the local copy — no remote code execution at runtime.
|
|
#
|
|
# bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/tools/pve/cron-update-lxcs.sh)"
|
|
|
|
set -euo pipefail
|
|
|
|
REPO_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main"
|
|
SCRIPT_URL="${REPO_URL}/tools/pve/update-lxcs-cron.sh"
|
|
LOCAL_SCRIPT="/usr/local/bin/update-lxcs.sh"
|
|
CONF_FILE="/etc/update-lxcs.conf"
|
|
LOG_FILE="/var/log/update-lxcs-cron.log"
|
|
CRON_ENTRY="0 0 * * 0 ${LOCAL_SCRIPT} >>${LOG_FILE} 2>&1"
|
|
|
|
clear
|
|
cat <<"EOF"
|
|
______ __ __ __ __ __ _ ________
|
|
/ ____/________ ____ / / / /___ ____/ /___ _/ /____ / / | |/ / ____/____
|
|
/ / / ___/ __ \/ __ \ / / / / __ \/ __ / __ `/ __/ _ \ / / | / / / ___/
|
|
/ /___/ / / /_/ / / / / / /_/ / /_/ / /_/ / /_/ / /_/ __/ / /___/ / /___(__ )
|
|
\____/_/ \____/_/ /_/ \____/ .___/\__,_/\__,_/\__/\___/ /_____/_/|_\____/____/
|
|
/_/
|
|
EOF
|
|
|
|
info() { echo -e "\n \e[36m[Info]\e[0m $1"; }
|
|
ok() { echo -e " \e[32m[OK]\e[0m $1"; }
|
|
err() { echo -e " \e[31m[Error]\e[0m $1" >&2; }
|
|
|
|
confirm() {
|
|
local prompt="${1:-Proceed?}"
|
|
while true; do
|
|
read -rp " ${prompt} (y/n): " yn
|
|
case $yn in
|
|
[Yy]*) return 0 ;;
|
|
[Nn]*) return 1 ;;
|
|
*) echo " Please answer yes or no." ;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
download_script() {
|
|
local tmp
|
|
tmp=$(mktemp)
|
|
if ! curl -fsSL -o "$tmp" "$SCRIPT_URL"; then
|
|
err "Failed to download script from:\n ${SCRIPT_URL}"
|
|
rm -f "$tmp"
|
|
return 1
|
|
fi
|
|
echo "$tmp"
|
|
}
|
|
|
|
review_script() {
|
|
local file="$1"
|
|
local hash
|
|
hash=$(sha256sum "$file" | awk '{print $1}')
|
|
echo ""
|
|
echo -e " \e[1;33m─── Script Content ───────────────────────────────────────────\e[0m"
|
|
cat "$file"
|
|
echo -e " \e[1;33m──────────────────────────────────────────────────────────────\e[0m"
|
|
echo -e " \e[36mSHA256:\e[0m ${hash}"
|
|
echo -e " \e[36mSource:\e[0m ${SCRIPT_URL}"
|
|
echo ""
|
|
}
|
|
|
|
remove_legacy_cron() {
|
|
if crontab -l -u root 2>/dev/null | grep -q "update-lxcs-cron.sh"; then
|
|
(crontab -l -u root 2>/dev/null | grep -v "update-lxcs-cron.sh") | crontab -u root -
|
|
ok "Removed legacy curl-based cron entry"
|
|
fi
|
|
}
|
|
|
|
add() {
|
|
info "Downloading update script..."
|
|
local tmp
|
|
tmp=$(download_script) || exit 1
|
|
|
|
local hash
|
|
hash=$(sha256sum "$tmp" | awk '{print $1}')
|
|
echo ""
|
|
echo -e " \e[1;33m─── Installation Summary ─────────────────────────────────────\e[0m"
|
|
echo -e " \e[36mSource:\e[0m ${SCRIPT_URL}"
|
|
echo -e " \e[36mSHA256:\e[0m ${hash}"
|
|
echo -e " \e[36mInstall to:\e[0m ${LOCAL_SCRIPT}"
|
|
echo -e " \e[36mConfig:\e[0m ${CONF_FILE}"
|
|
echo -e " \e[36mLog file:\e[0m ${LOG_FILE}"
|
|
echo -e " \e[36mCron schedule:\e[0m Every Sunday at midnight (0 0 * * 0)"
|
|
echo -e " \e[1;33m──────────────────────────────────────────────────────────────\e[0m"
|
|
echo ""
|
|
|
|
if confirm "Review script content before installing?"; then
|
|
review_script "$tmp"
|
|
fi
|
|
|
|
if ! confirm "Install this script and activate cron schedule?"; then
|
|
rm -f "$tmp"
|
|
echo " Aborted."
|
|
exit 0
|
|
fi
|
|
|
|
remove_legacy_cron
|
|
|
|
install -m 0755 "$tmp" "$LOCAL_SCRIPT"
|
|
rm -f "$tmp"
|
|
ok "Installed script to ${LOCAL_SCRIPT}"
|
|
|
|
if [[ ! -f "$CONF_FILE" ]]; then
|
|
cat >"$CONF_FILE" <<'CONF'
|
|
# Configuration for automatic LXC container OS updates.
|
|
# Add container IDs to exclude from updates (comma-separated):
|
|
# EXCLUDE=100,101,102
|
|
EXCLUDE=
|
|
CONF
|
|
ok "Created config ${CONF_FILE}"
|
|
fi
|
|
|
|
(
|
|
crontab -l -u root 2>/dev/null | grep -v "${LOCAL_SCRIPT}"
|
|
echo "${CRON_ENTRY}"
|
|
) | crontab -u root -
|
|
ok "Added cron schedule: Every Sunday at midnight"
|
|
echo ""
|
|
echo -e " \e[36mLocal script:\e[0m ${LOCAL_SCRIPT}"
|
|
echo -e " \e[36mConfig:\e[0m ${CONF_FILE}"
|
|
echo -e " \e[36mLog file:\e[0m ${LOG_FILE}"
|
|
echo ""
|
|
}
|
|
|
|
remove() {
|
|
if crontab -l -u root 2>/dev/null | grep -q "${LOCAL_SCRIPT}"; then
|
|
(crontab -l -u root 2>/dev/null | grep -v "${LOCAL_SCRIPT}") | crontab -u root -
|
|
ok "Removed cron schedule"
|
|
fi
|
|
remove_legacy_cron
|
|
[[ -f "$LOCAL_SCRIPT" ]] && rm -f "$LOCAL_SCRIPT" && ok "Removed ${LOCAL_SCRIPT}"
|
|
[[ -f "$LOG_FILE" ]] && rm -f "$LOG_FILE" && ok "Removed ${LOG_FILE}"
|
|
echo -e "\n Cron Update LXCs has been fully removed."
|
|
echo -e " \e[90mNote: ${CONF_FILE} was kept (remove manually if desired).\e[0m"
|
|
}
|
|
|
|
update_script() {
|
|
if [[ ! -f "$LOCAL_SCRIPT" ]]; then
|
|
err "No local script found at ${LOCAL_SCRIPT}. Use 'Add' first."
|
|
exit 1
|
|
fi
|
|
|
|
info "Downloading latest version..."
|
|
local tmp
|
|
tmp=$(download_script) || exit 1
|
|
|
|
if command -v diff &>/dev/null; then
|
|
local changes
|
|
changes=$(diff --color=auto "$LOCAL_SCRIPT" "$tmp" 2>/dev/null || true)
|
|
if [[ -z "$changes" ]]; then
|
|
ok "Script is already up-to-date (no changes)."
|
|
rm -f "$tmp"
|
|
return
|
|
fi
|
|
echo ""
|
|
echo -e " \e[1;33m─── Changes ──────────────────────────────────────────────────\e[0m"
|
|
echo "$changes"
|
|
echo -e " \e[1;33m──────────────────────────────────────────────────────────────\e[0m"
|
|
else
|
|
review_script "$tmp"
|
|
fi
|
|
|
|
local new_hash old_hash
|
|
new_hash=$(sha256sum "$tmp" | awk '{print $1}')
|
|
old_hash=$(sha256sum "$LOCAL_SCRIPT" | awk '{print $1}')
|
|
echo -e " \e[36mCurrent SHA256:\e[0m ${old_hash}"
|
|
echo -e " \e[36mNew SHA256:\e[0m ${new_hash}"
|
|
echo ""
|
|
|
|
if ! confirm "Apply update?"; then
|
|
rm -f "$tmp"
|
|
echo " Aborted."
|
|
return
|
|
fi
|
|
|
|
install -m 0755 "$tmp" "$LOCAL_SCRIPT"
|
|
rm -f "$tmp"
|
|
ok "Updated ${LOCAL_SCRIPT}"
|
|
}
|
|
|
|
view_script() {
|
|
if [[ ! -f "$LOCAL_SCRIPT" ]]; then
|
|
err "No local script found at ${LOCAL_SCRIPT}. Use 'Add' first."
|
|
exit 1
|
|
fi
|
|
|
|
local view_choice
|
|
view_choice=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "View Script" --menu "What do you want to view?" 12 60 3 \
|
|
"Worker" "Installed update script (${LOCAL_SCRIPT##*/})" \
|
|
"Cron" "Cron schedule & configuration" \
|
|
"Both" "Show everything" \
|
|
3>&1 1>&2 2>&3) || return 0
|
|
|
|
case "$view_choice" in
|
|
"Worker") view_worker_script ;;
|
|
"Cron") view_cron_config ;;
|
|
"Both") view_cron_config && echo "" && view_worker_script ;;
|
|
esac
|
|
}
|
|
|
|
view_worker_script() {
|
|
local hash
|
|
hash=$(sha256sum "$LOCAL_SCRIPT" | awk '{print $1}')
|
|
echo ""
|
|
echo -e " \e[1;33m─── ${LOCAL_SCRIPT} ───\e[0m"
|
|
cat "$LOCAL_SCRIPT"
|
|
echo -e " \e[1;33m──────────────────────────────────────────────────────────────\e[0m"
|
|
echo -e " \e[36mSHA256:\e[0m ${hash}"
|
|
echo -e " \e[36mInstalled:\e[0m $(stat -c '%y' "$LOCAL_SCRIPT" 2>/dev/null | cut -d. -f1)"
|
|
echo ""
|
|
}
|
|
|
|
view_cron_config() {
|
|
echo ""
|
|
echo -e " \e[1;33m─── Cron Configuration ───────────────────────────────────────\e[0m"
|
|
if crontab -l -u root 2>/dev/null | grep -q "${LOCAL_SCRIPT}"; then
|
|
local entry
|
|
entry=$(crontab -l -u root 2>/dev/null | grep "${LOCAL_SCRIPT}")
|
|
echo -e " \e[36mCron entry:\e[0m ${entry}"
|
|
local schedule
|
|
schedule=$(echo "$entry" | awk '{print $1,$2,$3,$4,$5}')
|
|
echo -e " \e[36mSchedule:\e[0m ${schedule} ($(cron_to_human "$schedule"))"
|
|
else
|
|
echo -e " \e[31mCron:\e[0m Not configured"
|
|
fi
|
|
if [[ -f "$CONF_FILE" ]]; then
|
|
echo -e " \e[36mConfig file:\e[0m ${CONF_FILE}"
|
|
local excludes
|
|
excludes=$(grep -oP '^\s*EXCLUDE\s*=\s*\K.*' "$CONF_FILE" 2>/dev/null || true)
|
|
echo -e " \e[36mExcluded:\e[0m ${excludes:-(none)}"
|
|
echo ""
|
|
echo -e " \e[90m--- ${CONF_FILE} ---\e[0m"
|
|
cat "$CONF_FILE"
|
|
else
|
|
echo -e " \e[36mConfig file:\e[0m (not created yet)"
|
|
fi
|
|
if [[ -f "$LOG_FILE" ]]; then
|
|
local log_size
|
|
log_size=$(du -h "$LOG_FILE" | awk '{print $1}')
|
|
echo -e " \e[36mLog file:\e[0m ${LOG_FILE} (${log_size})"
|
|
fi
|
|
echo -e " \e[1;33m──────────────────────────────────────────────────────────────\e[0m"
|
|
echo ""
|
|
}
|
|
|
|
cron_to_human() {
|
|
local schedule="$1"
|
|
case "$schedule" in
|
|
"0 0 * * 0") echo "Every Sunday at midnight" ;;
|
|
"0 0 * * *") echo "Daily at midnight" ;;
|
|
"0 * * * *") echo "Every hour" ;;
|
|
*) echo "Custom schedule" ;;
|
|
esac
|
|
}
|
|
|
|
show_status() {
|
|
echo ""
|
|
if [[ -f "$LOCAL_SCRIPT" ]]; then
|
|
local hash
|
|
hash=$(sha256sum "$LOCAL_SCRIPT" | awk '{print $1}')
|
|
ok "Script installed: ${LOCAL_SCRIPT}"
|
|
echo -e " \e[36mSHA256:\e[0m ${hash}"
|
|
echo -e " \e[36mInstalled:\e[0m $(stat -c '%y' "$LOCAL_SCRIPT" 2>/dev/null | cut -d. -f1)"
|
|
else
|
|
err "Script not installed"
|
|
fi
|
|
|
|
if crontab -l -u root 2>/dev/null | grep -q "${LOCAL_SCRIPT}"; then
|
|
local schedule
|
|
schedule=$(crontab -l -u root 2>/dev/null | grep "${LOCAL_SCRIPT}" | awk '{print $1,$2,$3,$4,$5}')
|
|
ok "Cron active: ${schedule}"
|
|
else
|
|
err "Cron not configured"
|
|
fi
|
|
|
|
if [[ -f "$CONF_FILE" ]]; then
|
|
local excludes
|
|
excludes=$(grep -oP '^\s*EXCLUDE\s*=\s*\K.*' "$CONF_FILE" 2>/dev/null || echo "(none)")
|
|
echo -e " \e[36mExcluded:\e[0m ${excludes:-"(none)"}"
|
|
fi
|
|
|
|
if [[ -f "$LOG_FILE" ]]; then
|
|
local log_size last_run
|
|
log_size=$(du -h "$LOG_FILE" | awk '{print $1}')
|
|
last_run=$(grep -oP '^\s+\K\w.*' "$LOG_FILE" | tail -1)
|
|
echo -e " \e[36mLog file:\e[0m ${LOG_FILE} (${log_size})"
|
|
[[ -n "${last_run:-}" ]] && echo -e " \e[36mLast run:\e[0m ${last_run}"
|
|
else
|
|
echo -e " \e[36mLog file:\e[0m (no runs yet)"
|
|
fi
|
|
echo ""
|
|
}
|
|
|
|
run_now() {
|
|
if [[ ! -f "$LOCAL_SCRIPT" ]]; then
|
|
err "No local script found at ${LOCAL_SCRIPT}. Use 'Add' first."
|
|
exit 1
|
|
fi
|
|
info "Running update script now..."
|
|
bash "$LOCAL_SCRIPT" | tee -a "$LOG_FILE"
|
|
ok "Run completed. Log appended to ${LOG_FILE}"
|
|
}
|
|
|
|
rotate_log() {
|
|
if [[ ! -f "$LOG_FILE" ]]; then
|
|
info "No log file to rotate."
|
|
return
|
|
fi
|
|
local log_size
|
|
log_size=$(stat -c '%s' "$LOG_FILE" 2>/dev/null || echo 0)
|
|
local log_size_h
|
|
log_size_h=$(du -h "$LOG_FILE" | awk '{print $1}')
|
|
if confirm "Rotate log file? (current size: ${log_size_h})"; then
|
|
mv "$LOG_FILE" "${LOG_FILE}.old"
|
|
ok "Rotated: ${LOG_FILE} → ${LOG_FILE}.old"
|
|
fi
|
|
}
|
|
|
|
OPTIONS=(
|
|
Add "Download, review & install cron schedule"
|
|
Remove "Remove cron schedule & local script"
|
|
Update "Update local script from repository"
|
|
Status "Show installation status & last run"
|
|
Run "Run update script now (manual trigger)"
|
|
View "View cron config & installed script"
|
|
Rotate "Rotate log file"
|
|
)
|
|
|
|
CHOICE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "Cron Update LXCs" --menu "Select an option:" 16 68 7 \
|
|
"${OPTIONS[@]}" 3>&1 1>&2 2>&3) || exit 0
|
|
|
|
case $CHOICE in
|
|
"Add") add ;;
|
|
"Remove") remove ;;
|
|
"Update") update_script ;;
|
|
"Status") show_status ;;
|
|
"Run") run_now ;;
|
|
"View") view_script ;;
|
|
"Rotate") rotate_log ;;
|
|
esac
|