mirror of
https://github.com/community-scripts/ProxmoxVE.git
synced 2025-09-13 15:43:02 +00:00

- **Interactive Configuration**: Added setup wizard for `TAG_FORMAT` (last_two_octets/last_octet/full) and `LOOP_INTERVAL` (30-7200 seconds) during installation/update - **Unified Installation Menu**: Replaced conditional service detection with unified menu offering install-with-service, install-command-only, and update options - **Post-Installation Guidance**: Added comprehensive information display about configuration file location (`/opt/iptag/iptag.conf`) and required CIDR_LIST setup - **Improved VM IP Detection**: Enhanced IP discovery with QM guest agent priority, ARP table validation via ping, and automatic removal of stale ARP entries - **Visual Indicators**: Replaced emoji symbols with standard ASCII characters (✓/✗) with proper colors for better terminal compatibility - **Simplified Update Process**: Removed complex service detection logic and provided direct update option in main menu These changes address user feedback about complex installation requirements, VM IP detection issues, and provide clearer guidance for network subnet configuration.
1108 lines
33 KiB
Bash
1108 lines
33 KiB
Bash
#!/usr/bin/env bash
|
||
|
||
# Copyright (c) 2021-2025 community-scripts ORG
|
||
# Author: MickLesk (Canbiz) && Desert_Gamer
|
||
# License: MIT
|
||
|
||
function header_info {
|
||
clear
|
||
cat <<"EOF"
|
||
___ ____ _____
|
||
|_ _| _ \ _ |_ _|_ _ __ _
|
||
| || |_) (_) | |/ _` |/ _` |
|
||
| || __/ _ | | (_| | (_| |
|
||
|___|_| (_) |_|\__,_|\__, |
|
||
|___/
|
||
EOF
|
||
}
|
||
|
||
clear
|
||
header_info
|
||
APP="IP-Tag"
|
||
hostname=$(hostname)
|
||
|
||
# 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} "
|
||
|
||
# Stop any running spinner
|
||
stop_spinner() {
|
||
if [ -n "$SPINNER_PID" ] && kill -0 "$SPINNER_PID" 2>/dev/null; then
|
||
kill -TERM "$SPINNER_PID" 2>/dev/null
|
||
wait "$SPINNER_PID" 2>/dev/null
|
||
fi
|
||
SPINNER_PID=""
|
||
printf "\e[?25h\r"
|
||
}
|
||
|
||
# Error handler for displaying error messages
|
||
error_handler() {
|
||
stop_spinner
|
||
local exit_code="$?"
|
||
local line_number="$1"
|
||
local command="$2"
|
||
local error_message="${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}"
|
||
echo -e "\n$error_message\n"
|
||
}
|
||
|
||
# Spinner for progress indication
|
||
spinner() {
|
||
local msg="$1"
|
||
local frames=('⠋' '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏')
|
||
local spin_i=0
|
||
local interval=0.1
|
||
|
||
trap 'exit 0' TERM INT
|
||
printf "\e[?25l" 2>/dev/null
|
||
|
||
while true; do
|
||
printf "\r%s ${YW}%s${CL}" "${frames[spin_i]}" "$msg" 2>/dev/null || exit 0
|
||
spin_i=$(((spin_i + 1) % ${#frames[@]}))
|
||
sleep "$interval" || exit 0
|
||
done
|
||
}
|
||
|
||
# Info message
|
||
msg_info() {
|
||
local msg="$1"
|
||
stop_spinner
|
||
spinner "$msg" &
|
||
SPINNER_PID=$!
|
||
disown $SPINNER_PID 2>/dev/null
|
||
}
|
||
|
||
# Success message
|
||
msg_ok() {
|
||
stop_spinner
|
||
local msg="$1"
|
||
echo -e "${BFR}${CM}${GN}${msg}${CL}"
|
||
}
|
||
|
||
# Error message
|
||
msg_error() {
|
||
stop_spinner
|
||
local msg="$1"
|
||
echo -e "${BFR}${CROSS}${RD}${msg}${CL}"
|
||
}
|
||
|
||
# Migrate configuration from old path to new
|
||
migrate_config() {
|
||
local old_config="/opt/lxc-iptag"
|
||
local new_config="/opt/iptag/iptag.conf"
|
||
|
||
if [[ -f "$old_config" ]]; then
|
||
msg_info "Migrating configuration from old path"
|
||
if cp "$old_config" "$new_config" &>/dev/null; then
|
||
rm -rf "$old_config" &>/dev/null
|
||
msg_ok "Configuration migrated and old config removed"
|
||
else
|
||
msg_error "Failed to migrate configuration"
|
||
fi
|
||
fi
|
||
}
|
||
|
||
|
||
# Update existing installation
|
||
update_installation() {
|
||
msg_info "Updating IP-Tag Scripts"
|
||
systemctl stop iptag.service &>/dev/null
|
||
msg_ok "Stopped IP-Tag service"
|
||
|
||
# Create directory if it doesn't exist
|
||
if [[ ! -d "/opt/iptag" ]]; then
|
||
mkdir -p /opt/iptag
|
||
fi
|
||
|
||
# Create new config file (check if exists and ask user)
|
||
if [[ -f "/opt/iptag/iptag.conf" ]]; then
|
||
echo -e "\n${YW}Configuration file already exists.${CL}"
|
||
echo -e "${YW}Note: No critical changes were made in this version.${CL}"
|
||
while true; do
|
||
read -p "Do you want to replace it with defaults? (y/n): " yn
|
||
case $yn in
|
||
[Yy]*)
|
||
interactive_config_setup
|
||
msg_info "Replacing configuration file"
|
||
generate_config >/opt/iptag/iptag.conf
|
||
msg_ok "Configuration file replaced with defaults"
|
||
break
|
||
;;
|
||
[Nn]*)
|
||
echo -e "${GN}✓ Keeping existing configuration file${CL}"
|
||
break
|
||
;;
|
||
*)
|
||
echo -e "${RD}Please answer yes or no.${CL}"
|
||
;;
|
||
esac
|
||
done
|
||
else
|
||
interactive_config_setup
|
||
msg_info "Creating new configuration file"
|
||
generate_config >/opt/iptag/iptag.conf
|
||
msg_ok "Created new configuration file at /opt/iptag/iptag.conf"
|
||
fi
|
||
|
||
# Update main script
|
||
msg_info "Updating main script"
|
||
generate_main_script >/opt/iptag/iptag
|
||
chmod +x /opt/iptag/iptag
|
||
msg_ok "Updated main script"
|
||
|
||
# Update service file
|
||
msg_info "Updating service file"
|
||
generate_service >/lib/systemd/system/iptag.service
|
||
msg_ok "Updated service file"
|
||
|
||
msg_info "Creating manual run command"
|
||
cat <<'EOF' >/usr/local/bin/iptag-run
|
||
#!/usr/bin/env bash
|
||
CONFIG_FILE="/opt/iptag/iptag.conf"
|
||
SCRIPT_FILE="/opt/iptag/iptag"
|
||
if [[ ! -f "$SCRIPT_FILE" ]]; then
|
||
echo "✗ Main script not found: $SCRIPT_FILE"
|
||
exit 1
|
||
fi
|
||
export FORCE_SINGLE_RUN=true
|
||
exec "$SCRIPT_FILE"
|
||
EOF
|
||
chmod +x /usr/local/bin/iptag-run
|
||
msg_ok "Created iptag-run executable - You can execute this manually by entering “iptag-run” in the Proxmox host, so the script is executed by hand."
|
||
|
||
msg_info "Restarting service"
|
||
systemctl daemon-reload &>/dev/null
|
||
systemctl enable -q --now iptag.service &>/dev/null
|
||
msg_ok "Updated IP-Tag Scripts"
|
||
|
||
# Show configuration information after update
|
||
show_post_install_info
|
||
}
|
||
|
||
# Install only command without service
|
||
install_command_only() {
|
||
msg_info "Installing IP-Tag Command Only"
|
||
|
||
# Create directory if it doesn't exist
|
||
if [[ ! -d "/opt/iptag" ]]; then
|
||
mkdir -p /opt/iptag
|
||
fi
|
||
|
||
# Migrate config if needed
|
||
migrate_config
|
||
|
||
# Interactive configuration setup
|
||
if [[ ! -f /opt/iptag/iptag.conf ]]; then
|
||
interactive_config_setup_command
|
||
msg_info "Setup Configuration"
|
||
generate_config >/opt/iptag/iptag.conf
|
||
msg_ok "Created configuration file at /opt/iptag/iptag.conf"
|
||
else
|
||
stop_spinner
|
||
echo -e "\n${YW}Configuration file already exists.${CL}"
|
||
read -p "Do you want to reconfigure tag format? (y/n): " reconfigure
|
||
case $reconfigure in
|
||
[Yy]*)
|
||
interactive_config_setup_command
|
||
msg_info "Updating Configuration"
|
||
generate_config >/opt/iptag/iptag.conf
|
||
msg_ok "Updated configuration file"
|
||
;;
|
||
*)
|
||
msg_ok "Keeping existing configuration file"
|
||
;;
|
||
esac
|
||
fi
|
||
|
||
# Setup main script
|
||
msg_info "Setup Main Script"
|
||
generate_main_script >/opt/iptag/iptag
|
||
chmod +x /opt/iptag/iptag
|
||
msg_ok "Created main script"
|
||
|
||
# Create manual run command
|
||
msg_info "Creating iptag-run command"
|
||
cat <<'EOF' >/usr/local/bin/iptag-run
|
||
#!/usr/bin/env bash
|
||
CONFIG_FILE="/opt/iptag/iptag.conf"
|
||
SCRIPT_FILE="/opt/iptag/iptag"
|
||
if [[ ! -f "$SCRIPT_FILE" ]]; then
|
||
echo "✗ Main script not found: $SCRIPT_FILE"
|
||
exit 1
|
||
fi
|
||
export FORCE_SINGLE_RUN=true
|
||
exec "$SCRIPT_FILE"
|
||
EOF
|
||
chmod +x /usr/local/bin/iptag-run
|
||
msg_ok "Created iptag-run command"
|
||
|
||
msg_ok "IP-Tag Command installed successfully! Use 'iptag-run' to run manually."
|
||
}
|
||
|
||
# Show post-installation information
|
||
show_post_install_info() {
|
||
stop_spinner
|
||
echo -e "\n${YW}=== Next Steps ===${CL}"
|
||
|
||
# Show usage information
|
||
if command -v iptag-run >/dev/null 2>&1; then
|
||
echo -e "${YW}Run IP tagging manually: ${GN}iptag-run${CL}"
|
||
echo -e "${YW}Add to cron for scheduled execution if needed${CL}"
|
||
echo -e ""
|
||
fi
|
||
|
||
echo -e "${RD}IMPORTANT: Configure your network subnets!${CL}"
|
||
echo -e ""
|
||
echo -e "${YW}Configuration file: ${GN}/opt/iptag/iptag.conf${CL}"
|
||
echo -e ""
|
||
echo -e "${YW}Edit CIDR_LIST with your actual subnets:${CL}"
|
||
echo -e "${GN}nano /opt/iptag/iptag.conf${CL} ${YW}or${CL} ${GN}vim /opt/iptag/iptag.conf${CL}"
|
||
echo -e ""
|
||
echo -e "${YW}Example configuration:${CL}"
|
||
echo -e "${GN}CIDR_LIST=(${CL}"
|
||
echo -e "${GN} 192.168.1.0/24 # Your actual subnet${CL}"
|
||
echo -e "${GN} 10.10.0.0/16 # Another subnet${CL}"
|
||
echo -e "${GN})${CL}"
|
||
echo -e ""
|
||
}
|
||
|
||
# Interactive configuration setup for command-only (TAG_FORMAT only)
|
||
interactive_config_setup_command() {
|
||
echo -e "\n${YW}=== Configuration Setup ===${CL}"
|
||
|
||
# TAG_FORMAT configuration
|
||
echo -e "\n${YW}Select tag format:${CL}"
|
||
echo -e "${GN}1)${CL} last_two_octets - Show last two octets (e.g., 0.100) [Default]"
|
||
echo -e "${GN}2)${CL} last_octet - Show only last octet (e.g., 100)"
|
||
echo -e "${GN}3)${CL} full - Show full IP address (e.g., 192.168.0.100)"
|
||
|
||
while true; do
|
||
read -p "Enter your choice (1-3) [1]: " tag_choice
|
||
case ${tag_choice:-1} in
|
||
1)
|
||
TAG_FORMAT="last_two_octets"
|
||
echo -e "${GN}✓ Selected: last_two_octets${CL}"
|
||
break
|
||
;;
|
||
2)
|
||
TAG_FORMAT="last_octet"
|
||
echo -e "${GN}✓ Selected: last_octet${CL}"
|
||
break
|
||
;;
|
||
3)
|
||
TAG_FORMAT="full"
|
||
echo -e "${GN}✓ Selected: full${CL}"
|
||
break
|
||
;;
|
||
*)
|
||
echo -e "${RD}Please enter 1, 2, or 3.${CL}"
|
||
;;
|
||
esac
|
||
done
|
||
|
||
# Set default LOOP_INTERVAL for command mode
|
||
LOOP_INTERVAL=300
|
||
}
|
||
|
||
# Interactive configuration setup for service (TAG_FORMAT + LOOP_INTERVAL)
|
||
interactive_config_setup() {
|
||
echo -e "\n${YW}=== Configuration Setup ===${CL}"
|
||
|
||
# TAG_FORMAT configuration
|
||
echo -e "\n${YW}Select tag format:${CL}"
|
||
echo -e "${GN}1)${CL} last_two_octets - Show last two octets (e.g., 0.100) [Default]"
|
||
echo -e "${GN}2)${CL} last_octet - Show only last octet (e.g., 100)"
|
||
echo -e "${GN}3)${CL} full - Show full IP address (e.g., 192.168.0.100)"
|
||
|
||
while true; do
|
||
read -p "Enter your choice (1-3) [1]: " tag_choice
|
||
case ${tag_choice:-1} in
|
||
1)
|
||
TAG_FORMAT="last_two_octets"
|
||
echo -e "${GN}✓ Selected: last_two_octets${CL}"
|
||
break
|
||
;;
|
||
2)
|
||
TAG_FORMAT="last_octet"
|
||
echo -e "${GN}✓ Selected: last_octet${CL}"
|
||
break
|
||
;;
|
||
3)
|
||
TAG_FORMAT="full"
|
||
echo -e "${GN}✓ Selected: full${CL}"
|
||
break
|
||
;;
|
||
*)
|
||
echo -e "${RD}Please enter 1, 2, or 3.${CL}"
|
||
;;
|
||
esac
|
||
done
|
||
|
||
# LOOP_INTERVAL configuration
|
||
echo -e "\n${YW}Set check interval (in seconds):${CL}"
|
||
echo -e "${YW}Default: 300 seconds (5 minutes)${CL}"
|
||
echo -e "${YW}Recommended range: 300-3600 seconds${CL}"
|
||
|
||
while true; do
|
||
read -p "Enter interval in seconds [300]: " interval_input
|
||
interval_input=${interval_input:-300}
|
||
|
||
if [[ $interval_input =~ ^[0-9]+$ ]] && [ $interval_input -ge 300 ] && [ $interval_input -le 7200 ]; then
|
||
LOOP_INTERVAL=$interval_input
|
||
echo -e "${GN}✓ Selected: ${LOOP_INTERVAL} seconds${CL}"
|
||
break
|
||
else
|
||
echo -e "${RD}Please enter a valid number between 300 and 7200 seconds.${CL}"
|
||
fi
|
||
done
|
||
}
|
||
|
||
# Generate configuration file content
|
||
generate_config() {
|
||
cat <<EOF
|
||
# Configuration file for IP tagging
|
||
|
||
# List of allowed CIDRs
|
||
CIDR_LIST=(
|
||
192.168.0.0/16
|
||
10.0.0.0/8
|
||
100.64.0.0/10
|
||
)
|
||
|
||
# Tag format options:
|
||
# - "full": full IP address (e.g., 192.168.0.100)
|
||
# - "last_octet": only the last octet (e.g., 100)
|
||
# - "last_two_octets": last two octets (e.g., 0.100)
|
||
TAG_FORMAT="${TAG_FORMAT:-last_two_octets}"
|
||
|
||
|
||
# Check interval (in seconds)
|
||
LOOP_INTERVAL=${LOOP_INTERVAL:-300}
|
||
|
||
# Debug settings (set to true to enable debugging)
|
||
DEBUG=false
|
||
EOF
|
||
}
|
||
|
||
# Generate systemd service file content
|
||
generate_service() {
|
||
cat <<EOF
|
||
[Unit]
|
||
Description=IP-Tag service
|
||
After=network.target
|
||
|
||
[Service]
|
||
Type=simple
|
||
ExecStart=/opt/iptag/iptag
|
||
Restart=on-failure
|
||
RestartSec=10
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
EOF
|
||
}
|
||
|
||
# Generate main script content
|
||
generate_main_script() {
|
||
cat <<'EOF'
|
||
#!/bin/bash
|
||
# =============== CONFIGURATION =============== #
|
||
readonly CONFIG_FILE="/opt/iptag/iptag.conf"
|
||
readonly DEFAULT_TAG_FORMAT="full"
|
||
readonly DEFAULT_CHECK_INTERVAL=60
|
||
|
||
# Load the configuration file if it exists
|
||
if [ -f "$CONFIG_FILE" ]; then
|
||
# shellcheck source=./iptag.conf
|
||
source "$CONFIG_FILE"
|
||
fi
|
||
|
||
# Set default DEBUG value if not defined
|
||
DEBUG=${DEBUG:-false}
|
||
|
||
# Debug logging function
|
||
debug_log() {
|
||
if [[ "$DEBUG" == "true" || "$DEBUG" == "1" ]]; then
|
||
echo "[DEBUG] $*" >&2
|
||
fi
|
||
}
|
||
|
||
# Color constants
|
||
readonly RED='\033[0;31m'
|
||
readonly GREEN='\033[0;32m'
|
||
readonly YELLOW='\033[0;33m'
|
||
readonly BLUE='\033[0;34m'
|
||
readonly PURPLE='\033[0;35m'
|
||
readonly CYAN='\033[0;36m'
|
||
readonly WHITE='\033[1;37m'
|
||
readonly GRAY='\033[0;37m'
|
||
readonly NC='\033[0m' # No Color
|
||
|
||
# Logging functions with colors
|
||
log_success() {
|
||
echo -e "${GREEN}✓${NC} $*"
|
||
}
|
||
|
||
log_info() {
|
||
echo -e "${BLUE}ℹ${NC} $*"
|
||
}
|
||
|
||
log_warning() {
|
||
echo -e "${YELLOW}⚠${NC} $*"
|
||
}
|
||
|
||
log_error() {
|
||
echo -e "${RED}✗${NC} $*"
|
||
}
|
||
|
||
log_change() {
|
||
echo -e "${CYAN}~${NC} $*"
|
||
}
|
||
|
||
log_unchanged() {
|
||
echo -e "${GRAY}=${NC} $*"
|
||
}
|
||
|
||
# Check if IP is in CIDR
|
||
ip_in_cidr() {
|
||
local ip="$1" cidr="$2"
|
||
debug_log "ip_in_cidr: checking '$ip' against '$cidr'"
|
||
|
||
# Manual CIDR check - более надёжный метод
|
||
debug_log "ip_in_cidr: using manual check (bypassing ipcalc)"
|
||
local network prefix
|
||
IFS='/' read -r network prefix <<< "$cidr"
|
||
|
||
# Convert IP and network to integers for comparison
|
||
local ip_int net_int mask
|
||
IFS='.' read -r a b c d <<< "$ip"
|
||
ip_int=$(( (a << 24) + (b << 16) + (c << 8) + d ))
|
||
|
||
IFS='.' read -r a b c d <<< "$network"
|
||
net_int=$(( (a << 24) + (b << 16) + (c << 8) + d ))
|
||
|
||
# Create subnet mask
|
||
mask=$(( 0xFFFFFFFF << (32 - prefix) ))
|
||
|
||
# Apply mask and compare
|
||
local ip_masked=$((ip_int & mask))
|
||
local net_masked=$((net_int & mask))
|
||
|
||
debug_log "ip_in_cidr: IP=$ip ($ip_int), Network=$network ($net_int), Prefix=$prefix"
|
||
debug_log "ip_in_cidr: Mask=$mask (hex: $(printf '0x%08x' $mask))"
|
||
debug_log "ip_in_cidr: IP&Mask=$ip_masked ($(printf '%d.%d.%d.%d' $((ip_masked>>24&255)) $((ip_masked>>16&255)) $((ip_masked>>8&255)) $((ip_masked&255))))"
|
||
debug_log "ip_in_cidr: Net&Mask=$net_masked ($(printf '%d.%d.%d.%d' $((net_masked>>24&255)) $((net_masked>>16&255)) $((net_masked>>8&255)) $((net_masked&255))))"
|
||
|
||
if (( ip_masked == net_masked )); then
|
||
debug_log "ip_in_cidr: manual check PASSED - IP is in CIDR"
|
||
return 0
|
||
else
|
||
debug_log "ip_in_cidr: manual check FAILED - IP is NOT in CIDR"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
# Format IP address according to the configuration
|
||
format_ip_tag() {
|
||
local ip="$1"
|
||
[[ -z "$ip" ]] && return
|
||
local format="${TAG_FORMAT:-$DEFAULT_TAG_FORMAT}"
|
||
case "$format" in
|
||
"last_octet") echo "${ip##*.}" ;;
|
||
"last_two_octets") echo "${ip#*.*.}" ;;
|
||
*) echo "$ip" ;;
|
||
esac
|
||
}
|
||
|
||
|
||
# Check if IP is in any CIDRs
|
||
ip_in_cidrs() {
|
||
local ip="$1" cidrs="$2"
|
||
[[ -z "$cidrs" ]] && return 1
|
||
local IFS=' '
|
||
debug_log "Checking IP '$ip' against CIDRs: '$cidrs'"
|
||
for cidr in $cidrs; do
|
||
debug_log "Testing IP '$ip' against CIDR '$cidr'"
|
||
if ip_in_cidr "$ip" "$cidr"; then
|
||
debug_log "IP '$ip' matches CIDR '$cidr' - PASSED"
|
||
return 0
|
||
else
|
||
debug_log "IP '$ip' does not match CIDR '$cidr'"
|
||
fi
|
||
done
|
||
debug_log "IP '$ip' failed all CIDR checks"
|
||
return 1
|
||
}
|
||
|
||
# Check if IP is valid
|
||
is_valid_ipv4() {
|
||
local ip="$1"
|
||
[[ "$ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] || return 1
|
||
|
||
local IFS='.' parts
|
||
read -ra parts <<< "$ip"
|
||
for part in "${parts[@]}"; do
|
||
(( part >= 0 && part <= 255 )) || return 1
|
||
done
|
||
return 0
|
||
}
|
||
|
||
# Get VM IPs using improved methods
|
||
get_vm_ips() {
|
||
local vmid=$1 ips=""
|
||
local vm_config="/etc/pve/qemu-server/${vmid}.conf"
|
||
[[ ! -f "$vm_config" ]] && return
|
||
|
||
debug_log "vm $vmid: starting IP detection"
|
||
|
||
# Check if VM is running first
|
||
local vm_status=""
|
||
if command -v qm >/dev/null 2>&1; then
|
||
vm_status=$(qm status "$vmid" 2>/dev/null | awk '{print $2}')
|
||
fi
|
||
|
||
if [[ "$vm_status" != "running" ]]; then
|
||
debug_log "vm $vmid: not running (status: $vm_status)"
|
||
return
|
||
fi
|
||
|
||
# Get MAC addresses from config
|
||
local mac_addresses=$(grep -E "^net[0-9]+:" "$vm_config" | grep -oE "([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}" | head -3)
|
||
debug_log "vm $vmid: found MACs: $mac_addresses"
|
||
|
||
# Method 1: QM guest agent (most reliable for current IP)
|
||
if command -v qm >/dev/null 2>&1; then
|
||
debug_log "vm $vmid: trying qm guest agent first"
|
||
local qm_ips=$(timeout 8 qm guest cmd "$vmid" network-get-interfaces 2>/dev/null | grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' | grep -v "127.0.0.1" | head -3)
|
||
for qm_ip in $qm_ips; do
|
||
if [[ "$qm_ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
|
||
debug_log "vm $vmid: found IP $qm_ip via qm guest cmd"
|
||
ips+="$qm_ip "
|
||
fi
|
||
done
|
||
fi
|
||
|
||
# Method 2: Fresh ARP table lookup (force refresh)
|
||
if [[ -n "$mac_addresses" ]]; then
|
||
debug_log "vm $vmid: refreshing ARP table and checking"
|
||
# Try to refresh ARP table by pinging network ranges
|
||
for mac in $mac_addresses; do
|
||
local mac_lower=$(echo "$mac" | tr '[:upper:]' '[:lower:]')
|
||
|
||
# First check current ARP table
|
||
local current_ip=$(ip neighbor show | grep "$mac_lower" | grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' | head -1)
|
||
|
||
# If found in ARP, verify it's still valid by trying to ping
|
||
if [[ -n "$current_ip" && "$current_ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
|
||
debug_log "vm $vmid: found IP $current_ip in ARP table for MAC $mac_lower, verifying..."
|
||
# Quick ping test to verify IP is still active
|
||
if timeout 2 ping -c 1 "$current_ip" >/dev/null 2>&1; then
|
||
debug_log "vm $vmid: verified IP $current_ip is active via ping"
|
||
ips+="$current_ip "
|
||
else
|
||
debug_log "vm $vmid: IP $current_ip failed ping verification, removing from ARP"
|
||
# Remove stale ARP entry
|
||
ip neighbor del "$current_ip" dev $(ip route get "$current_ip" 2>/dev/null | grep -oE 'dev [^ ]+' | cut -d' ' -f2) 2>/dev/null || true
|
||
fi
|
||
fi
|
||
done
|
||
fi
|
||
|
||
# Method 3: DHCP leases (backup method)
|
||
if [[ -z "$ips" ]]; then
|
||
debug_log "vm $vmid: checking DHCP leases as fallback"
|
||
for mac in $mac_addresses; do
|
||
local mac_lower=$(echo "$mac" | tr '[:upper:]' '[:lower:]')
|
||
for dhcp_file in "/var/lib/dhcp/dhcpd.leases" "/var/lib/dhcpcd5/dhcpcd.leases"; do
|
||
if [[ -f "$dhcp_file" ]]; then
|
||
# Look for most recent lease for this MAC
|
||
local dhcp_ip=$(tac "$dhcp_file" 2>/dev/null | grep -A 10 "ethernet $mac_lower" | grep "binding state active" -A 5 | grep -oE "([0-9]{1,3}\.){3}[0-9]{1,3}" | head -1)
|
||
if [[ -n "$dhcp_ip" && "$dhcp_ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
|
||
debug_log "vm $vmid: found IP $dhcp_ip via DHCP leases"
|
||
# Verify this IP responds
|
||
if timeout 2 ping -c 1 "$dhcp_ip" >/dev/null 2>&1; then
|
||
debug_log "vm $vmid: verified DHCP IP $dhcp_ip is active"
|
||
ips+="$dhcp_ip "
|
||
break 2
|
||
else
|
||
debug_log "vm $vmid: DHCP IP $dhcp_ip failed ping test"
|
||
fi
|
||
fi
|
||
fi
|
||
done
|
||
done
|
||
fi
|
||
|
||
# Remove duplicates and clean up
|
||
local unique_ips=$(echo "$ips" | tr ' ' '\n' | sort -u | tr '\n' ' ')
|
||
unique_ips="${unique_ips% }"
|
||
|
||
debug_log "vm $vmid: final IPs: '$unique_ips'"
|
||
echo "$unique_ips"
|
||
}
|
||
|
||
# Update tags for container or VM
|
||
update_tags() {
|
||
local type="$1" vmid="$2"
|
||
local current_ips_full
|
||
|
||
if [[ "$type" == "lxc" ]]; then
|
||
current_ips_full=$(get_lxc_ips "${vmid}")
|
||
while IFS= read -r line; do
|
||
[[ "$line" == tags:* ]] && current_tags_raw="${line#tags: }" && break
|
||
done < <(pct config "$vmid" 2>/dev/null)
|
||
else
|
||
current_ips_full=$(get_vm_ips "${vmid}")
|
||
local vm_config="/etc/pve/qemu-server/${vmid}.conf"
|
||
if [[ -f "$vm_config" ]]; then
|
||
local current_tags_raw=$(grep "^tags:" "$vm_config" 2>/dev/null | cut -d: -f2 | sed 's/^[[:space:]]*//')
|
||
fi
|
||
fi
|
||
|
||
local current_tags=() next_tags=() current_ip_tags=()
|
||
if [[ -n "$current_tags_raw" ]]; then
|
||
mapfile -t current_tags < <(echo "$current_tags_raw" | sed 's/;/\n/g')
|
||
fi
|
||
|
||
# Separate IP/numeric and user tags
|
||
for tag in "${current_tags[@]}"; do
|
||
if is_valid_ipv4 "${tag}" || [[ "$tag" =~ ^[0-9]+(\.[0-9]+)*$ ]]; then
|
||
current_ip_tags+=("${tag}")
|
||
else
|
||
next_tags+=("${tag}")
|
||
fi
|
||
done
|
||
|
||
# Generate new IP tags from current IPs
|
||
local formatted_ips=()
|
||
debug_log "$type $vmid current_ips_full: '$current_ips_full'"
|
||
debug_log "$type $vmid CIDR_LIST: ${CIDR_LIST[*]}"
|
||
for ip in $current_ips_full; do
|
||
[[ -z "$ip" ]] && continue
|
||
debug_log "$type $vmid processing IP: '$ip'"
|
||
if is_valid_ipv4 "$ip"; then
|
||
debug_log "$type $vmid IP '$ip' is valid"
|
||
if ip_in_cidrs "$ip" "${CIDR_LIST[*]}"; then
|
||
debug_log "$type $vmid IP '$ip' passed CIDR check"
|
||
local formatted_ip=$(format_ip_tag "$ip")
|
||
debug_log "$type $vmid formatted '$ip' -> '$formatted_ip'"
|
||
[[ -n "$formatted_ip" ]] && formatted_ips+=("$formatted_ip")
|
||
else
|
||
debug_log "$type $vmid IP '$ip' failed CIDR check"
|
||
fi
|
||
else
|
||
debug_log "$type $vmid IP '$ip' is invalid"
|
||
fi
|
||
done
|
||
debug_log "$type $vmid final formatted_ips: ${formatted_ips[*]}"
|
||
|
||
# If LXC and no IPs detected, do not touch tags at all
|
||
if [[ "$type" == "lxc" && ${#formatted_ips[@]} -eq 0 ]]; then
|
||
log_unchanged "LXC ${GRAY}${vmid}${NC}: No IP detected, tags unchanged"
|
||
return
|
||
fi
|
||
|
||
# Prepend new IP tags to the beginning of the tag list
|
||
local final_tags=()
|
||
for new_ip in "${formatted_ips[@]}"; do
|
||
final_tags+=("$new_ip")
|
||
done
|
||
for tag in "${next_tags[@]}"; do
|
||
final_tags+=("$tag")
|
||
done
|
||
next_tags=("${final_tags[@]}")
|
||
|
||
# Update tags if there are changes
|
||
local old_tags_str=$(IFS=';'; echo "${current_tags[*]}")
|
||
local new_tags_str=$(IFS=';'; echo "${next_tags[*]}")
|
||
|
||
debug_log "$type $vmid old_tags: '$old_tags_str'"
|
||
debug_log "$type $vmid new_tags: '$new_tags_str'"
|
||
debug_log "$type $vmid tags_equal: $([[ "$old_tags_str" == "$new_tags_str" ]] && echo true || echo false)"
|
||
|
||
if [[ "$old_tags_str" != "$new_tags_str" ]]; then
|
||
# Determine what changed
|
||
local old_ip_tags_count=${#current_ip_tags[@]}
|
||
local new_ip_tags_count=${#formatted_ips[@]}
|
||
|
||
# Build detailed change message
|
||
local change_details=""
|
||
|
||
if [[ $old_ip_tags_count -eq 0 ]]; then
|
||
change_details="added ${new_ip_tags_count} IP tag(s): [${GREEN}${formatted_ips[*]}${NC}]"
|
||
else
|
||
# Compare old and new IP tags
|
||
local added_tags=() removed_tags=() common_tags=()
|
||
|
||
# Find removed tags
|
||
for old_tag in "${current_ip_tags[@]}"; do
|
||
local found=false
|
||
for new_tag in "${formatted_ips[@]}"; do
|
||
if [[ "$old_tag" == "$new_tag" ]]; then
|
||
found=true
|
||
break
|
||
fi
|
||
done
|
||
if [[ "$found" == false ]]; then
|
||
removed_tags+=("$old_tag")
|
||
else
|
||
common_tags+=("$old_tag")
|
||
fi
|
||
done
|
||
|
||
# Find added tags
|
||
for new_tag in "${formatted_ips[@]}"; do
|
||
local found=false
|
||
for old_tag in "${current_ip_tags[@]}"; do
|
||
if [[ "$new_tag" == "$old_tag" ]]; then
|
||
found=true
|
||
break
|
||
fi
|
||
done
|
||
if [[ "$found" == false ]]; then
|
||
added_tags+=("$new_tag")
|
||
fi
|
||
done
|
||
|
||
# Build change message
|
||
local change_parts=()
|
||
if [[ ${#added_tags[@]} -gt 0 ]]; then
|
||
change_parts+=("added [${GREEN}${added_tags[*]}${NC}]")
|
||
fi
|
||
if [[ ${#removed_tags[@]} -gt 0 ]]; then
|
||
change_parts+=("removed [${YELLOW}${removed_tags[*]}${NC}]")
|
||
fi
|
||
if [[ ${#common_tags[@]} -gt 0 ]]; then
|
||
change_parts+=("kept [${GRAY}${common_tags[*]}${NC}]")
|
||
fi
|
||
|
||
change_details=$(IFS=', '; echo "${change_parts[*]}")
|
||
fi
|
||
|
||
log_change "${type^^} ${CYAN}${vmid}${NC}: ${change_details}"
|
||
|
||
if [[ "$type" == "lxc" ]]; then
|
||
pct set "${vmid}" -tags "$(IFS=';'; echo "${next_tags[*]}")" &>/dev/null
|
||
else
|
||
local vm_config="/etc/pve/qemu-server/${vmid}.conf"
|
||
if [[ -f "$vm_config" ]]; then
|
||
sed -i '/^tags:/d' "$vm_config"
|
||
if [[ ${#next_tags[@]} -gt 0 ]]; then
|
||
echo "tags: $(IFS=';'; echo "${next_tags[*]}")" >> "$vm_config"
|
||
fi
|
||
fi
|
||
fi
|
||
else
|
||
# Tags unchanged
|
||
local ip_count=${#formatted_ips[@]}
|
||
local status_msg=""
|
||
|
||
if [[ $ip_count -eq 0 ]]; then
|
||
status_msg="No IPs detected"
|
||
elif [[ $ip_count -eq 1 ]]; then
|
||
status_msg="IP tag [${GRAY}${formatted_ips[0]}${NC}] unchanged"
|
||
else
|
||
status_msg="${ip_count} IP tags [${GRAY}${formatted_ips[*]}${NC}] unchanged"
|
||
fi
|
||
|
||
log_unchanged "${type^^} ${GRAY}${vmid}${NC}: ${status_msg}"
|
||
fi
|
||
}
|
||
|
||
# Update all instances of specified type
|
||
update_all_tags() {
|
||
local type="$1" vmids count=0
|
||
|
||
if [[ "$type" == "lxc" ]]; then
|
||
vmids=($(pct list 2>/dev/null | grep -v VMID | awk '{print $1}'))
|
||
else
|
||
local all_vm_configs=($(ls /etc/pve/qemu-server/*.conf 2>/dev/null | sed 's/.*\/\([0-9]*\)\.conf/\1/' | sort -n))
|
||
vmids=("${all_vm_configs[@]}")
|
||
fi
|
||
|
||
count=${#vmids[@]}
|
||
[[ $count -eq 0 ]] && return
|
||
|
||
# Display processing header with color
|
||
if [[ "$type" == "lxc" ]]; then
|
||
log_info "Processing ${WHITE}${count}${NC} LXC container(s) sequentially"
|
||
else
|
||
log_info "Processing ${WHITE}${count}${NC} virtual machine(s) sequentially"
|
||
fi
|
||
|
||
# Process each VM/LXC container sequentially
|
||
for vmid in "${vmids[@]}"; do
|
||
update_tags "$type" "$vmid"
|
||
done
|
||
|
||
# Add completion message
|
||
if [[ "$type" == "lxc" ]]; then
|
||
log_success "Completed processing LXC containers"
|
||
else
|
||
log_success "Completed processing virtual machines"
|
||
fi
|
||
}
|
||
|
||
# Check if status changed
|
||
check_status_changed() {
|
||
local type="$1" current
|
||
case "$type" in
|
||
"lxc") current=$(pct list 2>/dev/null | grep -v VMID) ;;
|
||
"vm") current=$(ls -la /etc/pve/qemu-server/*.conf 2>/dev/null) ;;
|
||
"fw") current=$(ip link show type bridge 2>/dev/null) ;;
|
||
esac
|
||
local last_var="last_${type}_status"
|
||
[[ "${!last_var}" == "$current" ]] && return 1
|
||
eval "$last_var='$current'"
|
||
return 0
|
||
}
|
||
|
||
# Main check function
|
||
check() {
|
||
local current_time=$(date +%s)
|
||
|
||
# Simple periodic check - always update both LXC and VM every loop
|
||
log_info "Starting periodic check"
|
||
|
||
# Update LXC containers
|
||
update_all_tags "lxc"
|
||
|
||
# Update VMs
|
||
update_all_tags "vm"
|
||
}
|
||
|
||
# Main loop
|
||
main() {
|
||
# Display startup message
|
||
echo -e "\n${PURPLE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||
log_success "IP-Tag service started successfully"
|
||
echo -e "${BLUE}ℹ${NC} Loop interval: ${WHITE}${LOOP_INTERVAL:-300}${NC} seconds"
|
||
echo -e "${BLUE}ℹ${NC} Debug mode: ${WHITE}${DEBUG:-false}${NC}"
|
||
echo -e "${BLUE}ℹ${NC} Tag format: ${WHITE}${TAG_FORMAT:-full}${NC}"
|
||
echo -e "${BLUE}ℹ${NC} Allowed CIDRs: ${WHITE}${CIDR_LIST[*]}${NC}"
|
||
echo -e "${PURPLE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}\n"
|
||
|
||
if [[ "$FORCE_SINGLE_RUN" == "true" ]]; then
|
||
check
|
||
exit 0
|
||
fi
|
||
|
||
while true; do
|
||
check
|
||
sleep "${LOOP_INTERVAL:-300}"
|
||
done
|
||
}
|
||
|
||
|
||
|
||
|
||
|
||
# Simple LXC IP detection
|
||
get_lxc_ips() {
|
||
local vmid=$1
|
||
|
||
debug_log "lxc $vmid: starting IP detection"
|
||
|
||
# Check if LXC is running
|
||
local lxc_status=$(pct status "${vmid}" 2>/dev/null | awk '{print $2}')
|
||
if [[ "$lxc_status" != "running" ]]; then
|
||
debug_log "lxc $vmid: not running (status: $lxc_status)"
|
||
return
|
||
fi
|
||
|
||
local ips=""
|
||
|
||
# Method 1: Check Proxmox config for static IP
|
||
local pve_lxc_config="/etc/pve/lxc/${vmid}.conf"
|
||
if [[ -f "$pve_lxc_config" ]]; then
|
||
local static_ip=$(grep -E "^net[0-9]+:" "$pve_lxc_config" 2>/dev/null | grep -oE 'ip=([0-9]{1,3}\.){3}[0-9]{1,3}' | cut -d'=' -f2 | head -1)
|
||
if [[ -n "$static_ip" && "$static_ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
|
||
debug_log "lxc $vmid: found static IP $static_ip in config"
|
||
ips="$static_ip"
|
||
fi
|
||
fi
|
||
|
||
# Method 2: ARP table lookup if no static IP
|
||
if [[ -z "$ips" && -f "$pve_lxc_config" ]]; then
|
||
local mac_addr=$(grep -Eo 'hwaddr=([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}' "$pve_lxc_config" | head -1 | cut -d'=' -f2)
|
||
if [[ -n "$mac_addr" ]]; then
|
||
local bridge_name=$(grep -Eo 'bridge=[^,]+' "$pve_lxc_config" | head -1 | cut -d'=' -f2)
|
||
local arp_ip=$(ip neighbor show | grep "$mac_addr" | grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' | head -1)
|
||
if [[ -n "$arp_ip" && "$arp_ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
|
||
debug_log "lxc $vmid: found IP $arp_ip via ARP table"
|
||
ips="$arp_ip"
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
# Method 3: Direct container command if ARP failed
|
||
if [[ -z "$ips" ]]; then
|
||
local container_ip=$(timeout 5s pct exec "$vmid" -- ip -4 addr show 2>/dev/null | grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' | grep -v '127.0.0.1' | head -1)
|
||
if [[ -n "$container_ip" ]] && is_valid_ipv4 "$container_ip"; then
|
||
debug_log "lxc $vmid: found IP $container_ip via pct exec"
|
||
ips="$container_ip"
|
||
fi
|
||
fi
|
||
|
||
debug_log "lxc $vmid: final IPs: '$ips'"
|
||
echo "$ips"
|
||
}
|
||
|
||
main
|
||
EOF
|
||
}
|
||
|
||
# Choose installation/update mode
|
||
echo -e "\n${YW}Choose action:${CL}"
|
||
echo -e "${GN}1)${CL} Install with automatic service (recommended)"
|
||
echo -e "${GN}2)${CL} Install command only (manual execution)"
|
||
echo -e "${GN}3)${CL} Update existing installation"
|
||
echo -e "${RD}4)${CL} Cancel"
|
||
|
||
while true; do
|
||
read -p "Enter your choice (1-4): " choice
|
||
case $choice in
|
||
1)
|
||
INSTALL_MODE="service"
|
||
echo -e "${GN}✓ Selected: Service installation${CL}"
|
||
break
|
||
;;
|
||
2)
|
||
INSTALL_MODE="command"
|
||
echo -e "${GN}✓ Selected: Command-only installation${CL}"
|
||
break
|
||
;;
|
||
3)
|
||
echo -e "${GN}✓ Selected: Update installation${CL}"
|
||
update_installation
|
||
exit 0
|
||
;;
|
||
4)
|
||
msg_error "Action cancelled."
|
||
exit 0
|
||
;;
|
||
*)
|
||
msg_error "Please enter 1, 2, 3, or 4."
|
||
;;
|
||
esac
|
||
done
|
||
|
||
echo -e "\n${YW}This will install ${APP} on ${hostname} in $INSTALL_MODE mode.${CL}"
|
||
while true; do
|
||
read -p "Proceed? (y/n): " yn
|
||
case $yn in
|
||
[Yy]*)
|
||
break
|
||
;;
|
||
[Nn]*)
|
||
msg_error "Installation cancelled."
|
||
exit
|
||
;;
|
||
*)
|
||
msg_error "Please answer yes or no."
|
||
;;
|
||
esac
|
||
done
|
||
|
||
if ! pveversion | grep -Eq "pve-manager/(8\.[0-4]|9\.[0-9]+)(\.[0-9]+)*"; then
|
||
msg_error "This version of Proxmox Virtual Environment is not supported"
|
||
msg_error "⚠ Requires Proxmox Virtual Environment Version 8.0–8.4 or 9.x."
|
||
msg_error "Exiting..."
|
||
sleep 2
|
||
exit
|
||
fi
|
||
|
||
msg_info "Installing Dependencies"
|
||
apt-get update &>/dev/null
|
||
apt-get install -y ipcalc net-tools &>/dev/null
|
||
msg_ok "Installed Dependencies"
|
||
|
||
# Execute installation based on selected mode
|
||
if [[ "$INSTALL_MODE" == "service" ]]; then
|
||
# Full service installation
|
||
msg_info "Setting up IP-Tag Scripts"
|
||
mkdir -p /opt/iptag
|
||
msg_ok "Setup IP-Tag Scripts"
|
||
|
||
# Migrate config if needed
|
||
migrate_config
|
||
|
||
# Interactive configuration setup
|
||
if [[ ! -f /opt/iptag/iptag.conf ]]; then
|
||
interactive_config_setup
|
||
msg_info "Setup Default Config"
|
||
generate_config >/opt/iptag/iptag.conf
|
||
msg_ok "Setup default config"
|
||
else
|
||
stop_spinner
|
||
echo -e "\n${YW}Configuration file already exists.${CL}"
|
||
read -p "Do you want to reconfigure tag format and loop interval? (y/n): " reconfigure
|
||
case $reconfigure in
|
||
[Yy]*)
|
||
interactive_config_setup
|
||
msg_info "Updating Configuration"
|
||
generate_config >/opt/iptag/iptag.conf
|
||
msg_ok "Updated configuration file"
|
||
;;
|
||
*)
|
||
msg_ok "Keeping existing configuration file"
|
||
;;
|
||
esac
|
||
fi
|
||
|
||
msg_info "Setup Main Function"
|
||
generate_main_script >/opt/iptag/iptag
|
||
chmod +x /opt/iptag/iptag
|
||
msg_ok "Setup Main Function"
|
||
|
||
msg_info "Creating Service"
|
||
generate_service >/lib/systemd/system/iptag.service
|
||
msg_ok "Created Service"
|
||
|
||
msg_info "Starting Service"
|
||
systemctl daemon-reload &>/dev/null
|
||
systemctl enable -q --now iptag.service &>/dev/null
|
||
msg_ok "Started Service"
|
||
|
||
msg_info "Creating manual run command"
|
||
cat <<'EOF' >/usr/local/bin/iptag-run
|
||
#!/usr/bin/env bash
|
||
CONFIG_FILE="/opt/iptag/iptag.conf"
|
||
SCRIPT_FILE="/opt/iptag/iptag"
|
||
if [[ ! -f "$SCRIPT_FILE" ]]; then
|
||
echo "✗ Main script not found: $SCRIPT_FILE"
|
||
exit 1
|
||
fi
|
||
export FORCE_SINGLE_RUN=true
|
||
exec "$SCRIPT_FILE"
|
||
EOF
|
||
chmod +x /usr/local/bin/iptag-run
|
||
msg_ok "Created iptag-run command"
|
||
|
||
echo -e "\n${GN}${APP} service installation completed successfully! ${CL}"
|
||
echo -e "${YW}The service is now running automatically.${CL}"
|
||
echo -e "${YW}You can also run it manually with: ${GN}iptag-run${CL}\n"
|
||
|
||
# Show configuration information
|
||
show_post_install_info
|
||
|
||
elif [[ "$INSTALL_MODE" == "command" ]]; then
|
||
# Command-only installation
|
||
install_command_only
|
||
|
||
stop_spinner
|
||
echo -e "\n${GN}${APP} command installation completed successfully! ${CL}"
|
||
|
||
# Show configuration information
|
||
show_post_install_info
|
||
fi
|
||
|
||
# Clean up any running spinner and exit
|
||
stop_spinner
|
||
exit 0
|