2025-04-19 23:00:43 +05:00
#!/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)
2025-07-04 11:38:05 +03:00
# Color variables
2025-04-19 23:00:43 +05:00
YW = $( echo "\033[33m" )
GN = $( echo "\033[1;92m" )
RD = $( echo "\033[01;31m" )
CL = $( echo "\033[m" )
BFR = "\\r\\033[K"
HOLD = " "
2025-09-03 10:25:37 +03:00
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"
}
2025-04-19 23:00:43 +05:00
2025-07-04 11:38:05 +03:00
# Error handler for displaying error messages
2025-04-19 23:00:43 +05:00
error_handler( ) {
2025-09-03 10:25:37 +03:00
stop_spinner
2025-04-19 23:00:43 +05:00
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 "
}
2025-07-04 11:38:05 +03:00
# Spinner for progress indication
2025-04-19 23:00:43 +05:00
spinner( ) {
2025-09-03 10:25:37 +03:00
local msg = " $1 "
2025-04-19 23:00:43 +05:00
local frames = ( '⠋' '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏' )
local spin_i = 0
local interval = 0.1
2025-09-03 10:25:37 +03:00
trap 'exit 0' TERM INT
printf "\e[?25l" 2>/dev/null
2025-04-19 23:00:43 +05:00
while true; do
2025-09-03 10:25:37 +03:00
printf " \r%s ${ YW } %s ${ CL } " " ${ frames [spin_i] } " " $msg " 2>/dev/null || exit 0
2025-04-19 23:00:43 +05:00
spin_i = $(( ( spin_i + 1 ) % ${# frames [@] } ))
2025-09-03 10:25:37 +03:00
sleep " $interval " || exit 0
2025-04-19 23:00:43 +05:00
done
}
2025-07-04 11:38:05 +03:00
# Info message
2025-04-19 23:00:43 +05:00
msg_info( ) {
local msg = " $1 "
2025-09-03 10:25:37 +03:00
stop_spinner
spinner " $msg " &
2025-04-19 23:00:43 +05:00
SPINNER_PID = $!
2025-09-03 10:25:37 +03:00
disown $SPINNER_PID 2>/dev/null
2025-04-19 23:00:43 +05:00
}
2025-07-04 11:38:05 +03:00
# Success message
2025-04-19 23:00:43 +05:00
msg_ok( ) {
2025-09-03 10:25:37 +03:00
stop_spinner
2025-04-19 23:00:43 +05:00
local msg = " $1 "
echo -e " ${ BFR } ${ CM } ${ GN } ${ msg } ${ CL } "
}
2025-07-04 11:38:05 +03:00
# Error message
2025-04-19 23:00:43 +05:00
msg_error( ) {
2025-09-03 10:25:37 +03:00
stop_spinner
2025-04-19 23:00:43 +05:00
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
}
2025-09-03 10:25:37 +03:00
2025-04-19 23:00:43 +05:00
# Update existing installation
update_installation( ) {
msg_info "Updating IP-Tag Scripts"
systemctl stop iptag.service & >/dev/null
2025-07-04 11:38:05 +03:00
msg_ok "Stopped IP-Tag service"
2025-04-19 23:00:43 +05:00
# Create directory if it doesn't exist
if [ [ ! -d "/opt/iptag" ] ] ; then
mkdir -p /opt/iptag
fi
2025-07-04 11:38:05 +03:00
# 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 } "
2025-09-03 10:25:37 +03:00
echo -e " ${ YW } Note: No critical changes were made in this version. ${ CL } "
2025-07-04 11:38:05 +03:00
while true; do
read -p "Do you want to replace it with defaults? (y/n): " yn
case $yn in
[ Yy] *)
2025-09-03 10:25:37 +03:00
interactive_config_setup
2025-07-04 11:38:05 +03:00
msg_info "Replacing configuration file"
generate_config >/opt/iptag/iptag.conf
msg_ok "Configuration file replaced with defaults"
break
; ;
[ Nn] *)
2025-09-03 10:25:37 +03:00
echo -e " ${ GN } ✓ Keeping existing configuration file ${ CL } "
2025-07-04 11:38:05 +03:00
break
; ;
*)
echo -e " ${ RD } Please answer yes or no. ${ CL } "
; ;
esac
done
else
2025-09-03 10:25:37 +03:00
interactive_config_setup
2025-07-04 11:38:05 +03:00
msg_info "Creating new configuration file"
generate_config >/opt/iptag/iptag.conf
msg_ok "Created new configuration file at /opt/iptag/iptag.conf"
fi
2025-04-19 23:00:43 +05:00
# Update main script
2025-07-04 11:38:05 +03:00
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"
2025-07-07 14:41:23 +02:00
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
2025-09-03 10:25:37 +03:00
echo " ✗ Main script not found: $SCRIPT_FILE "
2025-07-07 14:41:23 +02:00
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."
2025-07-04 11:38:05 +03:00
msg_info "Restarting service"
systemctl daemon-reload & >/dev/null
systemctl enable -q --now iptag.service & >/dev/null
msg_ok "Updated IP-Tag Scripts"
2025-09-03 10:25:37 +03:00
# 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
2025-07-04 11:38:05 +03:00
}
# Generate configuration file content
generate_config( ) {
cat <<EOF
2025-09-03 10:25:37 +03:00
# Configuration file for IP tagging
2025-07-04 11:38:05 +03:00
# 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)
2025-09-03 10:25:37 +03:00
TAG_FORMAT = " ${ TAG_FORMAT :- last_two_octets } "
# Check interval (in seconds)
LOOP_INTERVAL = ${ LOOP_INTERVAL :- 300 }
2025-07-04 11:38:05 +03:00
# 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
2025-07-07 14:26:07 +02:00
Restart = on-failure
RestartSec = 10
2025-07-04 11:38:05 +03:00
[ Install]
WantedBy = multi-user.target
EOF
}
# Generate main script content
generate_main_script( ) {
cat <<'EOF'
2025-04-19 23:00:43 +05:00
#!/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
2025-07-04 11:38:05 +03:00
# 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 } $* "
2025-04-19 23:00:43 +05:00
}
# Check if IP is in CIDR
ip_in_cidr( ) {
local ip = " $1 " cidr = " $2 "
2025-07-04 11:38:05 +03:00
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 = $(( 0 xFFFFFFFF << ( 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
2025-04-19 23:00:43 +05:00
}
# Format IP address according to the configuration
format_ip_tag( ) {
local ip = " $1 "
2025-07-04 11:38:05 +03:00
[ [ -z " $ip " ] ] && return
2025-04-19 23:00:43 +05:00
local format = " ${ TAG_FORMAT :- $DEFAULT_TAG_FORMAT } "
case " $format " in
"last_octet" ) echo " ${ ip ##*. } " ; ;
"last_two_octets" ) echo " ${ ip #*.*. } " ; ;
*) echo " $ip " ; ;
esac
}
2025-09-03 10:25:37 +03:00
2025-04-19 23:00:43 +05:00
# Check if IP is in any CIDRs
ip_in_cidrs( ) {
local ip = " $1 " cidrs = " $2 "
[ [ -z " $cidrs " ] ] && return 1
local IFS = ' '
2025-07-04 11:38:05 +03:00
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 "
2025-04-19 23:00:43 +05:00
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
}
2025-09-03 10:25:37 +03:00
# Get VM IPs using improved methods
2025-04-19 23:00:43 +05:00
get_vm_ips( ) {
2025-07-04 11:38:05 +03:00
local vmid = $1 ips = ""
local vm_config = " /etc/pve/qemu-server/ ${ vmid } .conf "
[ [ ! -f " $vm_config " ] ] && return
2025-09-03 10:25:37 +03:00
debug_log " vm $vmid : starting IP detection "
2025-07-04 11:38:05 +03:00
2025-09-03 10:25:37 +03:00
# Check if VM is running first
2025-07-04 11:38:05 +03:00
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
2025-09-03 10:25:37 +03:00
debug_log " vm $vmid : not running (status: $vm_status ) "
2025-07-04 11:38:05 +03:00
return
fi
2025-09-03 10:25:37 +03:00
# Get MAC addresses from config
2025-07-04 11:38:05 +03:00
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 "
2025-09-03 10:25:37 +03:00
# Method 1: QM guest agent (most reliable for current IP)
2025-07-04 11:38:05 +03:00
if command -v qm >/dev/null 2>& 1; then
2025-09-03 10:25:37 +03:00
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)
2025-07-04 11:38:05 +03:00
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
2025-09-03 10:25:37 +03:00
# 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
2025-07-04 11:38:05 +03:00
fi
fi
done
fi
2025-09-03 10:25:37 +03:00
# 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 "
2025-07-04 11:38:05 +03:00
fi
2025-09-03 10:25:37 +03:00
fi
2025-07-04 11:38:05 +03:00
fi
2025-09-03 10:25:37 +03:00
done
2025-07-04 11:38:05 +03:00
done
fi
2025-09-03 10:25:37 +03:00
# Remove duplicates and clean up
2025-07-04 11:38:05 +03:00
local unique_ips = $( echo " $ips " | tr ' ' '\n' | sort -u | tr '\n' ' ' )
unique_ips = " ${ unique_ips % } "
2025-09-03 10:25:37 +03:00
debug_log " vm $vmid : final IPs: ' $unique_ips ' "
2025-07-04 11:38:05 +03:00
echo " $unique_ips "
2025-04-19 23:00:43 +05:00
}
2025-07-04 11:38:05 +03:00
# Update tags for container or VM
2025-04-19 23:00:43 +05:00
update_tags( ) {
2025-07-04 11:38:05 +03:00
local type = " $1 " vmid = " $2 "
2025-04-19 23:00:43 +05:00
local current_ips_full
2025-07-04 11:38:05 +03:00
2025-04-19 23:00:43 +05:00
if [ [ " $type " = = "lxc" ] ] ; then
2025-07-04 11:38:05 +03:00
current_ips_full = $( get_lxc_ips " ${ vmid } " )
2025-07-07 14:41:23 +02:00
while IFS = read -r line; do
[ [ " $line " = = tags:* ] ] && current_tags_raw = " ${ line #tags : } " && break
done < <( pct config " $vmid " 2>/dev/null)
2025-04-19 23:00:43 +05:00
else
current_ips_full = $( get_vm_ips " ${ vmid } " )
2025-07-04 11:38:05 +03:00
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
2025-04-19 23:00:43 +05:00
fi
local current_tags = ( ) next_tags = ( ) current_ip_tags = ( )
2025-07-04 11:38:05 +03:00
if [ [ -n " $current_tags_raw " ] ] ; then
mapfile -t current_tags < <( echo " $current_tags_raw " | sed 's/;/\n/g' )
fi
2025-04-19 23:00:43 +05:00
2025-07-04 11:38:05 +03:00
# Separate IP/numeric and user tags
2025-04-19 23:00:43 +05:00
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
2025-07-04 11:38:05 +03:00
# 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 "
2025-04-19 23:00:43 +05:00
fi
2025-07-04 11:38:05 +03:00
else
debug_log " $type $vmid IP ' $ip ' is invalid "
2025-04-19 23:00:43 +05:00
fi
done
2025-07-04 11:38:05 +03:00
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
2025-04-19 23:00:43 +05:00
2025-09-03 10:25:37 +03:00
# Prepend new IP tags to the beginning of the tag list
local final_tags = ( )
2025-07-04 11:38:05 +03:00
for new_ip in " ${ formatted_ips [@] } " ; do
2025-09-03 10:25:37 +03:00
final_tags += ( " $new_ip " )
2025-07-04 11:38:05 +03:00
done
2025-09-03 10:25:37 +03:00
for tag in " ${ next_tags [@] } " ; do
final_tags += ( " $tag " )
done
next_tags = ( " ${ final_tags [@] } " )
2025-04-19 23:00:43 +05:00
2025-07-04 11:38:05 +03:00
# 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 } ] " )
2025-04-19 23:00:43 +05:00
fi
2025-07-04 11:38:05 +03:00
change_details = $( IFS = ', ' ; echo " ${ change_parts [*] } " )
2025-04-19 23:00:43 +05:00
fi
2025-07-04 11:38:05 +03:00
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
}
2025-04-19 23:00:43 +05:00
2025-07-04 11:38:05 +03:00
# 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
2025-09-03 10:25:37 +03:00
log_info " Processing ${ WHITE } ${ count } ${ NC } LXC container(s) sequentially "
2025-07-04 11:38:05 +03:00
else
2025-09-03 10:25:37 +03:00
log_info " Processing ${ WHITE } ${ count } ${ NC } virtual machine(s) sequentially "
2025-07-04 11:38:05 +03:00
fi
2025-09-03 10:25:37 +03:00
# Process each VM/LXC container sequentially
for vmid in " ${ vmids [@] } " ; do
update_tags " $type " " $vmid "
done
2025-07-04 11:38:05 +03:00
# Add completion message
if [ [ " $type " = = "lxc" ] ] ; then
log_success "Completed processing LXC containers"
2025-04-19 23:00:43 +05:00
else
2025-07-04 11:38:05 +03:00
log_success "Completed processing virtual machines"
2025-04-19 23:00:43 +05:00
fi
}
# Check if status changed
2025-07-04 11:38:05 +03:00
check_status_changed( ) {
2025-04-19 23:00:43 +05:00
local type = " $1 " current
case " $type " in
"lxc" ) current = $( pct list 2>/dev/null | grep -v VMID) ; ;
2025-07-04 11:38:05 +03:00
"vm" ) current = $( ls -la /etc/pve/qemu-server/*.conf 2>/dev/null) ; ;
"fw" ) current = $( ip link show type bridge 2>/dev/null) ; ;
2025-04-19 23:00:43 +05:00
esac
local last_var = " last_ ${ type } _status "
[ [ " ${ !last_var } " = = " $current " ] ] && return 1
eval " $last_var =' $current ' "
return 0
}
# Main check function
check( ) {
2025-09-03 10:25:37 +03:00
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"
2025-04-19 23:00:43 +05:00
}
# Main loop
main( ) {
2025-07-04 11:38:05 +03:00
# Display startup message
echo -e " \n ${ PURPLE } ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ${ NC } "
log_success "IP-Tag service started successfully"
2025-09-03 10:25:37 +03:00
echo -e " ${ BLUE } ℹ ${ NC } Loop interval: ${ WHITE } ${ LOOP_INTERVAL :- 300 } ${ NC } seconds "
2025-07-04 11:38:05 +03:00
echo -e " ${ BLUE } ℹ ${ NC } Debug mode: ${ WHITE } ${ DEBUG :- false } ${ NC } "
2025-09-03 10:25:37 +03:00
echo -e " ${ BLUE } ℹ ${ NC } Tag format: ${ WHITE } ${ TAG_FORMAT :- full } ${ NC } "
2025-07-04 11:38:05 +03:00
echo -e " ${ BLUE } ℹ ${ NC } Allowed CIDRs: ${ WHITE } ${ CIDR_LIST [*] } ${ NC } "
echo -e " ${ PURPLE } ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ${ NC } \n "
2025-07-07 14:41:23 +02:00
if [ [ " $FORCE_SINGLE_RUN " = = "true" ] ] ; then
check
exit 0
fi
2025-07-07 14:26:07 +02:00
while true; do
check
sleep " ${ LOOP_INTERVAL :- 300 } "
done
2025-04-19 23:00:43 +05:00
}
2025-07-07 14:41:23 +02:00
2025-04-19 23:00:43 +05:00
2025-07-04 11:38:05 +03:00
2025-09-03 10:25:37 +03:00
# Simple LXC IP detection
2025-07-04 11:38:05 +03:00
get_lxc_ips( ) {
local vmid = $1
2025-09-03 10:25:37 +03:00
debug_log " lxc $vmid : starting IP detection "
2025-07-04 11:38:05 +03:00
2025-09-03 10:25:37 +03:00
# Check if LXC is running
local lxc_status = $( pct status " ${ vmid } " 2>/dev/null | awk '{print $2}' )
2025-07-04 11:38:05 +03:00
if [ [ " $lxc_status " != "running" ] ] ; then
debug_log " lxc $vmid : not running (status: $lxc_status ) "
return
fi
local ips = ""
2025-09-03 10:25:37 +03:00
# Method 1: Check Proxmox config for static IP
2025-07-04 11:38:05 +03:00
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
2025-09-03 10:25:37 +03:00
debug_log " lxc $vmid : found static IP $static_ip in config "
2025-07-04 11:38:05 +03:00
ips = " $static_ip "
fi
fi
2025-09-03 10:25:37 +03:00
# 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 "
2025-07-04 11:38:05 +03:00
fi
fi
fi
2025-09-03 10:25:37 +03:00
# Method 3: Direct container command if ARP failed
2025-07-04 11:38:05 +03:00
if [ [ -z " $ips " ] ] ; then
2025-09-03 10:25:37 +03:00
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 "
2025-07-04 11:38:05 +03:00
fi
fi
2025-09-03 10:25:37 +03:00
debug_log " lxc $vmid : final IPs: ' $ips ' "
2025-07-04 11:38:05 +03:00
echo " $ips "
}
main
EOF
2025-04-19 23:00:43 +05:00
}
2025-09-03 10:25:37 +03:00
# 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 } "
2025-04-19 23:00:43 +05:00
update_installation
exit 0
; ;
2025-09-03 10:25:37 +03:00
4)
msg_error "Action cancelled."
2025-04-19 23:00:43 +05:00
exit 0
; ;
*)
2025-09-03 10:25:37 +03:00
msg_error "Please enter 1, 2, 3, or 4."
2025-04-19 23:00:43 +05:00
; ;
2025-09-03 10:25:37 +03:00
esac
done
2025-04-19 23:00:43 +05:00
2025-09-03 10:25:37 +03:00
echo -e " \n ${ YW } This will install ${ APP } on ${ hostname } in $INSTALL_MODE mode. ${ CL } "
2025-04-19 23:00:43 +05:00
while true; do
2025-09-03 10:25:37 +03:00
read -p "Proceed? (y/n): " yn
2025-04-19 23:00:43 +05:00
case $yn in
[ Yy] *)
break
; ;
[ Nn] *)
msg_error "Installation cancelled."
exit
; ;
*)
msg_error "Please answer yes or no."
; ;
esac
done
2025-08-08 19:39:10 +02:00
if ! pveversion | grep -Eq "pve-manager/(8\.[0-4]|9\.[0-9]+)(\.[0-9]+)*" ; then
2025-04-19 23:00:43 +05:00
msg_error "This version of Proxmox Virtual Environment is not supported"
2025-09-03 10:25:37 +03:00
msg_error "⚠ Requires Proxmox Virtual Environment Version 8.0– 8.4 or 9.x."
2025-04-19 23:00:43 +05:00
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"
2025-09-03 10:25:37 +03:00
# 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"
2025-04-19 23:00:43 +05:00
2025-09-03 10:25:37 +03:00
# Migrate config if needed
migrate_config
2025-04-19 23:00:43 +05:00
2025-09-03 10:25:37 +03:00
# 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
2025-04-19 23:00:43 +05:00
2025-09-03 10:25:37 +03:00
msg_info "Setup Main Function"
2025-07-04 11:38:05 +03:00
generate_main_script >/opt/iptag/iptag
chmod +x /opt/iptag/iptag
2025-04-19 23:00:43 +05:00
msg_ok "Setup Main Function"
2025-09-03 10:25:37 +03:00
msg_info "Creating Service"
2025-07-04 11:38:05 +03:00
generate_service >/lib/systemd/system/iptag.service
2025-04-19 23:00:43 +05:00
msg_ok "Created Service"
2025-09-03 10:25:37 +03:00
msg_info "Starting Service"
systemctl daemon-reload & >/dev/null
systemctl enable -q --now iptag.service & >/dev/null
msg_ok "Started Service"
2025-07-04 11:38:05 +03:00
2025-09-03 10:25:37 +03:00
msg_info "Creating manual run command"
cat <<'EOF' >/usr/local/bin/iptag-run
2025-07-07 14:26:07 +02:00
#!/usr/bin/env bash
CONFIG_FILE = "/opt/iptag/iptag.conf"
SCRIPT_FILE = "/opt/iptag/iptag"
if [ [ ! -f " $SCRIPT_FILE " ] ] ; then
2025-09-03 10:25:37 +03:00
echo " ✗ Main script not found: $SCRIPT_FILE "
2025-07-07 14:26:07 +02:00
exit 1
fi
export FORCE_SINGLE_RUN = true
exec " $SCRIPT_FILE "
EOF
2025-09-03 10:25:37 +03:00
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
2025-07-04 11:38:05 +03:00
2025-09-03 10:25:37 +03:00
# Clean up any running spinner and exit
stop_spinner
2025-07-04 11:38:05 +03:00
exit 0