364 lines
11 KiB
Bash
364 lines
11 KiB
Bash
#!/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
|