Merge pull request #792 from kamushadenes/feat/dependency-check

feat: add dependency-check
This commit is contained in:
CanbiZ
2025-09-02 08:33:43 +02:00
committed by GitHub
2 changed files with 411 additions and 0 deletions

View File

@@ -0,0 +1,363 @@
#!/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