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, "ram": null,
"hdd": null, "hdd": null,
"os": null, "os": null,
"version": null "version": "PVE 8.x / 9.x"
} }
} }
], ],
@ -36,6 +36,10 @@
"text": "Execute within the Proxmox shell", "text": "Execute within the Proxmox shell",
"type": "info" "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.", "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" "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 #!/usr/bin/env bash
# Copyright (c) 2023 community-scripts ORG # Copyright (c) 2023-2026 community-scripts ORG
# This script is designed to install the Proxmox Dependency Check Hookscript. # Author: MickLesk | Maintainer: community-scripts
# It sets up a dependency-checking hookscript and automates its # License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# application to all new and existing guests using a systemd watcher. # Source: https://www.proxmox.com/
# License: MIT
function header_info { function header_info {
clear clear
@ -18,195 +17,220 @@ function header_info {
EOF EOF
} }
# Color variables
YW=$(echo "\033[33m") YW=$(echo "\033[33m")
GN=$(echo "\033[1;92m") GN=$(echo "\033[1;92m")
RD=$(echo "\033[01;31m") RD=$(echo "\033[01;31m")
BL=$(echo "\033[36m")
CL=$(echo "\033[m") CL=$(echo "\033[m")
BFR="\\r\\033[K" BFR="\\r\\033[K"
HOLD=" " CM="${GN}✔️${CL}"
CM="${GN}${CL}" CROSS="${RD}✖️${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() { msg_info() {
echo -ne " ${YW}${CL} $1..." local msg="$1"
echo -e "${INFO} ${YW}${msg}...${CL}"
} }
msg_ok() { msg_ok() {
echo -e "${BFR} ${CM} $1${CL}" local msg="$1"
echo -e "${BFR} ${CM} ${GN}${msg}${CL}"
} }
msg_error() { 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() { create_dependency_hookscript() {
msg_info "Creating dependency-check hookscript" msg_info "Creating dependency-check hookscript"
mkdir -p /var/lib/vz/snippets mkdir -p /var/lib/vz/snippets
cat <<'EOF' > /var/lib/vz/snippets/dependency-check.sh cat <<'EOF' >/var/lib/vz/snippets/dependency-check.sh
#!/bin/bash #!/bin/bash
# Proxmox Hookscript for Pre-Start Dependency Checking # Proxmox Hookscript for Pre-Start Dependency Checking
# Works for both QEMU VMs and LXC Containers # Works for both QEMU VMs and LXC Containers
# --- Configuration ---
POLL_INTERVAL=5 # Seconds to wait between checks POLL_INTERVAL=5 # Seconds to wait between checks
MAX_ATTEMPTS=60 # Max number of attempts before failing (60 * 5s = 5 minutes) MAX_ATTEMPTS=60 # Max number of attempts before failing (60 * 5s = 5 minutes)
# --- End Configuration ---
VMID=$1 VMID=$1
PHASE=$2 PHASE=$2
# Function for logging to syslog with a consistent format
log() { 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 if [ "$PHASE" != "pre-start" ]; then
exit 0 exit 0
fi fi
log "--- Starting Pre-Start Dependency Check ---" 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 if qm config "$VMID" >/dev/null 2>&1; then
GUEST_TYPE="qemu" CONFIG_CMD=(qm config "$VMID")
CONFIG_CMD="qm config"
log "Guest type is QEMU (VM)." log "Guest type is QEMU (VM)."
elif pct config "$VMID" >/dev/null 2>&1; then elif pct config "$VMID" >/dev/null 2>&1; then
GUEST_TYPE="lxc" CONFIG_CMD=(pct config "$VMID")
CONFIG_CMD="pct config"
log "Guest type is LXC (Container)." log "Guest type is LXC (Container)."
else else
log "ERROR: Could not determine guest type for $VMID. Aborting." log "ERROR: Could not determine guest type for $VMID. Aborting."
exit 1 exit 1
fi fi
GUEST_CONFIG=$($CONFIG_CMD "$VMID") GUEST_CONFIG=$("${CONFIG_CMD[@]}")
# --- 1. Storage Availability Check ---
log "Checking storage availability..." log "Checking storage availability..."
# Grep for all disk definitions (scsi, sata, virtio, ide, rootfs, mp) STORAGE_IDS=$(echo "$GUEST_CONFIG" | awk -F':' '
# and extract the storage identifier (the field between the colons). /^(scsi|sata|virtio|ide|efidisk|tpmstate|unused|rootfs|mp)[0-9]*:/ {
# Sort -u gets the unique list of storage pools. val=$2
STORAGE_IDS=$(echo "$GUEST_CONFIG" | grep -E '^(scsi|sata|virtio|ide|rootfs|mp)[0-9]*:' | awk -F'[:]' '{print $2}' | awk '{print$1}' | sort -u) 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 if [ -z "$STORAGE_IDS" ]; then
log "No storage dependencies found to check." log "No storage dependencies found to check."
else else
for STORAGE_ID in $STORAGE_IDS; do for STORAGE_ID in $STORAGE_IDS; do
log "Checking status of storage: '$STORAGE_ID'" STATUS=$(pvesm status 2>/dev/null | awk -v id="$STORAGE_ID" '$1 == id { print $3; exit }')
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 [ -z "$STATUS" ]; then
if [ $ATTEMPTS -ge $MAX_ATTEMPTS ]; then log "WARNING: Storage '$STORAGE_ID' not found in 'pvesm status'. Skipping this dependency."
log "ERROR: Timeout waiting for storage '$STORAGE_ID' to become active. Aborting start." continue
exit 1 fi
fi
log "Storage '$STORAGE_ID' is not active (current status: '${STATUS:-inactive/unknown}'). Waiting ${POLL_INTERVAL}s... (Attempt ${ATTEMPTS}/${MAX_ATTEMPTS})" 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
sleep $POLL_INTERVAL
done
done done
fi fi
log "All storage dependencies are met." log "All storage dependencies are met."
# --- 2. Custom Tag-Based Dependency Check ---
log "Checking for custom tag-based dependencies..." 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 if [ -z "$TAGS" ]; then
log "No tags found. Skipping custom dependency check." log "No tags found. Skipping custom dependency check."
else else
# Replace colons with spaces to loop through tags
for TAG in ${TAGS//;/ }; do for TAG in ${TAGS//;/ }; do
# Check if the tag matches our dependency format 'dep_*'
if [[ $TAG == dep_* ]]; then if [[ $TAG == dep_* ]]; then
log "Found dependency tag: '$TAG'" log "Found dependency tag: '$TAG'"
# Split tag into parts using underscore as delimiter IFS='_' read -r _ DEP_TYPE HOST PORT EXTRA <<< "$TAG"
IFS='_' read -ra PARTS <<< "$TAG"
DEP_TYPE="${PARTS[1]}"
ATTEMPTS=0 case "$DEP_TYPE" in
while true; do ping)
CHECK_PASSED=false if [ -z "$HOST" ]; then
case "$DEP_TYPE" in log "WARNING: Malformed ping dependency tag '$TAG'. Ignoring."
"tcp") continue
HOST="${PARTS[2]}" fi
PORT="${PARTS[3]}" wait_until "Ping dependency met: Host $HOST is reachable." "ping -c 1 -W 2 \"$HOST\" >/dev/null 2>&1" || exit 1
if [ -z "$HOST" ] || [ -z "$PORT" ]; then ;;
log "ERROR: Malformed TCP dependency tag '$TAG'. Skipping." tcp)
CHECK_PASSED=true # Skip to avoid infinite loop if [ -z "$HOST" ] || [ -z "$PORT" ] || ! [[ "$PORT" =~ ^[0-9]+$ ]] || [ "$PORT" -lt 1 ] || [ "$PORT" -gt 65535 ]; then
# nc -z is great for this. -w sets a timeout. log "WARNING: Malformed TCP dependency tag '$TAG'. Expected dep_tcp_<host>_<port>. Ignoring."
elif nc -z -w 2 "$HOST" "$PORT"; then continue
log "TCP dependency met: Host $HOST port $PORT is open." fi
CHECK_PASSED=true wait_until "TCP dependency met: Host $HOST port $PORT is open." "check_tcp \"$HOST\" \"$PORT\"" || exit 1
fi ;;
;; *)
log "WARNING: Unknown dependency type '$DEP_TYPE' in tag '$TAG'. Ignoring."
"ping") ;;
HOST="${PARTS[2]}" esac
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 fi
done done
fi fi
@ -215,17 +239,16 @@ log "All custom dependencies are met."
log "--- Dependency Check Complete. Proceeding with start. ---" log "--- Dependency Check Complete. Proceeding with start. ---"
exit 0 exit 0
EOF EOF
chmod +x /var/lib/vz/snippets/dependency-check.sh chmod +x "$HOOKSCRIPT_FILE"
msg_ok "Created dependency-check hookscript" msg_ok "Created dependency-check hookscript"
} }
# Function to create the config file for exclusions
create_exclusion_config() { create_exclusion_config() {
msg_info "Creating exclusion configuration file" msg_info "Creating exclusion configuration file"
if [ -f /etc/default/pve-auto-hook ]; then if [ -f "$CONFIG_FILE" ]; then
msg_ok "Exclusion file already exists, skipping." msg_ok "Exclusion file already exists, skipping."
else else
cat <<'EOF' > /etc/default/pve-auto-hook cat <<'EOF' >/etc/default/pve-auto-hook
# #
# Configuration for the Proxmox Automatic Hookscript Applicator # Configuration for the Proxmox Automatic Hookscript Applicator
# #
@ -238,71 +261,99 @@ create_exclusion_config() {
IGNORE_IDS="" IGNORE_IDS=""
EOF EOF
msg_ok "Created exclusion configuration file" chmod 0644 "$CONFIG_FILE"
fi msg_ok "Created exclusion configuration file"
fi
} }
# Function to create the script that applies the hook
create_applicator_script() { create_applicator_script() {
msg_info "Creating the hookscript applicator script" msg_info "Creating the hookscript applicator script"
cat <<'EOF' > /usr/local/bin/pve-apply-hookscript.sh cat <<'EOF' >/usr/local/bin/pve-apply-hookscript.sh
#!/bin/bash #!/bin/bash
HOOKSCRIPT_VOLUME_ID="local:snippets/dependency-check.sh" HOOKSCRIPT_VOLUME_ID="local:snippets/dependency-check.sh"
CONFIG_FILE="/etc/default/pve-auto-hook" CONFIG_FILE="/etc/default/pve-auto-hook"
LOG_TAG="pve-auto-hook-list" LOG_TAG="pve-auto-hook"
IGNORE_IDS=""
log() { log() {
systemd-cat -t "$LOG_TAG" <<< "$1" systemd-cat -t "$LOG_TAG" <<< "$1"
} }
if [ -f "$CONFIG_FILE" ]; then if [ -f "$CONFIG_FILE" ]; then
source "$CONFIG_FILE" IGNORE_IDS=$(grep -E '^IGNORE_IDS=' "$CONFIG_FILE" | head -n1 | cut -d'=' -f2- | tr -d '"')
fi fi
# Process QEMU VMs is_ignored() {
qm list | awk 'NR>1 {print $1}' | while read -r VMID; do local vmid="$1"
is_ignored=false
for id_to_ignore in $IGNORE_IDS; do 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 done
if $is_ignored; then continue; fi return 1
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" ensure_hookscript() {
done local guest_type="$1"
local vmid="$2"
# Process LXC Containers local current_hook=""
pct list | awk 'NR>1 {print $1}' | while read -r VMID; do
is_ignored=false if [ "$guest_type" = "qemu" ]; then
for id_to_ignore in $IGNORE_IDS; do current_hook=$(qm config "$vmid" | awk '/^hookscript:/ {print $2}')
if [ "$id_to_ignore" == "$VMID" ]; then is_ignored=true; break; fi else
done current_hook=$(pct config "$vmid" | awk '/^hookscript:/ {print $2}')
if $is_ignored; then continue; fi fi
if pct config "$VMID" | grep -q '^hookscript:'; then continue; fi
log "Hookscript not found for LXC $VMID. Applying..." if [ -n "$current_hook" ]; then
pct set "$VMID" --hookscript "$HOOKSCRIPT_VOLUME_ID" if [ "$current_hook" = "$HOOKSCRIPT_VOLUME_ID" ]; then
done return 0
EOF fi
chmod +x /usr/local/bin/pve-apply-hookscript.sh log "Guest $guest_type/$vmid already has another hookscript ($current_hook). Leaving unchanged."
msg_ok "Created applicator script" 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() { create_systemd_units() {
msg_info "Creating systemd watcher and service units" msg_info "Creating systemd watcher and service units"
cat <<'EOF' > /etc/systemd/system/pve-auto-hook.path cat <<'EOF' >/etc/systemd/system/pve-auto-hook.path
[Unit] [Unit]
Description=Watch for new Proxmox guest configs to apply hookscript Description=Watch for new Proxmox guest configs to apply hookscript
[Path] [Path]
PathModified=/etc/pve/qemu-server/ PathExistsGlob=/etc/pve/qemu-server/*.conf
PathModified=/etc/pve/lxc/ PathExistsGlob=/etc/pve/lxc/*.conf
Unit=pve-auto-hook.service
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target
EOF EOF
cat <<'EOF' > /etc/systemd/system/pve-auto-hook.service cat <<'EOF' >/etc/systemd/system/pve-auto-hook.service
[Unit] [Unit]
Description=Automatically add hookscript to new Proxmox guests Description=Automatically add hookscript to new Proxmox guests
@ -310,54 +361,149 @@ Description=Automatically add hookscript to new Proxmox guests
Type=oneshot Type=oneshot
ExecStart=/usr/local/bin/pve-apply-hookscript.sh ExecStart=/usr/local/bin/pve-apply-hookscript.sh
EOF 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 header_info
ensure_supported_pve
if ! command -v pveversion >/dev/null 2>&1; then case "${1:---install}" in
msg_error "This script must be run on a Proxmox VE host." --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 exit 1
fi fi
echo -e "\nThis script will install a service to automatically apply a" echo ""
echo -e "dependency-checking hookscript to all new and existing Proxmox guests." install_stack
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 -e "\n${GN}Installation successful!${CL}"
echo echo -e "The service is now active and will monitor for new guests."
if [[ ! $REPLY =~ ^[Yy]$ ]]; then echo -e "To ${YW}exclude${CL} a VM or LXC, add its ID to ${YW}IGNORE_IDS${CL} in:"
msg_error "Installation cancelled." 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 exit 1
fi fi
echo -e "\n" echo ""
create_dependency_hookscript uninstall_stack
create_exclusion_config ;;
create_applicator_script *)
create_systemd_units msg_error "Unknown option: $1"
print_usage
msg_info "Reloading systemd and enabling the watcher" exit 1
(systemctl daemon-reload && systemctl enable --now pve-auto-hook.path) >/dev/null 2>&1 & ;;
spinner esac
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 exit 0