update_etc_hosts.sh:

  • by default, running in ‘dry run’ mode.
  • use --apply flag to actually change the /etc/hosts file.
  • check out the rule in SUBNET_DOMAINS to see how subnets are mapped to different domains.
#!/bin/bash
 
# This script updates /etc/hosts with entries for running PVE guests.
# It runs in DRY-RUN mode by default for safety.
# Use the --apply flag to apply changes.
 
# --- Configuration ---
HOSTS_FILE="/etc/hosts"
BACKUP_FILE="/etc/hosts.bak"
START_MARKER="# --- BEGIN PVE HOSTS ---"
END_MARKER="# --- END PVE HOSTS ---"
 
# --- SUBNET TO DOMAIN MAPPING ---
# Define the mapping from subnet prefixes to domain names.
# The script will match the START of an IP address against these prefixes.
declare -A SUBNET_DOMAINS=(
    ["192.168.1."]="lan"
    ["192.168.2."]="opn.lan"
    # ["10.10.0."]="corp.lan" # <-- Add new mappings here
)
 
# --- Runtime Variables ---
DRY_RUN=true
TEMP_HOSTS="/tmp/pve_hosts_$$"
FINAL_HOSTS_CONTENT="/tmp/final_hosts_$$"
 
# --- Argument Parsing ---
if [ "$1" == "--apply" ]; then
    DRY_RUN=false
    echo "⚡ LIVE RUN MODE ENABLED: /etc/hosts will be modified. ⚡"
else
    echo "ⓘ DRY RUN MODE ENABLED (default): No files will be changed. Use --apply to apply."
fi
 
# --- Display Mappings ---
echo -e "\nNetwork to Domain Mapping Rules:"
for subnet in "${!SUBNET_DOMAINS[@]}"; do
    echo "  - IPs starting with '${subnet}' will be assigned the '.${SUBNET_DOMAINS[$subnet]}' domain."
done
echo "" # Newline for spacing
 
# --- Cleanup Function ---
function cleanup {
    rm -f "$TEMP_HOSTS" "$FINAL_HOSTS_CONTENT"
}
trap cleanup EXIT
 
# --- Helper Function for Domain Logic ---
# Iterates through the SUBNET_DOMAINS array to find a matching domain for a given IP.
function get_domain_for_ip {
    local ip=$1
    for subnet in "${!SUBNET_DOMAINS[@]}"; do
        # Check if the IP address starts with the subnet prefix
        if [[ $ip == ${subnet}* ]]; then
            echo "${SUBNET_DOMAINS[$subnet]}"
            return
        fi
    done
    # Return empty if no match is found
}
 
# --- Main Logic ---
 
> "$TEMP_HOSTS" # Clear temp file
 
echo "Processing running guests... ⚙️"
# --- Process KVM Virtual Machines ---
for VMID in $(qm list | awk 'NR>1 {print $1}'); do
    STATUS=$(qm status "$VMID" | awk '{print $2}')
    if [ "$STATUS" != "running" ]; then continue; fi
 
    VM_NAME=$(qm config "$VMID" | grep 'name:' | awk '{print $2}')
    IP_ADDRESS=$(qm guest cmd "$VMID" network-get-interfaces 2>/dev/null | grep '"ip-address"' | grep -v '127.0.0.1' | grep -oE "\b([0-9]{1,3}\.){3}[0-9]{1,3}\b" | head -n 1)
 
    if [ -n "$IP_ADDRESS" ]; then
        DOMAIN=$(get_domain_for_ip "$IP_ADDRESS")
        HOSTNAME_ENTRY="$VM_NAME"
        if [ -n "$DOMAIN" ]; then HOSTNAME_ENTRY="$VM_NAME.$DOMAIN"; fi
        echo "  ✔️  Found VM: $VM_NAME ($VMID) with IP: $IP_ADDRESS -> Domain: ${DOMAIN:--none-}"
        echo "$IP_ADDRESS $HOSTNAME_ENTRY $VM_NAME" >> "$TEMP_HOSTS"
    fi
done
 
# --- Process LXC Containers ---
for CTID in $(pct list | awk 'NR>1 {print $1}'); do
    STATUS=$(pct status "$CTID" | awk '{print $2}')
    if [ "$STATUS" != "running" ]; then continue; fi
 
    CT_NAME=$(pct config "$CTID" | grep 'hostname:' | awk '{print $2}')
    IP_ADDRESS=$(pct exec "$CTID" ip a | grep 'inet ' | grep -v '127.0.0.1' | awk '{print $2}' | cut -d'/' -f1 | head -n 1)
 
    if [ -n "$IP_ADDRESS" ]; then
        DOMAIN=$(get_domain_for_ip "$IP_ADDRESS")
        HOSTNAME_ENTRY="$CT_NAME"
        if [ -n "$DOMAIN" ]; then HOSTNAME_ENTRY="$CT_NAME.$DOMAIN"; fi
        echo "  ✔️  Found LXC: $CT_NAME ($CTID) with IP: $IP_ADDRESS -> Domain: ${DOMAIN:--none-}"
        echo "$IP_ADDRESS $HOSTNAME_ENTRY $CT_NAME" >> "$TEMP_HOSTS"
    fi
done
 
# --- Build the new hosts file content ---
if grep -qF "$START_MARKER" "$HOSTS_FILE"; then
    sed "/$START_MARKER/q" "$HOSTS_FILE" | sed '$d' > "$FINAL_HOSTS_CONTENT"
    echo "$START_MARKER" >> "$FINAL_HOSTS_CONTENT"
    if [ -s "$TEMP_HOSTS" ]; then cat "$TEMP_HOSTS" >> "$FINAL_HOSTS_CONTENT"; fi
    echo "$END_MARKER" >> "$FINAL_HOSTS_CONTENT"
    sed -n "/$END_MARKER/,\$p" "$HOSTS_FILE" | sed '1d' >> "$FINAL_HOSTS_CONTENT"
else
    IPV6_MARKER_LINE=$(grep -n '::1' "$HOSTS_FILE" | head -n 1 | cut -d: -f1)
    NEW_BLOCK_CONTENT="\n$START_MARKER\n"
    if [ -s "$TEMP_HOSTS" ]; then NEW_BLOCK_CONTENT+=$(cat "$TEMP_HOSTS")$'\n'; fi
    NEW_BLOCK_CONTENT+="$END_MARKER\n"
    if [ -n "$IPV6_MARKER_LINE" ]; then
        head -n $((IPV6_MARKER_LINE - 1)) "$HOSTS_FILE" > "$FINAL_HOSTS_CONTENT"
        echo -e "$NEW_BLOCK_CONTENT" >> "$FINAL_HOSTS_CONTENT"
        tail -n +$IPV6_MARKER_LINE "$HOSTS_FILE" >> "$FINAL_HOSTS_CONTENT"
    else
        cat "$HOSTS_FILE" > "$FINAL_HOSTS_CONTENT"
        echo -e "$NEW_BLOCK_CONTENT" >> "$FINAL_HOSTS_CONTENT"
    fi
fi
 
# --- Final Action: Output to screen or file ---
if [ "$DRY_RUN" = true ]; then
    echo -e "\n---------- DRY RUN: Full expected content of /etc/hosts ----------"
    cat "$FINAL_HOSTS_CONTENT"
    echo "--------------------------------------------------------------"
else
    echo "Updating $HOSTS_FILE... 📝"
    if [ ! -f "$BACKUP_FILE" ]; then
        cp "$HOSTS_FILE" "$BACKUP_FILE"
        echo "Backup of $HOSTS_FILE created at $BACKUP_FILE"
    fi
    cat "$FINAL_HOSTS_CONTENT" > "$HOSTS_FILE"
    echo "✅ Hosts file updated successfully."
fi