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.
This commit is contained in:
CanbiZ (MickLesk) 2026-02-17 10:23:26 +01:00
parent 3eaa0ecf10
commit 6c43b624c1
3 changed files with 722 additions and 211 deletions

View File

@ -23,7 +23,7 @@
"ram": null,
"hdd": null,
"os": null,
"version": null
"version": "PVE 8.x / 9.x"
}
}
],
@ -36,6 +36,10 @@
"text": "Execute within the Proxmox shell",
"type": "info"
},
{
"text": "The script supports --install (default), --status and --uninstall for clean lifecycle management.",
"type": "info"
},
{
"text": "To wait until a certain host is available, tag the VM or container with `dep_ping_<hostname>` where `<hostname>` is the name or IP of the host to ping. The script will wait until the host is reachable before proceeding with the startup.",
"type": "info"

View File

@ -0,0 +1,361 @@
#!/usr/bin/env bash
# Copyright (c) 2023 community-scripts ORG
# This script is designed to install the Proxmox Dependency Check Hookscript.
# It sets up a dependency-checking hookscript and automates its
# application to all new and existing guests using a systemd watcher.
# License: MIT
function header_info {
clear
cat <<"EOF"
____ _ ____ _ _
| _ \ ___ _ __ ___ _ __ __| | ___ _ __ ___ _ _ / ___| |__ ___ ___| | __
| | | |/ _ \ '_ \ / _ \ '_ \ / _` |/ _ \ '_ \ / __| | | | | | '_ \ / _ \/ __| |/ /
| |_| | __/ |_) | __/ | | | (_| | __/ | | | (__| |_| | |___| | | | __/ (__| <
|____/ \___| .__/ \___|_| |_|\__,_|\___|_| |_|\___|\__, |\____|_| |_|\___|\___|_|\_\
|_| |___/
EOF
}
# Color variables
YW=$(echo "\033[33m")
GN=$(echo "\033[1;92m")
RD=$(echo "\033[01;31m")
CL=$(echo "\033[m")
BFR="\\r\\033[K"
HOLD=" "
CM="${GN}${CL}"
CROSS="${RD}${CL}"
# Spinner for progress indication (simplified)
spinner() {
local pid=$!
local delay=0.1
local spinstr='|/-\'
while [ "$(ps a | awk '{print $1}' | grep $pid)" ]; do
local temp=${spinstr#?}
printf " [%c] " "$spinstr"
local spinstr=$temp${spinstr%"$temp"}
sleep $delay
printf "\b\b\b\b\b\b"
done
printf " \b\b\b\b"
}
# Message functions
msg_info() {
echo -ne " ${YW}${CL} $1..."
}
msg_ok() {
echo -e "${BFR} ${CM} $1${CL}"
}
msg_error() {
echo -e "${BFR} ${CROSS} $1${CL}"
}
# --- End of base script functions ---
# --- Installation Functions ---
# Function to create the actual hookscript that runs before guest startup
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
# --- Configuration ---
POLL_INTERVAL=5 # Seconds to wait between checks
MAX_ATTEMPTS=60 # Max number of attempts before failing (60 * 5s = 5 minutes)
# --- End Configuration ---
VMID=$1
PHASE=$2
# Function for logging to syslog with a consistent format
log() {
echo "[hookscript-dep-check] VMID $VMID: $1"
}
# This script only runs in the 'pre-start' phase
if [ "$PHASE" != "pre-start" ]; then
exit 0
fi
log "--- Starting Pre-Start Dependency Check ---"
# --- Determine Guest Type (QEMU or LXC) ---
GUEST_TYPE=""
CONFIG_CMD=""
if qm config "$VMID" >/dev/null 2>&1; then
GUEST_TYPE="qemu"
CONFIG_CMD="qm config"
log "Guest type is QEMU (VM)."
elif pct config "$VMID" >/dev/null 2>&1; then
GUEST_TYPE="lxc"
CONFIG_CMD="pct config"
log "Guest type is LXC (Container)."
else
log "ERROR: Could not determine guest type for $VMID. Aborting."
exit 1
fi
GUEST_CONFIG=$($CONFIG_CMD "$VMID")
# --- 1. Storage Availability Check ---
log "Checking storage availability..."
# Grep for all disk definitions (scsi, sata, virtio, ide, rootfs, mp)
# and extract the storage identifier (the field between the colons).
# Sort -u gets the unique list of storage pools.
STORAGE_IDS=$(echo "$GUEST_CONFIG" | grep -E '^(scsi|sata|virtio|ide|rootfs|mp)[0-9]*:' | awk -F'[:]' '{print $2}' | awk '{print$1}' | sort -u)
if [ -z "$STORAGE_IDS" ]; then
log "No storage dependencies found to check."
else
for STORAGE_ID in $STORAGE_IDS; do
log "Checking status of storage: '$STORAGE_ID'"
ATTEMPTS=0
while true; do
# Grep for the storage ID line in pvesm status and check the 'Active' column (3rd column)
STATUS=$(pvesm status | grep "^\s*$STORAGE_ID\s" | awk '{print $3}')
if [ "$STATUS" == "active" ]; then
log "Storage '$STORAGE_ID' is active."
break
fi
ATTEMPTS=$((ATTEMPTS + 1))
if [ $ATTEMPTS -ge $MAX_ATTEMPTS ]; then
log "ERROR: Timeout waiting for storage '$STORAGE_ID' to become active. Aborting start."
exit 1
fi
log "Storage '$STORAGE_ID' is not active (current status: '${STATUS:-inactive/unknown}'). Waiting ${POLL_INTERVAL}s... (Attempt ${ATTEMPTS}/${MAX_ATTEMPTS})"
sleep $POLL_INTERVAL
done
done
fi
log "All storage dependencies are met."
# --- 2. Custom Tag-Based Dependency Check ---
log "Checking for custom tag-based dependencies..."
TAGS=$(echo "$GUEST_CONFIG" | grep '^tags:' | awk '{print $2}')
if [ -z "$TAGS" ]; then
log "No tags found. Skipping custom dependency check."
else
# Replace colons with spaces to loop through tags
for TAG in ${TAGS//;/ }; do
# Check if the tag matches our dependency format 'dep_*'
if [[ $TAG == dep_* ]]; then
log "Found dependency tag: '$TAG'"
# Split tag into parts using underscore as delimiter
IFS='_' read -ra PARTS <<< "$TAG"
DEP_TYPE="${PARTS[1]}"
ATTEMPTS=0
while true; do
CHECK_PASSED=false
case "$DEP_TYPE" in
"tcp")
HOST="${PARTS[2]}"
PORT="${PARTS[3]}"
if [ -z "$HOST" ] || [ -z "$PORT" ]; then
log "ERROR: Malformed TCP dependency tag '$TAG'. Skipping."
CHECK_PASSED=true # Skip to avoid infinite loop
# nc -z is great for this. -w sets a timeout.
elif nc -z -w 2 "$HOST" "$PORT"; then
log "TCP dependency met: Host $HOST port $PORT is open."
CHECK_PASSED=true
fi
;;
"ping")
HOST="${PARTS[2]}"
if [ -z "$HOST" ]; then
log "ERROR: Malformed PING dependency tag '$TAG'. Skipping."
CHECK_PASSED=true # Skip to avoid infinite loop
# ping -c 1 (one packet) -W 2 (2-second timeout)
elif ping -c 1 -W 2 "$HOST" >/dev/null 2>&1; then
log "Ping dependency met: Host $HOST is reachable."
CHECK_PASSED=true
fi
;;
*)
log "WARNING: Unknown dependency type '$DEP_TYPE' in tag '$TAG'. Ignoring."
CHECK_PASSED=true # Mark as passed to avoid getting stuck
;;
esac
if $CHECK_PASSED; then
break
fi
ATTEMPTS=$((ATTEMPTS + 1))
if [ $ATTEMPTS -ge $MAX_ATTEMPTS ]; then
log "ERROR: Timeout waiting for dependency '$TAG'. Aborting start."
exit 1
fi
log "Dependency '$TAG' not met. Waiting ${POLL_INTERVAL}s... (Attempt ${ATTEMPTS}/${MAX_ATTEMPTS})"
sleep $POLL_INTERVAL
done
fi
done
fi
log "All custom dependencies are met."
log "--- Dependency Check Complete. Proceeding with start. ---"
exit 0
EOF
chmod +x /var/lib/vz/snippets/dependency-check.sh
msg_ok "Created dependency-check hookscript"
}
# Function to create the config file for exclusions
create_exclusion_config() {
msg_info "Creating exclusion configuration file"
if [ -f /etc/default/pve-auto-hook ]; 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
msg_ok "Created exclusion configuration file"
fi
}
# Function to create the script that applies the hook
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-list"
log() {
systemd-cat -t "$LOG_TAG" <<< "$1"
}
if [ -f "$CONFIG_FILE" ]; then
source "$CONFIG_FILE"
fi
# Process QEMU VMs
qm list | awk 'NR>1 {print $1}' | while read -r VMID; do
is_ignored=false
for id_to_ignore in $IGNORE_IDS; do
if [ "$id_to_ignore" == "$VMID" ]; then is_ignored=true; break; fi
done
if $is_ignored; then continue; fi
if qm config "$VMID" | grep -q '^hookscript:'; then continue; fi
log "Hookscript not found for VM $VMID. Applying..."
qm set "$VMID" --hookscript "$HOOKSCRIPT_VOLUME_ID"
done
# Process LXC Containers
pct list | awk 'NR>1 {print $1}' | while read -r VMID; do
is_ignored=false
for id_to_ignore in $IGNORE_IDS; do
if [ "$id_to_ignore" == "$VMID" ]; then is_ignored=true; break; fi
done
if $is_ignored; then continue; fi
if pct config "$VMID" | grep -q '^hookscript:'; then continue; fi
log "Hookscript not found for LXC $VMID. Applying..."
pct set "$VMID" --hookscript "$HOOKSCRIPT_VOLUME_ID"
done
EOF
chmod +x /usr/local/bin/pve-apply-hookscript.sh
msg_ok "Created applicator script"
}
# Function to set up the systemd watcher and service
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]
PathModified=/etc/pve/qemu-server/
PathModified=/etc/pve/lxc/
[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
msg_ok "Created systemd units"
}
# --- Main Execution ---
header_info
if ! command -v pveversion >/dev/null 2>&1; then
msg_error "This script must be run on a Proxmox VE host."
exit 1
fi
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"
read -p "Do you want to proceed with the installation? (y/n): " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
msg_error "Installation cancelled."
exit 1
fi
echo -e "\n"
create_dependency_hookscript
create_exclusion_config
create_applicator_script
create_systemd_units
msg_info "Reloading systemd and enabling the watcher"
(systemctl daemon-reload && systemctl enable --now pve-auto-hook.path) >/dev/null 2>&1 &
spinner
msg_ok "Systemd watcher enabled and running"
msg_info "Performing initial run to update existing guests"
/usr/local/bin/pve-apply-hookscript.sh >/dev/null 2>&1 &
spinner
msg_ok "Initial run complete"
echo -e "\n\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 the ${YW}IGNORE_IDS${CL} variable in:"
echo -e " ${YW}/etc/default/pve-auto-hook${CL}"
echo -e "\nYou can monitor the service's activity with:"
echo -e " ${YW}journalctl -fu pve-auto-hook.service${CL}\n"
exit 0

View File

@ -1,10 +1,9 @@
#!/usr/bin/env bash
# Copyright (c) 2023 community-scripts ORG
# This script is designed to install the Proxmox Dependency Check Hookscript.
# It sets up a dependency-checking hookscript and automates its
# application to all new and existing guests using a systemd watcher.
# License: MIT
# 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
@ -18,195 +17,220 @@ function header_info {
EOF
}
# Color variables
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"
HOLD=" "
CM="${GN}${CL}"
CROSS="${RD}${CL}"
CM="${GN}✔️${CL}"
CROSS="${RD}✖️${CL}"
INFO="${BL}${CL}"
# Spinner for progress indication (simplified)
spinner() {
local pid=$!
local delay=0.1
local spinstr='|/-\'
while [ "$(ps a | awk '{print $1}' | grep $pid)" ]; do
local temp=${spinstr#?}
printf " [%c] " "$spinstr"
local spinstr=$temp${spinstr%"$temp"}
sleep $delay
printf "\b\b\b\b\b\b"
done
printf " \b\b\b\b"
}
# Message functions
msg_info() {
echo -ne " ${YW}${CL} $1..."
local msg="$1"
echo -e "${INFO} ${YW}${msg}...${CL}"
}
msg_ok() {
echo -e "${BFR} ${CM} $1${CL}"
local msg="$1"
echo -e "${BFR} ${CM} ${GN}${msg}${CL}"
}
msg_error() {
echo -e "${BFR} ${CROSS} $1${CL}"
local msg="$1"
echo -e "${BFR} ${CROSS} ${RD}${msg}${CL}"
}
# --- End of base script functions ---
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"
# --- Installation Functions ---
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]$ ]]
}
# Function to create the actual hookscript that runs before guest startup
create_dependency_hookscript() {
msg_info "Creating dependency-check hookscript"
mkdir -p /var/lib/vz/snippets
cat <<'EOF' > /var/lib/vz/snippets/dependency-check.sh
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
# --- Configuration ---
POLL_INTERVAL=5 # Seconds to wait between checks
MAX_ATTEMPTS=60 # Max number of attempts before failing (60 * 5s = 5 minutes)
# --- End Configuration ---
VMID=$1
PHASE=$2
# Function for logging to syslog with a consistent format
log() {
echo "[hookscript-dep-check] VMID $VMID: $1"
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
}
# This script only runs in the 'pre-start' phase
if [ "$PHASE" != "pre-start" ]; then
exit 0
fi
log "--- Starting Pre-Start Dependency Check ---"
# --- Determine Guest Type (QEMU or LXC) ---
GUEST_TYPE=""
CONFIG_CMD=""
if qm config "$VMID" >/dev/null 2>&1; then
GUEST_TYPE="qemu"
CONFIG_CMD="qm config"
CONFIG_CMD=(qm config "$VMID")
log "Guest type is QEMU (VM)."
elif pct config "$VMID" >/dev/null 2>&1; then
GUEST_TYPE="lxc"
CONFIG_CMD="pct config"
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 "$VMID")
GUEST_CONFIG=$("${CONFIG_CMD[@]}")
# --- 1. Storage Availability Check ---
log "Checking storage availability..."
# Grep for all disk definitions (scsi, sata, virtio, ide, rootfs, mp)
# and extract the storage identifier (the field between the colons).
# Sort -u gets the unique list of storage pools.
STORAGE_IDS=$(echo "$GUEST_CONFIG" | grep -E '^(scsi|sata|virtio|ide|rootfs|mp)[0-9]*:' | awk -F'[:]' '{print $2}' | awk '{print$1}' | sort -u)
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
log "Checking status of storage: '$STORAGE_ID'"
ATTEMPTS=0
while true; do
# Grep for the storage ID line in pvesm status and check the 'Active' column (3rd column)
STATUS=$(pvesm status | grep "^\s*$STORAGE_ID\s" | awk '{print $3}')
if [ "$STATUS" == "active" ]; then
log "Storage '$STORAGE_ID' is active."
break
fi
STATUS=$(pvesm status 2>/dev/null | awk -v id="$STORAGE_ID" '$1 == id { print $3; exit }')
ATTEMPTS=$((ATTEMPTS + 1))
if [ $ATTEMPTS -ge $MAX_ATTEMPTS ]; then
log "ERROR: Timeout waiting for storage '$STORAGE_ID' to become active. Aborting start."
exit 1
fi
if [ -z "$STATUS" ]; then
log "WARNING: Storage '$STORAGE_ID' not found in 'pvesm status'. Skipping this dependency."
continue
fi
log "Storage '$STORAGE_ID' is not active (current status: '${STATUS:-inactive/unknown}'). Waiting ${POLL_INTERVAL}s... (Attempt ${ATTEMPTS}/${MAX_ATTEMPTS})"
sleep $POLL_INTERVAL
done
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."
# --- 2. Custom Tag-Based Dependency Check ---
log "Checking for custom tag-based dependencies..."
TAGS=$(echo "$GUEST_CONFIG" | grep '^tags:' | awk '{print $2}')
TAGS=$(echo "$GUEST_CONFIG" | awk -F': ' '/^tags:/ {print $2}')
if [ -z "$TAGS" ]; then
log "No tags found. Skipping custom dependency check."
else
# Replace colons with spaces to loop through tags
for TAG in ${TAGS//;/ }; do
# Check if the tag matches our dependency format 'dep_*'
if [[ $TAG == dep_* ]]; then
log "Found dependency tag: '$TAG'"
# Split tag into parts using underscore as delimiter
IFS='_' read -ra PARTS <<< "$TAG"
DEP_TYPE="${PARTS[1]}"
IFS='_' read -r _ DEP_TYPE HOST PORT EXTRA <<< "$TAG"
ATTEMPTS=0
while true; do
CHECK_PASSED=false
case "$DEP_TYPE" in
"tcp")
HOST="${PARTS[2]}"
PORT="${PARTS[3]}"
if [ -z "$HOST" ] || [ -z "$PORT" ]; then
log "ERROR: Malformed TCP dependency tag '$TAG'. Skipping."
CHECK_PASSED=true # Skip to avoid infinite loop
# nc -z is great for this. -w sets a timeout.
elif nc -z -w 2 "$HOST" "$PORT"; then
log "TCP dependency met: Host $HOST port $PORT is open."
CHECK_PASSED=true
fi
;;
"ping")
HOST="${PARTS[2]}"
if [ -z "$HOST" ]; then
log "ERROR: Malformed PING dependency tag '$TAG'. Skipping."
CHECK_PASSED=true # Skip to avoid infinite loop
# ping -c 1 (one packet) -W 2 (2-second timeout)
elif ping -c 1 -W 2 "$HOST" >/dev/null 2>&1; then
log "Ping dependency met: Host $HOST is reachable."
CHECK_PASSED=true
fi
;;
*)
log "WARNING: Unknown dependency type '$DEP_TYPE' in tag '$TAG'. Ignoring."
CHECK_PASSED=true # Mark as passed to avoid getting stuck
;;
esac
if $CHECK_PASSED; then
break
fi
ATTEMPTS=$((ATTEMPTS + 1))
if [ $ATTEMPTS -ge $MAX_ATTEMPTS ]; then
log "ERROR: Timeout waiting for dependency '$TAG'. Aborting start."
exit 1
fi
log "Dependency '$TAG' not met. Waiting ${POLL_INTERVAL}s... (Attempt ${ATTEMPTS}/${MAX_ATTEMPTS})"
sleep $POLL_INTERVAL
done
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
@ -215,17 +239,16 @@ log "All custom dependencies are met."
log "--- Dependency Check Complete. Proceeding with start. ---"
exit 0
EOF
chmod +x /var/lib/vz/snippets/dependency-check.sh
msg_ok "Created dependency-check hookscript"
chmod +x "$HOOKSCRIPT_FILE"
msg_ok "Created dependency-check hookscript"
}
# Function to create the config file for exclusions
create_exclusion_config() {
msg_info "Creating exclusion configuration file"
if [ -f /etc/default/pve-auto-hook ]; then
msg_ok "Exclusion file already exists, skipping."
else
cat <<'EOF' > /etc/default/pve-auto-hook
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
#
@ -238,71 +261,99 @@ create_exclusion_config() {
IGNORE_IDS=""
EOF
msg_ok "Created exclusion configuration file"
fi
chmod 0644 "$CONFIG_FILE"
msg_ok "Created exclusion configuration file"
fi
}
# Function to create the script that applies the hook
create_applicator_script() {
msg_info "Creating the hookscript applicator script"
cat <<'EOF' > /usr/local/bin/pve-apply-hookscript.sh
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-list"
LOG_TAG="pve-auto-hook"
IGNORE_IDS=""
log() {
systemd-cat -t "$LOG_TAG" <<< "$1"
}
if [ -f "$CONFIG_FILE" ]; then
source "$CONFIG_FILE"
IGNORE_IDS=$(grep -E '^IGNORE_IDS=' "$CONFIG_FILE" | head -n1 | cut -d'=' -f2- | tr -d '"')
fi
# Process QEMU VMs
qm list | awk 'NR>1 {print $1}' | while read -r VMID; do
is_ignored=false
is_ignored() {
local vmid="$1"
for id_to_ignore in $IGNORE_IDS; do
if [ "$id_to_ignore" == "$VMID" ]; then is_ignored=true; break; fi
if [ "$id_to_ignore" = "$vmid" ]; then
return 0
fi
done
if $is_ignored; then continue; fi
if qm config "$VMID" | grep -q '^hookscript:'; then continue; fi
log "Hookscript not found for VM $VMID. Applying..."
qm set "$VMID" --hookscript "$HOOKSCRIPT_VOLUME_ID"
done
# Process LXC Containers
pct list | awk 'NR>1 {print $1}' | while read -r VMID; do
is_ignored=false
for id_to_ignore in $IGNORE_IDS; do
if [ "$id_to_ignore" == "$VMID" ]; then is_ignored=true; break; fi
done
if $is_ignored; then continue; fi
if pct config "$VMID" | grep -q '^hookscript:'; then continue; fi
log "Hookscript not found for LXC $VMID. Applying..."
pct set "$VMID" --hookscript "$HOOKSCRIPT_VOLUME_ID"
done
EOF
chmod +x /usr/local/bin/pve-apply-hookscript.sh
msg_ok "Created applicator script"
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"
}
# Function to set up the systemd watcher and service
create_systemd_units() {
msg_info "Creating systemd watcher and service units"
cat <<'EOF' > /etc/systemd/system/pve-auto-hook.path
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]
PathModified=/etc/pve/qemu-server/
PathModified=/etc/pve/lxc/
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
cat <<'EOF' >/etc/systemd/system/pve-auto-hook.service
[Unit]
Description=Automatically add hookscript to new Proxmox guests
@ -310,54 +361,149 @@ Description=Automatically add hookscript to new Proxmox guests
Type=oneshot
ExecStart=/usr/local/bin/pve-apply-hookscript.sh
EOF
msg_ok "Created systemd units"
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
}
# --- Main Execution ---
header_info
ensure_supported_pve
if ! command -v pveversion >/dev/null 2>&1; then
msg_error "This script must be run on a Proxmox VE host."
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
fi
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"
echo ""
install_stack
read -p "Do you want to proceed with the installation? (y/n): " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
msg_error "Installation cancelled."
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
fi
echo -e "\n"
create_dependency_hookscript
create_exclusion_config
create_applicator_script
create_systemd_units
msg_info "Reloading systemd and enabling the watcher"
(systemctl daemon-reload && systemctl enable --now pve-auto-hook.path) >/dev/null 2>&1 &
spinner
msg_ok "Systemd watcher enabled and running"
msg_info "Performing initial run to update existing guests"
/usr/local/bin/pve-apply-hookscript.sh >/dev/null 2>&1 &
spinner
msg_ok "Initial run complete"
echo -e "\n\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 the ${YW}IGNORE_IDS${CL} variable in:"
echo -e " ${YW}/etc/default/pve-auto-hook${CL}"
echo -e "\nYou can monitor the service's activity with:"
echo -e " ${YW}journalctl -fu pve-auto-hook.service${CL}\n"
echo ""
uninstall_stack
;;
*)
msg_error "Unknown option: $1"
print_usage
exit 1
;;
esac
exit 0