diff --git a/frontend/public/json/dependency-check.json b/frontend/public/json/dependency-check.json index 5f9307da1..22f08ae79 100644 --- a/frontend/public/json/dependency-check.json +++ b/frontend/public/json/dependency-check.json @@ -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_` where `` 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" @@ -45,4 +49,4 @@ "type": "info" } ] -} +} \ No newline at end of file diff --git a/tools/pve/dependency-check copy.sh b/tools/pve/dependency-check copy.sh new file mode 100644 index 000000000..d0a328912 --- /dev/null +++ b/tools/pve/dependency-check copy.sh @@ -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 diff --git a/tools/pve/dependency-check.sh b/tools/pve/dependency-check.sh index b7798d95a..36418dbc1 100644 --- a/tools/pve/dependency-check.sh +++ b/tools/pve/dependency-check.sh @@ -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 </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/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__. 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