ProxmoxVED/tools/pve/dependency-check.sh
CanbiZ (MickLesk) 6c43b624c1 Enhance Proxmox dependency-check scripts
Update frontend docs and significantly refactor dependency-check tooling.

- frontend: set supported version to "PVE 8.x / 9.x" and add info about --install/--status/--uninstall flags.
- Add new tools/pve/dependency-check copy.sh (installer wrapper).
- Rework tools/pve/dependency-check.sh: add CLI (install/status/uninstall), PVE version detection/validation, improved logging/colors, safer config parsing, more robust storage checks, validated tag handling (dep_ping/dep_tcp), portable TCP/ping checks, and wait/timeout helper.
- Improve applicator script handling (ignore list, avoid overwriting other hookscripts), update systemd units (PathExistsGlob, unit binding), and implement uninstall to remove assignments and installed files.

These changes harden lifecycle management and make installation/cleanup and runtime checks more robust and observable.
2026-02-17 10:23:26 +01:00

510 lines
14 KiB
Bash
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# Copyright (c) 2023-2026 community-scripts ORG
# Author: MickLesk | Maintainer: community-scripts
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://www.proxmox.com/
function header_info {
clear
cat <<"EOF"
____ _ ____ _ _
| _ \ ___ _ __ ___ _ __ __| | ___ _ __ ___ _ _ / ___| |__ ___ ___| | __
| | | |/ _ \ '_ \ / _ \ '_ \ / _` |/ _ \ '_ \ / __| | | | | | '_ \ / _ \/ __| |/ /
| |_| | __/ |_) | __/ | | | (_| | __/ | | | (__| |_| | |___| | | | __/ (__| <
|____/ \___| .__/ \___|_| |_|\__,_|\___|_| |_|\___|\__, |\____|_| |_|\___|\___|_|\_\
|_| |___/
EOF
}
YW=$(echo "\033[33m")
GN=$(echo "\033[1;92m")
RD=$(echo "\033[01;31m")
BL=$(echo "\033[36m")
CL=$(echo "\033[m")
BFR="\\r\\033[K"
CM="${GN}✔️${CL}"
CROSS="${RD}✖️${CL}"
INFO="${BL}${CL}"
msg_info() {
local msg="$1"
echo -e "${INFO} ${YW}${msg}...${CL}"
}
msg_ok() {
local msg="$1"
echo -e "${BFR} ${CM} ${GN}${msg}${CL}"
}
msg_error() {
local msg="$1"
echo -e "${BFR} ${CROSS} ${RD}${msg}${CL}"
}
SCRIPT_NAME="$(basename "$0")"
HOOKSCRIPT_FILE="/var/lib/vz/snippets/dependency-check.sh"
HOOKSCRIPT_VOLUME_ID="local:snippets/dependency-check.sh"
CONFIG_FILE="/etc/default/pve-auto-hook"
APPLICATOR_FILE="/usr/local/bin/pve-apply-hookscript.sh"
PATH_UNIT_FILE="/etc/systemd/system/pve-auto-hook.path"
SERVICE_UNIT_FILE="/etc/systemd/system/pve-auto-hook.service"
function print_usage {
cat <<EOF
Usage: ${SCRIPT_NAME} [OPTIONS]
Install or remove the Proxmox startup dependency-check hook system.
Options:
--install Install/update hookscript automation (default)
--uninstall Remove automation and cleanup hookscript assignments
--status Show current installation state
--help, -h Show this help message
EOF
}
function ensure_supported_pve {
if ! command -v pveversion >/dev/null 2>&1; then
msg_error "This script must be run on a Proxmox VE host"
exit 1
fi
local pve_version major
pve_version=$(pveversion | grep -oE 'pve-manager/[0-9.]+' | cut -d'/' -f2)
major=$(echo "$pve_version" | cut -d'.' -f1)
if [[ -z "$major" ]] || ! [[ "$major" =~ ^[0-9]+$ ]]; then
msg_error "Unable to detect a supported Proxmox version"
exit 1
fi
if [[ "$major" -lt 8 ]] || [[ "$major" -gt 9 ]]; then
msg_error "Supported on Proxmox VE 8.x and 9.x (detected: $pve_version)"
exit 1
fi
msg_ok "Proxmox VE $pve_version detected"
}
function confirm_action {
local prompt="$1"
read -r -p "$prompt (y/n): " -n 1 REPLY
echo
[[ "$REPLY" =~ ^[Yy]$ ]]
}
create_dependency_hookscript() {
msg_info "Creating dependency-check hookscript"
mkdir -p /var/lib/vz/snippets
cat <<'EOF' >/var/lib/vz/snippets/dependency-check.sh
#!/bin/bash
# Proxmox Hookscript for Pre-Start Dependency Checking
# Works for both QEMU VMs and LXC Containers
POLL_INTERVAL=5 # Seconds to wait between checks
MAX_ATTEMPTS=60 # Max number of attempts before failing (60 * 5s = 5 minutes)
VMID=$1
PHASE=$2
log() {
logger -t hookscript-dep-check "VMID $VMID: $1"
}
has_cmd() {
command -v "$1" >/dev/null 2>&1
}
check_tcp() {
local host="$1"
local port="$2"
if has_cmd nc; then
nc -z -w 2 "$host" "$port" >/dev/null 2>&1
return $?
fi
timeout 2 bash -c "</dev/tcp/${host}/${port}" >/dev/null 2>&1
}
wait_until() {
local description="$1"
local check_cmd="$2"
local attempts=0
while true; do
if eval "$check_cmd"; then
log "$description"
return 0
fi
attempts=$((attempts + 1))
if [ "$attempts" -ge "$MAX_ATTEMPTS" ]; then
log "ERROR: Timeout waiting for condition: $description"
return 1
fi
log "Waiting ${POLL_INTERVAL}s for condition: $description (Attempt ${attempts}/${MAX_ATTEMPTS})"
sleep "$POLL_INTERVAL"
done
}
if [ "$PHASE" != "pre-start" ]; then
exit 0
fi
log "--- Starting Pre-Start Dependency Check ---"
if qm config "$VMID" >/dev/null 2>&1; then
CONFIG_CMD=(qm config "$VMID")
log "Guest type is QEMU (VM)."
elif pct config "$VMID" >/dev/null 2>&1; then
CONFIG_CMD=(pct config "$VMID")
log "Guest type is LXC (Container)."
else
log "ERROR: Could not determine guest type for $VMID. Aborting."
exit 1
fi
GUEST_CONFIG=$("${CONFIG_CMD[@]}")
log "Checking storage availability..."
STORAGE_IDS=$(echo "$GUEST_CONFIG" | awk -F':' '
/^(scsi|sata|virtio|ide|efidisk|tpmstate|unused|rootfs|mp)[0-9]*:/ {
val=$2
gsub(/^[[:space:]]+/, "", val)
split(val, parts, ",")
storage=parts[1]
# Skip bind-mount style paths and empty values
if (storage == "" || storage ~ /^\//) next
print storage
}
' | sort -u)
if [ -z "$STORAGE_IDS" ]; then
log "No storage dependencies found to check."
else
for STORAGE_ID in $STORAGE_IDS; do
STATUS=$(pvesm status 2>/dev/null | awk -v id="$STORAGE_ID" '$1 == id { print $3; exit }')
if [ -z "$STATUS" ]; then
log "WARNING: Storage '$STORAGE_ID' not found in 'pvesm status'. Skipping this dependency."
continue
fi
wait_until "Storage '$STORAGE_ID' is active." "[ \"\$(pvesm status 2>/dev/null | awk -v id=\"$STORAGE_ID\" '\$1 == id { print \$3; exit }')\" = \"active\" ]" || exit 1
done
fi
log "All storage dependencies are met."
log "Checking for custom tag-based dependencies..."
TAGS=$(echo "$GUEST_CONFIG" | awk -F': ' '/^tags:/ {print $2}')
if [ -z "$TAGS" ]; then
log "No tags found. Skipping custom dependency check."
else
for TAG in ${TAGS//;/ }; do
if [[ $TAG == dep_* ]]; then
log "Found dependency tag: '$TAG'"
IFS='_' read -r _ DEP_TYPE HOST PORT EXTRA <<< "$TAG"
case "$DEP_TYPE" in
ping)
if [ -z "$HOST" ]; then
log "WARNING: Malformed ping dependency tag '$TAG'. Ignoring."
continue
fi
wait_until "Ping dependency met: Host $HOST is reachable." "ping -c 1 -W 2 \"$HOST\" >/dev/null 2>&1" || exit 1
;;
tcp)
if [ -z "$HOST" ] || [ -z "$PORT" ] || ! [[ "$PORT" =~ ^[0-9]+$ ]] || [ "$PORT" -lt 1 ] || [ "$PORT" -gt 65535 ]; then
log "WARNING: Malformed TCP dependency tag '$TAG'. Expected dep_tcp_<host>_<port>. Ignoring."
continue
fi
wait_until "TCP dependency met: Host $HOST port $PORT is open." "check_tcp \"$HOST\" \"$PORT\"" || exit 1
;;
*)
log "WARNING: Unknown dependency type '$DEP_TYPE' in tag '$TAG'. Ignoring."
;;
esac
fi
done
fi
log "All custom dependencies are met."
log "--- Dependency Check Complete. Proceeding with start. ---"
exit 0
EOF
chmod +x "$HOOKSCRIPT_FILE"
msg_ok "Created dependency-check hookscript"
}
create_exclusion_config() {
msg_info "Creating exclusion configuration file"
if [ -f "$CONFIG_FILE" ]; then
msg_ok "Exclusion file already exists, skipping."
else
cat <<'EOF' >/etc/default/pve-auto-hook
#
# Configuration for the Proxmox Automatic Hookscript Applicator
#
# Add VM or LXC IDs here to prevent the hookscript from being added.
# Separate IDs with spaces.
#
# Example:
# IGNORE_IDS="9000 9001 105"
#
IGNORE_IDS=""
EOF
chmod 0644 "$CONFIG_FILE"
msg_ok "Created exclusion configuration file"
fi
}
create_applicator_script() {
msg_info "Creating the hookscript applicator script"
cat <<'EOF' >/usr/local/bin/pve-apply-hookscript.sh
#!/bin/bash
HOOKSCRIPT_VOLUME_ID="local:snippets/dependency-check.sh"
CONFIG_FILE="/etc/default/pve-auto-hook"
LOG_TAG="pve-auto-hook"
IGNORE_IDS=""
log() {
systemd-cat -t "$LOG_TAG" <<< "$1"
}
if [ -f "$CONFIG_FILE" ]; then
IGNORE_IDS=$(grep -E '^IGNORE_IDS=' "$CONFIG_FILE" | head -n1 | cut -d'=' -f2- | tr -d '"')
fi
is_ignored() {
local vmid="$1"
for id_to_ignore in $IGNORE_IDS; do
if [ "$id_to_ignore" = "$vmid" ]; then
return 0
fi
done
return 1
}
ensure_hookscript() {
local guest_type="$1"
local vmid="$2"
local current_hook=""
if [ "$guest_type" = "qemu" ]; then
current_hook=$(qm config "$vmid" | awk '/^hookscript:/ {print $2}')
else
current_hook=$(pct config "$vmid" | awk '/^hookscript:/ {print $2}')
fi
if [ -n "$current_hook" ]; then
if [ "$current_hook" = "$HOOKSCRIPT_VOLUME_ID" ]; then
return 0
fi
log "Guest $guest_type/$vmid already has another hookscript ($current_hook). Leaving unchanged."
return 0
fi
log "Applying hookscript to $guest_type/$vmid"
if [ "$guest_type" = "qemu" ]; then
qm set "$vmid" --hookscript "$HOOKSCRIPT_VOLUME_ID" >/dev/null 2>&1
else
pct set "$vmid" --hookscript "$HOOKSCRIPT_VOLUME_ID" >/dev/null 2>&1
fi
}
qm list | awk 'NR>1 {print $1}' | while read -r VMID; do
if is_ignored "$VMID"; then
continue
fi
ensure_hookscript "qemu" "$VMID"
done
pct list | awk 'NR>1 {print $1}' | while read -r VMID; do
if is_ignored "$VMID"; then
continue
fi
ensure_hookscript "lxc" "$VMID"
done
EOF
chmod +x "$APPLICATOR_FILE"
msg_ok "Created applicator script"
}
create_systemd_units() {
msg_info "Creating systemd watcher and service units"
cat <<'EOF' >/etc/systemd/system/pve-auto-hook.path
[Unit]
Description=Watch for new Proxmox guest configs to apply hookscript
[Path]
PathExistsGlob=/etc/pve/qemu-server/*.conf
PathExistsGlob=/etc/pve/lxc/*.conf
Unit=pve-auto-hook.service
[Install]
WantedBy=multi-user.target
EOF
cat <<'EOF' >/etc/systemd/system/pve-auto-hook.service
[Unit]
Description=Automatically add hookscript to new Proxmox guests
[Service]
Type=oneshot
ExecStart=/usr/local/bin/pve-apply-hookscript.sh
EOF
chmod 0644 "$PATH_UNIT_FILE" "$SERVICE_UNIT_FILE"
msg_ok "Created systemd units"
}
remove_hookscript_assignments() {
msg_info "Removing hookscript assignment from guests using dependency-check"
qm list | awk 'NR>1 {print $1}' | while read -r vmid; do
current_hook=$(qm config "$vmid" | awk '/^hookscript:/ {print $2}')
if [ "$current_hook" = "$HOOKSCRIPT_VOLUME_ID" ]; then
qm set "$vmid" --delete hookscript >/dev/null 2>&1 && msg_ok "Removed hookscript from VM $vmid"
fi
done
pct list | awk 'NR>1 {print $1}' | while read -r vmid; do
current_hook=$(pct config "$vmid" | awk '/^hookscript:/ {print $2}')
if [ "$current_hook" = "$HOOKSCRIPT_VOLUME_ID" ]; then
pct set "$vmid" --delete hookscript >/dev/null 2>&1 && msg_ok "Removed hookscript from LXC $vmid"
fi
done
}
install_stack() {
create_dependency_hookscript
create_exclusion_config
create_applicator_script
create_systemd_units
msg_info "Reloading systemd and enabling watcher"
if systemctl daemon-reload && systemctl enable --now pve-auto-hook.path >/dev/null 2>&1; then
msg_ok "Systemd watcher enabled and running"
else
msg_error "Could not enable pve-auto-hook.path"
exit 1
fi
msg_info "Performing initial run to update existing guests"
if "$APPLICATOR_FILE" >/dev/null 2>&1; then
msg_ok "Initial run complete"
else
msg_error "Initial run failed"
exit 1
fi
}
uninstall_stack() {
remove_hookscript_assignments
msg_info "Stopping and disabling systemd units"
systemctl disable --now pve-auto-hook.path >/dev/null 2>&1 || true
systemctl disable --now pve-auto-hook.service >/dev/null 2>&1 || true
msg_info "Removing installed files"
rm -f "$HOOKSCRIPT_FILE" "$APPLICATOR_FILE" "$PATH_UNIT_FILE" "$SERVICE_UNIT_FILE" "$CONFIG_FILE"
if systemctl daemon-reload >/dev/null 2>&1; then
msg_ok "systemd daemon reloaded"
else
msg_error "Failed to reload systemd daemon"
exit 1
fi
msg_ok "Dependency-check stack successfully removed"
}
show_status() {
echo -e "\n${BL}Dependency-check status${CL}"
echo -e "--------------------------------"
[ -f "$HOOKSCRIPT_FILE" ] && echo -e "Hookscript file: ${GN}present${CL}" || echo -e "Hookscript file: ${RD}missing${CL}"
[ -f "$APPLICATOR_FILE" ] && echo -e "Applicator script: ${GN}present${CL}" || echo -e "Applicator script: ${RD}missing${CL}"
[ -f "$CONFIG_FILE" ] && echo -e "Config file: ${GN}present${CL}" || echo -e "Config file: ${RD}missing${CL}"
[ -f "$PATH_UNIT_FILE" ] && echo -e "Path unit: ${GN}present${CL}" || echo -e "Path unit: ${RD}missing${CL}"
[ -f "$SERVICE_UNIT_FILE" ] && echo -e "Service unit: ${GN}present${CL}" || echo -e "Service unit: ${RD}missing${CL}"
if systemctl is-enabled pve-auto-hook.path >/dev/null 2>&1; then
echo -e "Watcher enabled: ${GN}yes${CL}"
else
echo -e "Watcher enabled: ${YW}no${CL}"
fi
if systemctl is-active pve-auto-hook.path >/dev/null 2>&1; then
echo -e "Watcher active: ${GN}yes${CL}"
else
echo -e "Watcher active: ${YW}no${CL}"
fi
}
header_info
ensure_supported_pve
case "${1:---install}" in
--help | -h)
print_usage
exit 0
;;
--status)
show_status
exit 0
;;
--install)
echo -e "\nThis script will install a service to automatically apply a"
echo -e "dependency-checking hookscript to all new and existing Proxmox guests."
echo -e "${YW}This includes creating files in:${CL}"
echo -e " - /var/lib/vz/snippets/"
echo -e " - /usr/local/bin/"
echo -e " - /etc/default/"
echo -e " - /etc/systemd/system/\n"
if ! confirm_action "Do you want to proceed with the installation?"; then
msg_error "Installation cancelled"
exit 1
fi
echo ""
install_stack
echo -e "\n${GN}Installation successful!${CL}"
echo -e "The service is now active and will monitor for new guests."
echo -e "To ${YW}exclude${CL} a VM or LXC, add its ID to ${YW}IGNORE_IDS${CL} in:"
echo -e " ${YW}${CONFIG_FILE}${CL}"
echo -e "\nMonitor activity with:"
echo -e " ${YW}journalctl -fu pve-auto-hook.service${CL}\n"
;;
--uninstall)
echo -e "\nThis will completely remove the dependency-check stack:"
echo -e " - hookscript and applicator"
echo -e " - systemd path/service units"
echo -e " - exclusion config"
echo -e " - hookscript assignment from guests using ${HOOKSCRIPT_VOLUME_ID}\n"
if ! confirm_action "Do you want to proceed with uninstall?"; then
msg_error "Uninstall cancelled"
exit 1
fi
echo ""
uninstall_stack
;;
*)
msg_error "Unknown option: $1"
print_usage
exit 1
;;
esac
exit 0