From a00f7af0d2cecbeb5cf088c2d8f14f240d9172b9 Mon Sep 17 00:00:00 2001
From: CanbiZ <47820557+MickLesk@users.noreply.github.com>
Date: Mon, 16 Dec 2024 13:47:16 +0100
Subject: [PATCH] New Script: LXC IP-Tag (#536)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* New Script: LXC IP-Tag

* add comma in json

* Update misc/add-lxc-iptag.sh

Co-authored-by: Håvard Gjøby Thom <34199185+havardthom@users.noreply.github.com>

* Update json/add-lxc-iptag.json

Co-authored-by: Håvard Gjøby Thom <34199185+havardthom@users.noreply.github.com>

* Update json/add-lxc-iptag.json

Co-authored-by: Håvard Gjøby Thom <34199185+havardthom@users.noreply.github.com>

* remove files

* Full-Update to Single-File

* Finalo

* Update add-lxc-iptag.json

---------

Co-authored-by: Håvard Gjøby Thom <34199185+havardthom@users.noreply.github.com>
---
 json/add-lxc-iptag.json |  43 +++++
 misc/add-lxc-iptag.sh   | 351 ++++++++++++++++++++++++++++++++++++++++
 2 files changed, 394 insertions(+)
 create mode 100644 json/add-lxc-iptag.json
 create mode 100644 misc/add-lxc-iptag.sh

diff --git a/json/add-lxc-iptag.json b/json/add-lxc-iptag.json
new file mode 100644
index 000000000..1657a0acb
--- /dev/null
+++ b/json/add-lxc-iptag.json
@@ -0,0 +1,43 @@
+{
+    "name": "Proxmox VE LXC IP-Tag",
+    "slug": "add-lxc-iptag",
+    "categories": [
+        1
+    ],
+    "date_created": "2024-11-27",
+    "type": "misc",
+    "updateable": false,
+    "privileged": false,
+    "interface_port": null,
+    "documentation": null,
+    "website": null,
+    "logo": "https://raw.githubusercontent.com/home-assistant/brands/master/core_integrations/proxmoxve/icon.png",
+    "description": "This script automatically adds IP address as tags to LXC containers using a Systemd service. The service also updates the tags if a LXC IP address is changed.",
+    "install_methods": [
+        {
+            "type": "default",
+            "script": "misc/add-lxc-iptag.sh",
+            "resources": {
+                "cpu": null,
+                "ram": null,
+                "hdd": null,
+                "os": null,
+                "version": null
+            }
+        }
+    ],
+    "default_credentials": {
+        "username": null,
+        "password": null
+    },
+    "notes": [
+        {
+            "text": "Execute within the Proxmox shell",
+            "type": "Info"
+        },
+        {
+            "text": "Configuration: `nano /opt/lxc-iptag/iptag.conf`. iptag.service must be restarted after change.",
+            "type": "Info"
+        }
+    ]
+}
diff --git a/misc/add-lxc-iptag.sh b/misc/add-lxc-iptag.sh
new file mode 100644
index 000000000..093faaa7f
--- /dev/null
+++ b/misc/add-lxc-iptag.sh
@@ -0,0 +1,351 @@
+#!/usr/bin/env bash
+
+# Copyright (c) 2021-2024 community-scripts ORG
+# Author: MickLesk (Canbiz)
+# License: MIT
+# https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
+# Source: https://github.com/gitsang/lxc-iptag
+
+function header_info {
+  clear
+  cat <<"EOF"
+    __   _  ________   ________      ______           
+   / /  | |/ / ____/  /  _/ __ \    /_  __/___ _____ _
+  / /   |   / /       / // /_/ /_____/ / / __ `/ __ `/
+ / /___/   / /___   _/ // ____/_____/ / / /_/ / /_/ / 
+/_____/_/|_\____/  /___/_/         /_/  \__,_/\__, /  
+                                             /____/   
+EOF
+}
+
+clear
+header_info
+APP="LXC IP-Tag"
+hostname=$(hostname)
+
+# Farbvariablen
+YW=$(echo "\033[33m")
+GN=$(echo "\033[1;92m")
+RD=$(echo "\033[01;31m")
+CL=$(echo "\033[m")
+BFR="\\r\\033[K"
+HOLD=" "
+CM=" ✔️ ${CL}"
+CROSS=" ✖️ ${CL}"
+
+# This function enables error handling in the script by setting options and defining a trap for the ERR signal.
+catch_errors() {
+  set -Eeuo pipefail
+  trap 'error_handler $LINENO "$BASH_COMMAND"' ERR
+}
+
+# This function is called when an error occurs. It receives the exit code, line number, and command that caused the error, and displays an error message.
+error_handler() {
+  if [ -n "$SPINNER_PID" ] && ps -p $SPINNER_PID > /dev/null; then kill $SPINNER_PID > /dev/null; fi
+  printf "\e[?25h"
+  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() {
+    local frames=('⠋' '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏')
+    local spin_i=0
+    local interval=0.1 
+    printf "\e[?25l"  
+    local orange="\e[38;5;214m"
+
+    while true; do
+        printf "\r ${orange}%s\e[0m " "${frames[spin_i]}"
+        spin_i=$(( (spin_i + 1) % ${#frames[@]} ))
+        sleep "$interval"
+    done
+}
+
+# This function displays an informational message with a yellow color.
+msg_info() {
+    local msg="$1"
+    echo -ne " ${HOLD} ${YW}${msg}   "
+    spinner & 
+    SPINNER_PID=$!  
+}
+
+# This function displays a success message with a green color.
+msg_ok() {
+  if [ -n "$SPINNER_PID" ] && ps -p $SPINNER_PID > /dev/null; then kill $SPINNER_PID > /dev/null; fi
+  printf "\e[?25h"
+  local msg="$1"
+  echo -e "${BFR}${CM} ${GN}${msg}${CL}"
+}
+
+# This function displays a error message with a red color.
+msg_error() {
+  if [ -n "$SPINNER_PID" ] && ps -p $SPINNER_PID > /dev/null; then kill $SPINNER_PID > /dev/null; fi
+  printf "\e[?25h"
+  local msg="$1"
+  echo -e "${BFR}${CROSS} ${RD}${msg}${CL}"
+}
+
+while true; do
+    read -p "This will install ${APP} on ${hostname}. Proceed? (y/n): " yn
+    case $yn in
+    [Yy]*) break ;;
+    [Nn]*) msg_info "Installation cancelled."; exit ;;
+    *) msg_info "Please answer yes or no." ;;
+    esac
+done
+
+if ! pveversion | grep -Eq "pve-manager/8.[0-3]"; then
+  msg_error "This version of Proxmox Virtual Environment is not supported"
+  msg_error "⚠️ Requires Proxmox Virtual Environment Version 8.0 or later."
+  msg_error "Exiting..."
+  sleep 2
+  exit
+fi
+
+FILE_PATH="/usr/local/bin/iptag"
+if [[ -f "$FILE_PATH" ]]; then
+  msg_info "The file already exists: '$FILE_PATH'. Skipping installation."
+  exit 0
+fi
+
+msg_info "Installing Dependencies"
+apt-get update &>/dev/null
+apt-get install -y ipcalc net-tools &>/dev/null
+msg_ok "Installed Dependencies"
+
+msg_info "Setting up IP-Tag Scripts"
+mkdir -p /opt/lxc-iptag
+
+msg_info "Setup Default Config"
+if [[ ! -f /opt/lxc-iptag/iptag.conf ]]; then
+    cat <<EOF > /opt/lxc-iptag/iptag.conf
+# Configuration file for LXC IP tagging
+
+# List of allowed CIDRs
+CIDR_LIST=(
+    192.168.0.0/16
+    100.64.0.0/10
+    10.0.0.0/8
+)
+
+# Interval settings (in seconds)
+LOOP_INTERVAL=60
+FW_NET_INTERFACE_CHECK_INTERVAL=60
+LXC_STATUS_CHECK_INTERVAL=-1
+FORCE_UPDATE_INTERVAL=1800
+EOF
+    msg_ok "Setup default config"
+else
+    msg_ok "Default config already exists"
+fi
+
+msg_info "Setup Main Function"
+if [[ ! -f /opt/lxc-iptag/iptag ]]; then
+    cat <<'EOF' > /opt/lxc-iptag/iptag
+#!/bin/bash
+
+# =============== CONFIGURATION =============== #
+
+CONFIG_FILE="/opt/lxc-iptag/iptag.conf"
+
+# Load the configuration file if it exists
+if [ -f "$CONFIG_FILE" ]; then
+    # shellcheck source=./lxc-iptag.conf
+    source "$CONFIG_FILE"
+fi
+
+# Convert IP to integer for comparison
+ip_to_int() {
+    local ip="${1}"
+    local a b c d
+
+    IFS=. read -r a b c d <<< "${ip}"
+    echo "$((a << 24 | b << 16 | c << 8 | d))"
+}
+
+# Check if IP is in CIDR
+ip_in_cidr() {
+    local ip="${1}"
+    local cidr="${2}"
+
+    ip_int=$(ip_to_int "${ip}")
+    netmask_int=$(ip_to_int "$(ipcalc -b "${cidr}" | grep Broadcast | awk '{print $2}')")
+    masked_ip_int=$(( "${ip_int}" & "${netmask_int}" ))
+    [[ ${ip_int} -eq ${masked_ip_int} ]] && return 0 || return 1
+}
+
+# Check if IP is in any CIDRs
+ip_in_cidrs() {
+    local ip="${1}"
+    local cidrs=()
+
+    mapfile -t cidrs < <(echo "${2}" | tr ' ' '\n')
+    for cidr in "${cidrs[@]}"; do
+        ip_in_cidr "${ip}" "${cidr}" && return 0
+    done
+
+    return 1
+}
+
+# Check if IP is valid
+is_valid_ipv4() {
+    local ip=$1
+    local regex="^([0-9]{1,3}\.){3}[0-9]{1,3}$"
+
+    if [[ $ip =~ $regex ]]; then
+        IFS='.' read -r -a parts <<< "$ip"
+        for part in "${parts[@]}"; do
+            if ! [[ $part =~ ^[0-9]+$ ]] || ((part < 0 || part > 255)); then
+                return 1
+            fi
+        done
+        return 0
+    else
+        return 1
+    fi
+}
+
+lxc_status_changed() {
+    current_lxc_status=$(pct list 2>/dev/null)
+    if [ "${last_lxc_status}" == "${current_lxc_status}" ]; then
+        return 1
+    else
+        last_lxc_status="${current_lxc_status}"
+        return 0
+    fi
+}
+
+fw_net_interface_changed() {
+    current_net_interface=$(ifconfig | grep "^fw")
+    if [ "${last_net_interface}" == "${current_net_interface}" ]; then
+        return 1
+    else
+        last_net_interface="${current_net_interface}"
+        return 0
+    fi
+}
+
+# =============== MAIN =============== #
+
+update_lxc_iptags() {
+    vmid_list=$(pct list 2>/dev/null | grep -v VMID | awk '{print $1}')
+    for vmid in ${vmid_list}; do
+        last_tagged_ips=()
+        current_valid_ips=()
+        next_tags=()
+
+        # Parse current tags
+        mapfile -t current_tags < <(pct config "${vmid}" | grep tags | awk '{print $2}' | sed 's/;/\n/g')
+        for current_tag in "${current_tags[@]}"; do
+            if is_valid_ipv4 "${current_tag}"; then
+                last_tagged_ips+=("${current_tag}")
+                continue
+            fi
+            next_tags+=("${current_tag}")
+        done
+
+        # Get current IPs
+        current_ips_full=$(lxc-info -n "${vmid}" -i | awk '{print $2}')
+        for ip in ${current_ips_full}; do
+            if is_valid_ipv4 "${ip}" && ip_in_cidrs "${ip}" "${CIDR_LIST[*]}"; then
+                current_valid_ips+=("${ip}")
+                next_tags+=("${ip}")
+            fi
+        done
+
+        # Skip if no ip change
+        if [[ "$(echo "${last_tagged_ips[@]}" | tr ' ' '\n' | sort -u)" == "$(echo "${current_valid_ips[@]}" | tr ' ' '\n' | sort -u)" ]]; then
+            echo "Skipping ${vmid} cause ip no changes"
+            continue
+        fi
+
+        # Set tags
+        echo "Setting ${vmid} tags from ${current_tags[*]} to ${next_tags[*]}"
+        pct set "${vmid}" -tags "$(IFS=';'; echo "${next_tags[*]}")"
+    done
+}
+
+check() {
+    current_time=$(date +%s)
+
+    time_since_last_lxc_status_check=$((current_time - last_lxc_status_check_time))
+    if [[ "${LXC_STATUS_CHECK_INTERVAL}" -gt 0 ]] \
+        && [[ "${time_since_last_lxc_status_check}" -ge "${STATUS_CHECK_INTERVAL}" ]]; then
+        echo "Checking lxc status..."
+        last_lxc_status_check_time=${current_time}
+        if lxc_status_changed; then
+            update_lxc_iptags
+            last_update_time=${current_time}
+            return
+        fi
+    fi
+
+    time_since_last_fw_net_interface_check=$((current_time - last_fw_net_interface_check_time))
+    if [[ "${FW_NET_INTERFACE_CHECK_INTERVAL}" -gt 0 ]] \
+        && [[ "${time_since_last_fw_net_interface_check}" -ge "${FW_NET_INTERFACE_CHECK_INTERVAL}" ]]; then
+        echo "Checking fw net interface..."
+        last_fw_net_interface_check_time=${current_time}
+        if fw_net_interface_changed; then
+            update_lxc_iptags
+            last_update_time=${current_time}
+            return
+        fi
+    fi
+
+    time_since_last_update=$((current_time - last_update_time))
+    if [ ${time_since_last_update} -ge ${FORCE_UPDATE_INTERVAL} ]; then
+        echo "Force updating lxc iptags..."
+        update_lxc_iptags
+        last_update_time=${current_time}
+        return
+    fi
+}
+
+# main: Set the IP tags for all LXC containers
+main() {
+    while true; do
+        check
+        sleep "${LOOP_INTERVAL}"
+    done
+}
+
+main
+EOF
+    msg_ok "Setup Main Function"
+else
+    msg_ok "Main Function already exists"
+fi
+chmod +x /opt/lxc-iptag/iptag 
+
+msg_info "Creating Service"
+if [[ ! -f /lib/systemd/system/iptag.service ]]; then
+    echo "Systemd service file not found. Creating it now..."
+    cat <<EOF > /lib/systemd/system/iptag.service
+[Unit]
+Description=LXC IP-Tag service
+After=network.target
+
+[Service]
+Type=simple
+ExecStart=/opt/lxc-iptag/iptag
+Restart=always
+
+[Install]
+WantedBy=multi-user.target
+EOF
+    msg_ok "Created Service"
+else
+    msg_ok "Service already exists."
+fi
+
+msg_ok "Setup IP-Tag Scripts"
+
+msg_info "Starting Service"
+systemctl daemon-reload &>/dev/null
+systemctl enable -q --now iptag.service &>/dev/null
+msg_ok "Started Service"
+
+echo -e "\n${APP} installation completed successfully! ${CL}\n"