ProxmoxVED/tools/pve/dependency-check.sh
2025-08-20 21:06:59 -03:00

364 lines
11 KiB
Bash
Raw Permalink Blame History

This file contains ambiguous Unicode characters

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 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