ProxmoxVE/tools/pve/add-iptag.sh
Desert Gamer 6a2fc4083b
Enhanced IP-Tag installation script with interactive configuration, improved VM IP detection, and better visual indicators (#7366)
- **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.
2025-09-03 09:25:37 +02:00

1108 lines
33 KiB
Bash
Raw 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) 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.08.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