diff --git a/README.md b/README.md index 738a645..8160762 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,40 @@ # proxmox-ve-install-scripts +huntarr: `bash -c "$(curl -fsSL https://git.bila.li/Proxmox/proxmox-ve-install-scripts/raw/branch/dev/lxc/huntarr.sh)"` + +# Proxmox VE Install Scripts + +A collection of installation scripts for Proxmox VE containers. + +## Installation Commands + +### Huntarr + +Create a new Huntarr LXC container: + +```bash +bash -c "$(curl -fsSL https://git.bila.li/Proxmox/proxmox-ve-install-scripts/raw/branch/dev/lxc/huntarr.sh)" +``` + +Install Huntarr in an existing container: + +```bash +bash -c "$(curl -fsSL https://git.bila.li/Proxmox/proxmox-ve-install-scripts/raw/branch/dev/install/huntarr-install.sh)" +``` + +### Custom Options + +You can customize the LXC creation with environment variables: + +```bash +var_cpu=4 var_ram=4096 var_disk=16 HUNTARR_VERSION=6.2 bash -c "$(curl -fsSL https://git.bila.li/Proxmox/proxmox-ve-install-scripts/raw/branch/dev/lxc/huntarr.sh)" +``` + +#### Available Options: + +- var_cpu: Number of CPU cores +- var_ram: RAM in MB +- var_disk: Disk size in GB +- var_os: Operating system (debian, ubuntu) +- var_version: OS version (12, 11, 22.04, etc.) +- HUNTARR_VERSION: Version of Huntarr to install diff --git a/install/huntarr-install.sh b/install/huntarr-install.sh index 753929e..cf835cf 100644 --- a/install/huntarr-install.sh +++ b/install/huntarr-install.sh @@ -13,6 +13,9 @@ setting_up_container network_check update_os +APP="Huntarr" +HUNTARR_PORT="3000" + msg_info "Installing Dependencies" $STD apt-get install -y curl wget gnupg git openjdk-17-jdk nodejs npm msg_ok "Installed Dependencies" @@ -55,4 +58,7 @@ customize msg_info "Cleaning up" $STD apt-get -y autoremove $STD apt-get -y autoclean -msg_ok "Cleaned" \ No newline at end of file +msg_ok "Cleaned" + +echo -e "\n${APP} installation completed successfully!" +echo -e "\nYou can access the ${APP} web interface at http://${HOSTNAME}:${HUNTARR_PORT}" \ No newline at end of file diff --git a/lxc/huntarr.sh b/lxc/huntarr.sh index 7d1f347..457d34c 100644 --- a/lxc/huntarr.sh +++ b/lxc/huntarr.sh @@ -9,7 +9,7 @@ APP="Huntarr" var_tags="${var_tags:-arr}" var_cpu="${var_cpu:-2}" var_ram="${var_ram:-2048}" -var_disk="${var_disk:-4}" +var_disk="${var_disk:-8}" var_os="${var_os:-debian}" var_version="${var_version:-12}" var_unprivileged="${var_unprivileged:-1}" @@ -30,27 +30,18 @@ function update_script() { fi msg_info "Updating $APP LXC" - temp_file="$(mktemp)" - rm -rf /opt/huntarr - - # If Huntarr publishes releases with version tags: - RELEASE=$(curl -fsSL https://api.github.com/repos/huntarr/huntarr/releases/latest | grep "tag_name" | awk '{print substr($2, 3, length($2)-4) }') - curl -fsSL "https://github.com/huntarr/huntarr/archive/refs/tags/v${RELEASE}.tar.gz" -o "$temp_file" - $STD tar -xvzf "$temp_file" - $STD mv huntarr-${RELEASE} /opt/huntarr - - # Build process still needed for source-based installation cd /opt/huntarr + HUNTARR_VERSION="${HUNTARR_VERSION:-6.2}" + $STD git fetch --all + $STD git checkout "v${HUNTARR_VERSION}" $STD npm install $STD npm run build - $STD chown -R huntarr:huntarr /opt/huntarr - $STD chmod 775 /opt/huntarr - - $STD systemctl restart huntarr - msg_ok "Updated $APP to version ${RELEASE}" + systemctl restart huntarr + msg_ok "Updated $APP LXC" msg_info "Cleaning up" - rm -rf "$temp_file" + $STD apt-get -y autoremove + $STD apt-get -y autoclean msg_ok "Cleaned up" exit } @@ -61,55 +52,45 @@ description msg_info "Installing Dependencies" $STD apt-get update -$STD apt-get -y install curl wget gnupg git openjdk-17-jdk nodejs npm +$STD apt-get install -y curl wget gnupg git openjdk-17-jdk nodejs npm msg_ok "Installed Dependencies" -msg_info "Creating Huntarr user" -$STD useradd -r -d /var/lib/huntarr -s /bin/bash huntarr -msg_ok "Created Huntarr user" - -msg_info "Setting up directories" -$STD mkdir -p /opt/huntarr -$STD mkdir -p /var/lib/huntarr -$STD chown -R huntarr:huntarr /var/lib/huntarr -msg_ok "Set up directories" - -msg_info "Installing $APP" +msg_info "Installing Huntarr" +mkdir -p /var/lib/huntarr/ +chmod 775 /var/lib/huntarr/ $STD git clone https://github.com/huntarr/huntarr.git /opt/huntarr cd /opt/huntarr -$STD git checkout "v${var_huntarr_version}" +HUNTARR_VERSION="${HUNTARR_VERSION:-6.2}" +$STD git checkout "v${HUNTARR_VERSION}" $STD npm install $STD npm run build -$STD chown -R huntarr:huntarr /opt/huntarr -msg_ok "Installed $APP" +msg_ok "Installed Huntarr" msg_info "Creating Service" -cat < /etc/systemd/system/huntarr.service +cat </etc/systemd/system/huntarr.service [Unit] Description=Huntarr Service After=network.target [Service] +UMask=0002 +Type=simple WorkingDirectory=/opt/huntarr ExecStart=/usr/bin/npm start Restart=on-failure -User=huntarr -Group=huntarr -Environment=HOME=/var/lib/huntarr +TimeoutStopSec=20 +KillMode=process [Install] WantedBy=multi-user.target EOF - -$STD systemctl daemon-reload -$STD systemctl enable huntarr -$STD systemctl start huntarr +systemctl enable -q --now huntarr msg_ok "Created Service" -# Determine the port Huntarr is using (assuming default 3000) -HUNTARR_PORT=3000 +motd_ssh +customize msg_ok "Completed Successfully!\n" echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}" echo -e "${INFO}${YW} Access it using the following URL:${CL}" -echo -e "${TAB}${GATEWAY}${BGN}http://${IP}:${HUNTARR_PORT}${CL}" \ No newline at end of file +echo -e "${INFO}${GN} http://${BL}$(hostname)${GN}:3000${CL}\n" \ No newline at end of file diff --git a/misc/add-iptag.sh b/misc/add-iptag.sh new file mode 100644 index 0000000..657f4bc --- /dev/null +++ b/misc/add-iptag.sh @@ -0,0 +1,867 @@ +#!/usr/bin/env bash + +# Copyright (c) 2021-2025 community-scripts ORG +# Author: MickLesk (Canbiz) && Desert_Gamer +# License: MIT +# Source: https://github.com/gitsang/iptag + +function header_info { + clear + cat <<"EOF" + ___ ____ _____ +|_ _| _ \ _ |_ _|_ _ __ _ + | || |_) (_) | |/ _` |/ _` | + | || __/ _ | | (_| | (_| | +|___|_| (_) |_|\__,_|\__, | + |___/ +EOF +} + +clear +header_info +APP="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" +} + +# This function displays a spinner. +spinner() { + local frames=('⠋' '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏') + local spin_i=0 + local interval=0.1 + printf "\e[?25l" + + local color="${YWB}" + + while true; do + printf "\r ${color}%s${CL}" "${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 "${TAB}${YW}${HOLD}${msg}${HOLD}" + 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}" +} + +# Check if service exists +check_service_exists() { + if systemctl is-active --quiet iptag.service; then + return 0 + else + return 1 + fi +} + +# 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 + + # Create directory if it doesn't exist + if [[ ! -d "/opt/iptag" ]]; then + mkdir -p /opt/iptag + fi + + # Migrate config if needed + migrate_config + + # Update main script + cat <<'EOF' >/opt/iptag/iptag +#!/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 + +# 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" cidr="$2" + ipcalc -c "$ip" "$cidr" >/dev/null 2>&1 || return 1 + + local network prefix ip_parts net_parts + network=$(echo "$cidr" | cut -d/ -f1) + prefix=$(echo "$cidr" | cut -d/ -f2) + IFS=. read -r -a ip_parts <<< "$ip" + IFS=. read -r -a net_parts <<< "$network" + + case $prefix in + 8) [[ "${ip_parts[0]}" == "${net_parts[0]}" ]] ;; + 16) [[ "${ip_parts[0]}.${ip_parts[1]}" == "${net_parts[0]}.${net_parts[1]}" ]] ;; + 24) [[ "${ip_parts[0]}.${ip_parts[1]}.${ip_parts[2]}" == "${net_parts[0]}.${net_parts[1]}.${net_parts[2]}" ]] ;; + 32) [[ "$ip" == "$network" ]] ;; + *) return 1 ;; + esac +} + +# Format IP address according to the configuration +format_ip_tag() { + local ip="$1" + 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=' ' + 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" + [[ "$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 +} + +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 +} + +vm_status_changed() { + current_vm_status=$(qm list 2>/dev/null) + if [ "${last_vm_status}" == "${current_vm_status}" ]; then + return 1 + else + last_vm_status="${current_vm_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 +} + +# Get VM IPs using MAC addresses and ARP table +get_vm_ips() { + local vmid=$1 ips="" macs found_ip=false + qm status "$vmid" 2>/dev/null | grep -q "status: running" || return + + macs=$(qm config "$vmid" 2>/dev/null | grep -E 'net[0-9]+' | grep -oE '[a-fA-F0-9]{2}(:[a-fA-F0-9]{2}){5}') + [[ -z "$macs" ]] && return + + for mac in $macs; do + local ip + ip=$(arp -an 2>/dev/null | grep -i "$mac" | grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}') + [[ -n "$ip" ]] && { ips+="$ip "; found_ip=true; } + done + + if ! $found_ip; then + local agent_ip + agent_ip=$(qm agent "$vmid" network-get-interfaces 2>/dev/null | grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' || true) + [[ -n "$agent_ip" ]] && ips+="$agent_ip " + fi + + echo "${ips% }" +} + +# Update tags +update_tags() { + local type="$1" vmid="$2" config_cmd="pct" + [[ "$type" == "vm" ]] && config_cmd="qm" + + local current_ips_full + if [[ "$type" == "lxc" ]]; then + current_ips_full=$(lxc-info -n "${vmid}" -i 2>/dev/null | grep -E "^IP:" | awk '{print $2}') + else + current_ips_full=$(get_vm_ips "${vmid}") + fi + [[ -z "$current_ips_full" ]] && return + + local current_tags=() next_tags=() current_ip_tags=() + mapfile -t current_tags < <($config_cmd config "${vmid}" 2>/dev/null | grep tags | awk '{print $2}' | sed 's/;/\n/g') + + # Separate IP and non-IP 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 + + local formatted_ips=() needs_update=false added_ips=() + for ip in ${current_ips_full}; do + if is_valid_ipv4 "$ip" && ip_in_cidrs "$ip" "${CIDR_LIST[*]}"; then + local formatted_ip=$(format_ip_tag "$ip") + formatted_ips+=("$formatted_ip") + if [[ ! " ${current_ip_tags[*]} " =~ " ${formatted_ip} " ]]; then + needs_update=true + added_ips+=("$formatted_ip") + next_tags+=("$formatted_ip") + fi + fi + done + + [[ ${#formatted_ips[@]} -eq 0 ]] && return + + # Add existing IP tags that are still valid + for tag in "${current_ip_tags[@]}"; do + if [[ " ${formatted_ips[*]} " =~ " ${tag} " ]]; then + if [[ ! " ${next_tags[*]} " =~ " ${tag} " ]]; then + next_tags+=("$tag") + fi + fi + done + + if [[ "$needs_update" == true ]]; then + echo "${type^} ${vmid}: adding IP tags: ${added_ips[*]}" + $config_cmd set "${vmid}" -tags "$(IFS=';'; echo "${next_tags[*]}")" &>/dev/null + elif [[ ${#current_ip_tags[@]} -gt 0 ]]; then + echo "${type^} ${vmid}: IP tags already set: ${current_ip_tags[*]}" + else + echo "${type^} ${vmid}: setting initial IP tags: ${formatted_ips[*]}" + $config_cmd set "${vmid}" -tags "$(IFS=';'; echo "${formatted_ips[*]}")" &>/dev/null + fi +} + +# Check if status changed +check_status() { + local type="$1" current + case "$type" in + "lxc") current=$(pct list 2>/dev/null | grep -v VMID) ;; + "vm") current=$(qm list 2>/dev/null | grep -v VMID) ;; + "fw") current=$(ifconfig 2>/dev/null | grep "^fw") ;; + esac + local last_var="last_${type}_status" + [[ "${!last_var}" == "$current" ]] && return 1 + eval "$last_var='$current'" + return 0 +} + +# Update all instances +update_all() { + local type="$1" list_cmd="pct" vmids count=0 + [[ "$type" == "vm" ]] && list_cmd="qm" + + vmids=$($list_cmd list 2>/dev/null | grep -v VMID | awk '{print $1}') + for vmid in $vmids; do ((count++)); done + + echo "Found ${count} running ${type}s" + [[ $count -eq 0 ]] && return + + for vmid in $vmids; do + update_tags "$type" "$vmid" + done +} + +# Main check function +check() { + local current_time changes_detected=false + current_time=$(date +%s) + + for type in "lxc" "vm"; do + local interval_var="${type^^}_STATUS_CHECK_INTERVAL" + local last_check_var="last_${type}_check_time" + local last_update_var="last_update_${type}_time" + + if [[ "${!interval_var}" -gt 0 ]] && (( current_time - ${!last_check_var} >= ${!interval_var} )); then + echo "Checking ${type^^} status..." + eval "${last_check_var}=\$current_time" + if check_status "$type"; then + changes_detected=true + update_all "$type" + eval "${last_update_var}=\$current_time" + fi + fi + + if (( current_time - ${!last_update_var} >= FORCE_UPDATE_INTERVAL )); then + echo "Force updating ${type} tags..." + changes_detected=true + update_all "$type" + eval "${last_update_var}=\$current_time" + fi + done + + if [[ "${FW_NET_INTERFACE_CHECK_INTERVAL}" -gt 0 ]] && \ + (( current_time - last_fw_check_time >= FW_NET_INTERFACE_CHECK_INTERVAL )); then + echo "Checking network interfaces..." + last_fw_check_time=$current_time + if check_status "fw"; then + changes_detected=true + update_all "lxc" + update_all "vm" + last_update_lxc_time=$current_time + last_update_vm_time=$current_time + fi + fi + + $changes_detected || echo "No changes detected in system status" +} + +# Initialize time variables +declare -g last_lxc_status="" last_vm_status="" last_fw_status="" +declare -g last_lxc_check_time=0 last_vm_check_time=0 last_fw_check_time=0 +declare -g last_update_lxc_time=0 last_update_vm_time=0 + +# Main loop +main() { + while true; do + check + sleep "${LOOP_INTERVAL:-$DEFAULT_CHECK_INTERVAL}" + done +} + +main +EOF + chmod +x /opt/iptag/iptag + + # Update service file + cat </lib/systemd/system/iptag.service +[Unit] +Description=IP-Tag service +After=network.target + +[Service] +Type=simple +ExecStart=/opt/iptag/iptag +Restart=always + +[Install] +WantedBy=multi-user.target +EOF + + systemctl daemon-reload &>/dev/null + systemctl enable -q --now iptag.service &>/dev/null + msg_ok "Updated IP-Tag Scripts" +} + +# Main installation process +if check_service_exists; then + while true; do + read -p "IP-Tag service is already installed. Do you want to update it? (y/n): " yn + case $yn in + [Yy]*) + update_installation + exit 0 + ;; + [Nn]*) + msg_error "Installation cancelled." + exit 0 + ;; + *) + msg_error "Please answer yes or no." + ;; + esac + done +fi + +while true; do + read -p "This will install ${APP} on ${hostname}. 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](\.[0-9]+)*"; 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/iptag +msg_ok "Setup IP-Tag Scripts" + +# Migrate config if needed +migrate_config + +msg_info "Setup Default Config" +if [[ ! -f /opt/iptag/iptag.conf ]]; then + cat </opt/iptag/iptag.conf +# Configuration file for LXC IP tagging + +# List of allowed CIDRs +CIDR_LIST=( + 192.168.0.0/16 + 172.16.0.0/12 + 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="full" + +# Interval settings (in seconds) +LOOP_INTERVAL=60 +VM_STATUS_CHECK_INTERVAL=60 +FW_NET_INTERFACE_CHECK_INTERVAL=60 +LXC_STATUS_CHECK_INTERVAL=60 +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/iptag/iptag ]]; then + cat <<'EOF' >/opt/iptag/iptag +#!/bin/bash +# =============== CONFIGURATION =============== # +CONFIG_FILE="/opt/iptag/iptag.conf" + +# Load the configuration file if it exists +if [ -f "$CONFIG_FILE" ]; then + # shellcheck source=./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" + + # Use ipcalc with the -c option (check), which returns 0 if the IP is in the network + if ipcalc -c "$ip" "$cidr" >/dev/null 2>&1; then + # Get network address and mask from CIDR + local network prefix + network=$(echo "$cidr" | cut -d/ -f1) + prefix=$(echo "$cidr" | cut -d/ -f2) + + # Check if IP is in the network + local ip_a ip_b ip_c ip_d net_a net_b net_c net_d + IFS=. read -r ip_a ip_b ip_c ip_d <<< "$ip" + IFS=. read -r net_a net_b net_c net_d <<< "$network" + + # Check octets match based on prefix length + local result=0 + if (( prefix >= 8 )); then + [[ "$ip_a" != "$net_a" ]] && result=1 + fi + if (( prefix >= 16 )); then + [[ "$ip_b" != "$net_b" ]] && result=1 + fi + if (( prefix >= 24 )); then + [[ "$ip_c" != "$net_c" ]] && result=1 + fi + + return $result + fi + + return 1 +} + +# Format IP address according to the configuration +format_ip_tag() { + local ip="$1" + local format="${TAG_FORMAT:-full}" + + 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" + local cidrs="$2" + + # Check that cidrs is not empty + [[ -z "$cidrs" ]] && return 1 + + local IFS=' ' + 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" + [[ "$ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] || return 1 + local IFS='.' + read -ra parts <<< "$ip" + for part in "${parts[@]}"; do + [[ "$part" =~ ^[0-9]+$ ]] && ((part >= 0 && part <= 255)) || return 1 + done + return 0 +} + +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 +} + +vm_status_changed() { + current_vm_status=$(qm list 2>/dev/null) + if [ "${last_vm_status}" == "${current_vm_status}" ]; then + return 1 + else + last_vm_status="${current_vm_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 +} + +# Get VM IPs using MAC addresses and ARP table +get_vm_ips() { + local vmid=$1 + local ips="" + + # Check if VM is running + qm status "$vmid" 2>/dev/null | grep -q "status: running" || return + + # Get MAC addresses from VM configuration + local macs + macs=$(qm config "$vmid" 2>/dev/null | grep -E 'net[0-9]+' | grep -o -E '[a-fA-F0-9]{2}(:[a-fA-F0-9]{2}){5}') + + # Look up IPs from ARP table using MAC addresses + for mac in $macs; do + local ip + ip=$(arp -an 2>/dev/null | grep -i "$mac" | grep -o -E '([0-9]{1,3}\.){3}[0-9]{1,3}') + if [ -n "$ip" ]; then + ips+="$ip " + fi + done + + echo "$ips" +} + +# Update tags for container or VM +update_tags() { + local type="$1" + local vmid="$2" + local config_cmd="pct" + [[ "$type" == "vm" ]] && config_cmd="qm" + + # Get current IPs + local current_ips_full + if [[ "$type" == "lxc" ]]; then + # Redirect error output to suppress AppArmor warnings + current_ips_full=$(lxc-info -n "${vmid}" -i 2>/dev/null | grep -E "^IP:" | awk '{print $2}') + else + current_ips_full=$(get_vm_ips "${vmid}") + fi + + # Parse current tags and get valid IPs + local current_tags=() + local next_tags=() + mapfile -t current_tags < <($config_cmd config "${vmid}" 2>/dev/null | grep tags | awk '{print $2}' | sed 's/;/\n/g') + + for tag in "${current_tags[@]}"; do + # Skip tag if it looks like an IP (full or partial) + if ! is_valid_ipv4 "${tag}" && ! [[ "$tag" =~ ^[0-9]+(\.[0-9]+)*$ ]]; then + next_tags+=("${tag}") + fi + done + + # Add valid IPs to tags + local added_ips=() + local skipped_ips=() + + for ip in ${current_ips_full}; do + if is_valid_ipv4 "${ip}"; then + if ip_in_cidrs "${ip}" "${CIDR_LIST[*]}"; then + local formatted_ip=$(format_ip_tag "$ip") + next_tags+=("${formatted_ip}") + added_ips+=("${formatted_ip}") + else + skipped_ips+=("${ip}") + fi + fi + done + + # Log only if there are changes + if [ ${#added_ips[@]} -gt 0 ]; then + echo "${type^} ${vmid}: added IP tags: ${added_ips[*]}" + fi + + # Update if changed + if [[ "$(IFS=';'; echo "${current_tags[*]}")" != "$(IFS=';'; echo "${next_tags[*]}")" ]]; then + $config_cmd set "${vmid}" -tags "$(IFS=';'; echo "${next_tags[*]}")" &>/dev/null + fi +} + +# Check if status changed +check_status_changed() { + local type="$1" + local current_status + + case "$type" in + "lxc") + current_status=$(pct list 2>/dev/null | grep -v VMID) + [[ "${last_lxc_status}" == "${current_status}" ]] && return 1 + last_lxc_status="${current_status}" + ;; + "vm") + current_status=$(qm list 2>/dev/null | grep -v VMID) + [[ "${last_vm_status}" == "${current_status}" ]] && return 1 + last_vm_status="${current_status}" + ;; + "fw") + current_status=$(ifconfig 2>/dev/null | grep "^fw") + [[ "${last_net_interface}" == "${current_status}" ]] && return 1 + last_net_interface="${current_status}" + ;; + esac + return 0 +} + +check() { + current_time=$(date +%s) + + # Check LXC status + 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 "${LXC_STATUS_CHECK_INTERVAL}" ]]; then + echo "Checking LXC status..." + last_lxc_status_check_time=${current_time} + if check_status_changed "lxc"; then + update_all_tags "lxc" + last_update_lxc_time=${current_time} + fi + fi + + # Check VM status + time_since_last_vm_status_check=$((current_time - last_vm_status_check_time)) + if [[ "${VM_STATUS_CHECK_INTERVAL}" -gt 0 ]] \ + && [[ "${time_since_last_vm_status_check}" -ge "${VM_STATUS_CHECK_INTERVAL}" ]]; then + echo "Checking VM status..." + last_vm_status_check_time=${current_time} + if check_status_changed "vm"; then + update_all_tags "vm" + last_update_vm_time=${current_time} + fi + fi + + # Check network interface changes + 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 network interfaces..." + last_fw_net_interface_check_time=${current_time} + if check_status_changed "fw"; then + update_all_tags "lxc" + update_all_tags "vm" + last_update_lxc_time=${current_time} + last_update_vm_time=${current_time} + fi + fi + + # Force update if needed + for type in "lxc" "vm"; do + local last_update_var="last_update_${type}_time" + local time_since_last_update=$((current_time - ${!last_update_var})) + if [ ${time_since_last_update} -ge ${FORCE_UPDATE_INTERVAL} ]; then + echo "Force updating ${type} tags..." + update_all_tags "$type" + eval "${last_update_var}=${current_time}" + fi + done +} + +# Initialize time variables +last_lxc_status_check_time=0 +last_vm_status_check_time=0 +last_fw_net_interface_check_time=0 +last_update_lxc_time=0 +last_update_vm_time=0 + +# main: Set the IP tags for all LXC containers and VMs +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/iptag/iptag + +msg_info "Creating Service" +if [[ ! -f /lib/systemd/system/iptag.service ]]; then + cat </lib/systemd/system/iptag.service +[Unit] +Description=IP-Tag service +After=network.target + +[Service] +Type=simple +ExecStart=/opt/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" +SPINNER_PID="" +echo -e "\n${APP} installation completed successfully! ${CL}\n" \ No newline at end of file diff --git a/misc/api.func b/misc/api.func new file mode 100644 index 0000000..27438b9 --- /dev/null +++ b/misc/api.func @@ -0,0 +1,123 @@ +# Copyright (c) 2021-2025 community-scripts ORG +# Author: michelroegl-brunner +# License: MIT | https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/LICENSE + +post_to_api() { + + if ! command -v curl &> /dev/null; then + return + fi + + if [ "$DIAGNOSTICS" = "no" ]; then + return + fi + + if [ -z "$RANDOM_UUID" ]; then + return + fi + + local API_URL="http://api.community-scripts.org/upload" + local pve_version="not found" + pve_version=$(pveversion | awk -F'[/ ]' '{print $2}') + + JSON_PAYLOAD=$(cat < /dev/null; then + return + fi + + if [ "$POST_UPDATE_DONE" = true ]; then + return 0 + fi + local API_URL="http://api.community-scripts.org/upload/updatestatus" + local status="${1:-failed}" + local error="${2:-No error message}" + + JSON_PAYLOAD=$(cat </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}" + post_update_to_api "failed" "${command}" + echo -e "\n$error_message\n" +} + +# This function displays an informational message with logging support. +declare -A MSG_INFO_SHOWN +SPINNER_ACTIVE=0 +SPINNER_PID="" +SPINNER_MSG="" + +trap 'stop_spinner' EXIT INT TERM HUP + +start_spinner() { + local msg="$1" + local frames=(⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏) + local spin_i=0 + local interval=0.1 + + SPINNER_MSG="$msg" + printf "\r\e[2K" >&2 + + { + while [[ "$SPINNER_ACTIVE" -eq 1 ]]; do + printf "\r\e[2K%s %b" "${frames[spin_i]}" "${YW}${SPINNER_MSG}${CL}" >&2 + spin_i=$(((spin_i + 1) % ${#frames[@]})) + sleep "$interval" + done + } & + + SPINNER_PID=$! + disown "$SPINNER_PID" +} + +stop_spinner() { + if [[ ${SPINNER_PID+v} && -n "$SPINNER_PID" ]] && kill -0 "$SPINNER_PID" 2>/dev/null; then + kill "$SPINNER_PID" 2>/dev/null + sleep 0.1 + kill -0 "$SPINNER_PID" 2>/dev/null && kill -9 "$SPINNER_PID" 2>/dev/null + wait "$SPINNER_PID" 2>/dev/null || true + fi + SPINNER_ACTIVE=0 + unset SPINNER_PID +} + +spinner_guard() { + if [[ "$SPINNER_ACTIVE" -eq 1 ]] && [[ -n "$SPINNER_PID" ]]; then + kill "$SPINNER_PID" 2>/dev/null + wait "$SPINNER_PID" 2>/dev/null || true + SPINNER_ACTIVE=0 + unset SPINNER_PID + fi +} + +msg_info() { + local msg="$1" + [[ -n "${MSG_INFO_SHOWN["$msg"]+x}" ]] && return + MSG_INFO_SHOWN["$msg"]=1 + + spinner_guard + SPINNER_ACTIVE=1 + start_spinner "$msg" +} + +msg_ok() { + local msg="$1" + stop_spinner + printf "\r\e[2K%s %b\n" "${CM}" "${GN}${msg}${CL}" >&2 + unset MSG_INFO_SHOWN["$msg"] +} + +msg_error() { + stop_spinner + local msg="$1" + printf "\r\e[2K%s %b\n" "${CROSS}" "${RD}${msg}${CL}" >&2 +} + +# Check if the shell is using bash +shell_check() { + if [[ "$(basename "$SHELL")" != "bash" ]]; then + clear + msg_error "Your default shell is currently not set to Bash. To use these scripts, please switch to the Bash shell." + echo -e "\nExiting..." + sleep 2 + exit + fi +} + +# Run as root only +root_check() { + if [[ "$(id -u)" -ne 0 || $(ps -o comm= -p $PPID) == "sudo" ]]; then + clear + msg_error "Please run this script as root." + echo -e "\nExiting..." + sleep 2 + exit + fi +} + +# This function checks the version of Proxmox Virtual Environment (PVE) and exits if the version is not supported. +pve_check() { + if ! pveversion | grep -Eq "pve-manager/8\.[0-4](\.[0-9]+)*"; then + msg_error "${CROSS}${RD}This version of Proxmox Virtual Environment is not supported" + echo -e "Requires Proxmox Virtual Environment Version 8.1 or later." + echo -e "Exiting..." + sleep 2 + exit + fi +} + +# When a node is running tens of containers, it's possible to exceed the kernel's cryptographic key storage allocations. +# These are tuneable, so verify if the currently deployment is approaching the limits, advise the user on how to tune the limits, and exit the script. +# https://cleveruptime.com/docs/files/proc-key-users | https://docs.kernel.org/security/keys/core.html +maxkeys_check() { + # Read kernel parameters + per_user_maxkeys=$(cat /proc/sys/kernel/keys/maxkeys 2>/dev/null || echo 0) + per_user_maxbytes=$(cat /proc/sys/kernel/keys/maxbytes 2>/dev/null || echo 0) + + # Exit if kernel parameters are unavailable + if [[ "$per_user_maxkeys" -eq 0 || "$per_user_maxbytes" -eq 0 ]]; then + echo -e "${CROSS}${RD} Error: Unable to read kernel parameters. Ensure proper permissions.${CL}" + exit 1 + fi + + # Fetch key usage for user ID 100000 (typical for containers) + used_lxc_keys=$(awk '/100000:/ {print $2}' /proc/key-users 2>/dev/null || echo 0) + used_lxc_bytes=$(awk '/100000:/ {split($5, a, "/"); print a[1]}' /proc/key-users 2>/dev/null || echo 0) + + # Calculate thresholds and suggested new limits + threshold_keys=$((per_user_maxkeys - 100)) + threshold_bytes=$((per_user_maxbytes - 1000)) + new_limit_keys=$((per_user_maxkeys * 2)) + new_limit_bytes=$((per_user_maxbytes * 2)) + + # Check if key or byte usage is near limits + failure=0 + if [[ "$used_lxc_keys" -gt "$threshold_keys" ]]; then + echo -e "${CROSS}${RD} Warning: Key usage is near the limit (${used_lxc_keys}/${per_user_maxkeys}).${CL}" + echo -e "${INFO} Suggested action: Set ${GN}kernel.keys.maxkeys=${new_limit_keys}${CL} in ${BOLD}/etc/sysctl.d/98-community-scripts.conf${CL}." + failure=1 + fi + if [[ "$used_lxc_bytes" -gt "$threshold_bytes" ]]; then + echo -e "${CROSS}${RD} Warning: Key byte usage is near the limit (${used_lxc_bytes}/${per_user_maxbytes}).${CL}" + echo -e "${INFO} Suggested action: Set ${GN}kernel.keys.maxbytes=${new_limit_bytes}${CL} in ${BOLD}/etc/sysctl.d/98-community-scripts.conf${CL}." + failure=1 + fi + + # Provide next steps if issues are detected + if [[ "$failure" -eq 1 ]]; then + echo -e "${INFO} To apply changes, run: ${BOLD}service procps force-reload${CL}" + exit 1 + fi + + echo -e "${CM}${GN} All kernel key limits are within safe thresholds.${CL}" +} + +# This function checks the system architecture and exits if it's not "amd64". +arch_check() { + if [ "$(dpkg --print-architecture)" != "amd64" ]; then + echo -e "\n ${INFO}${YWB}This script will not work with PiMox! \n" + echo -e "\n ${YWB}Visit https://github.com/asylumexp/Proxmox for ARM64 support. \n" + echo -e "Exiting..." + sleep 2 + exit + fi +} + +# Function to get the current IP address based on the distribution +get_current_ip() { + if [ -f /etc/os-release ]; then + # Check for Debian/Ubuntu (uses hostname -I) + if grep -qE 'ID=debian|ID=ubuntu' /etc/os-release; then + CURRENT_IP=$(hostname -I | awk '{print $1}') + # Check for Alpine (uses ip command) + elif grep -q 'ID=alpine' /etc/os-release; then + CURRENT_IP=$(ip -4 addr show eth0 | awk '/inet / {print $2}' | cut -d/ -f1 | head -n 1) + else + CURRENT_IP="Unknown" + fi + fi + echo "$CURRENT_IP" +} + +# Function to update the IP address in the MOTD file +update_motd_ip() { + MOTD_FILE="/etc/motd" + + if [ -f "$MOTD_FILE" ]; then + # Remove existing IP Address lines to prevent duplication + sed -i '/IP Address:/d' "$MOTD_FILE" + + IP=$(get_current_ip) + # Add the new IP address + echo -e "${TAB}${NETWORK}${YW} IP Address: ${GN}${IP}${CL}" >>"$MOTD_FILE" + fi +} + +# Function to download & save header files +get_header() { + local app_name=$(echo "${APP,,}" | tr -d ' ') + local header_url="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/headers/${app_name}" + local local_header_path="/usr/local/community-scripts/headers/${app_name}" + + mkdir -p "$(dirname "$local_header_path")" + + if [ ! -s "$local_header_path" ]; then + if ! curl -fsSL "$header_url" -o "$local_header_path"; then + echo -e "Failed to download header for ${app_name}. No header will be displayed." + return 1 + fi + fi + + cat "$local_header_path" +} + +# This function sets the APP-Name into an ASCII Header in Slant, figlet needed on proxmox main node. +header_info() { + local app_name=$(echo "${APP,,}" | tr -d ' ') + local header_content + + # Download & save Header-File locally + header_content=$(get_header "$app_name") + if [ $? -ne 0 ]; then + # Fallback: Doesn't show Header + return 0 + fi + + # Show ASCII-Header + term_width=$(tput cols 2>/dev/null || echo 120) + clear + echo "$header_content" +} + +# This function checks if the script is running through SSH and prompts the user to confirm if they want to proceed or exit. +ssh_check() { + if [ -n "${SSH_CLIENT:+x}" ]; then + if whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "SSH DETECTED" --yesno "It's advisable to utilize the Proxmox shell rather than SSH, as there may be potential complications with variable retrieval. Proceed using SSH?" 10 72; then + whiptail --backtitle "Proxmox VE Helper Scripts" --msgbox --title "Proceed using SSH" "You've chosen to proceed using SSH. If any issues arise, please run the script in the Proxmox shell before creating a repository issue." 10 72 + else + clear + echo "Exiting due to SSH usage. Please consider using the Proxmox shell." + exit + fi + fi +} + +base_settings() { + # Default Settings + CT_TYPE="1" + DISK_SIZE="4" + CORE_COUNT="1" + RAM_SIZE="1024" + VERBOSE="${1:-no}" + PW="" + CT_ID=$NEXTID + HN=$NSAPP + BRG="vmbr0" + NET="dhcp" + GATE="" + APT_CACHER="" + APT_CACHER_IP="" + DISABLEIP6="no" + MTU="" + SD="" + NS="" + MAC="" + VLAN="" + SSH="no" + SSH_AUTHORIZED_KEY="" + TAGS="community-script;" + + # Override default settings with variables from ct script + CT_TYPE=${var_unprivileged:-$CT_TYPE} + DISK_SIZE=${var_disk:-$DISK_SIZE} + CORE_COUNT=${var_cpu:-$CORE_COUNT} + RAM_SIZE=${var_ram:-$RAM_SIZE} + VERB=${var_verbose:-$VERBOSE} + TAGS="${TAGS}${var_tags:-}" + + # Since these 2 are only defined outside of default_settings function, we add a temporary fallback. TODO: To align everything, we should add these as constant variables (e.g. OSTYPE and OSVERSION), but that would currently require updating the default_settings function for all existing scripts + if [ -z "$var_os" ]; then + var_os="debian" + fi + if [ -z "$var_version" ]; then + var_version="12" + fi +} + +# This function displays the default values for various settings. +echo_default() { + # Convert CT_TYPE to description + CT_TYPE_DESC="Unprivileged" + if [ "$CT_TYPE" -eq 0 ]; then + CT_TYPE_DESC="Privileged" + fi + + # Output the selected values with icons + echo -e "${OS}${BOLD}${DGN}Operating System: ${BGN}$var_os${CL}" + echo -e "${OSVERSION}${BOLD}${DGN}Version: ${BGN}$var_version${CL}" + echo -e "${CONTAINERTYPE}${BOLD}${DGN}Container Type: ${BGN}$CT_TYPE_DESC${CL}" + echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE} GB${CL}" + echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}${CORE_COUNT}${CL}" + echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE} MiB${CL}" + echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}${CT_ID}${CL}" + if [ "$VERB" == "yes" ]; then + echo -e "${SEARCH}${BOLD}${DGN}Verbose Mode: ${BGN}Enabled${CL}" + fi + echo -e "${CREATING}${BOLD}${BL}Creating a ${APP} LXC using the above default settings${CL}" + echo -e " " +} + +# This function is called when the user decides to exit the script. It clears the screen and displays an exit message. +exit_script() { + clear + echo -e "\n${CROSS}${RD}User exited script${CL}\n" + exit +} + +# This function allows the user to configure advanced settings for the script. +advanced_settings() { + whiptail --backtitle "Proxmox VE Helper Scripts" --msgbox --title "Here is an instructional tip:" "To make a selection, use the Spacebar." 8 58 + # Setting Default Tag for Advanced Settings + TAGS="community-script;${var_tags:-}" + CT_DEFAULT_TYPE="${CT_TYPE}" + CT_TYPE="" + while [ -z "$CT_TYPE" ]; do + if [ "$CT_DEFAULT_TYPE" == "1" ]; then + if CT_TYPE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "CONTAINER TYPE" --radiolist "Choose Type" 10 58 2 \ + "1" "Unprivileged" ON \ + "0" "Privileged" OFF \ + 3>&1 1>&2 2>&3); then + if [ -n "$CT_TYPE" ]; then + CT_TYPE_DESC="Unprivileged" + if [ "$CT_TYPE" -eq 0 ]; then + CT_TYPE_DESC="Privileged" + fi + echo -e "${OS}${BOLD}${DGN}Operating System: ${BGN}$var_os${CL}" + echo -e "${OSVERSION}${BOLD}${DGN}Version: ${BGN}$var_version${CL}" + echo -e "${CONTAINERTYPE}${BOLD}${DGN}Container Type: ${BGN}$CT_TYPE_DESC${CL}" + fi + else + exit_script + fi + fi + if [ "$CT_DEFAULT_TYPE" == "0" ]; then + if CT_TYPE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "CONTAINER TYPE" --radiolist "Choose Type" 10 58 2 \ + "1" "Unprivileged" OFF \ + "0" "Privileged" ON \ + 3>&1 1>&2 2>&3); then + if [ -n "$CT_TYPE" ]; then + CT_TYPE_DESC="Unprivileged" + if [ "$CT_TYPE" -eq 0 ]; then + CT_TYPE_DESC="Privileged" + fi + echo -e "${OS}${BOLD}${DGN}Operating System: ${BGN}$var_os${CL}" + echo -e "${OSVERSION}${BOLD}${DGN}Version: ${BGN}$var_version${CL}" + echo -e "${CONTAINERTYPE}${BOLD}${DGN}Container Type: ${BGN}$CT_TYPE_DESC${CL}" + fi + else + exit_script + fi + fi + done + + while true; do + if PW1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --passwordbox "\nSet Root Password (needed for root ssh access)" 9 58 --title "PASSWORD (leave blank for automatic login)" 3>&1 1>&2 2>&3); then + if [[ ! -z "$PW1" ]]; then + if [[ "$PW1" == *" "* ]]; then + whiptail --msgbox "Password cannot contain spaces. Please try again." 8 58 + elif [ ${#PW1} -lt 5 ]; then + whiptail --msgbox "Password must be at least 5 characters long. Please try again." 8 58 + else + if PW2=$(whiptail --backtitle "Proxmox VE Helper Scripts" --passwordbox "\nVerify Root Password" 9 58 --title "PASSWORD VERIFICATION" 3>&1 1>&2 2>&3); then + if [[ "$PW1" == "$PW2" ]]; then + PW="-password $PW1" + echo -e "${VERIFYPW}${BOLD}${DGN}Root Password: ${BGN}********${CL}" + break + else + whiptail --msgbox "Passwords do not match. Please try again." 8 58 + fi + else + exit_script + fi + fi + else + PW1="Automatic Login" + PW="" + echo -e "${VERIFYPW}${BOLD}${DGN}Root Password: ${BGN}$PW1${CL}" + break + fi + else + exit_script + fi + done + + if CT_ID=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Container ID" 8 58 "$NEXTID" --title "CONTAINER ID" 3>&1 1>&2 2>&3); then + if [ -z "$CT_ID" ]; then + CT_ID="$NEXTID" + echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}$CT_ID${CL}" + else + echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}$CT_ID${CL}" + fi + else + exit + fi + + if CT_NAME=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Hostname" 8 58 "$NSAPP" --title "HOSTNAME" 3>&1 1>&2 2>&3); then + if [ -z "$CT_NAME" ]; then + HN="$NSAPP" + else + HN=$(echo "${CT_NAME,,}" | tr -d ' ') + fi + echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}$HN${CL}" + else + exit_script + fi + + if DISK_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Disk Size in GB" 8 58 "$var_disk" --title "DISK SIZE" 3>&1 1>&2 2>&3); then + if [ -z "$DISK_SIZE" ]; then + DISK_SIZE="$var_disk" + echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE} GB${CL}" + else + if ! [[ $DISK_SIZE =~ $INTEGER ]]; then + echo -e "{INFO}${HOLD}${RD} DISK SIZE MUST BE AN INTEGER NUMBER!${CL}" + advanced_settings + fi + echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE} GB${CL}" + fi + else + exit_script + fi + + if CORE_COUNT=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Allocate CPU Cores" 8 58 "$var_cpu" --title "CORE COUNT" 3>&1 1>&2 2>&3); then + if [ -z "$CORE_COUNT" ]; then + CORE_COUNT="$var_cpu" + echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}$CORE_COUNT${CL}" + else + echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}$CORE_COUNT${CL}" + fi + else + exit_script + fi + + if RAM_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Allocate RAM in MiB" 8 58 "$var_ram" --title "RAM" 3>&1 1>&2 2>&3); then + if [ -z "$RAM_SIZE" ]; then + RAM_SIZE="$var_ram" + echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE} MiB${CL}" + else + echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE} MiB${CL}" + fi + else + exit_script + fi + + if BRG=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a Bridge" 8 58 vmbr0 --title "BRIDGE" 3>&1 1>&2 2>&3); then + if [ -z "$BRG" ]; then + BRG="vmbr0" + echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}" + else + echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}" + fi + else + exit_script + fi + + while true; do + NET=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a Static IPv4 CIDR Address (/24)" 8 58 dhcp --title "IP ADDRESS" 3>&1 1>&2 2>&3) + exit_status=$? + if [ $exit_status -eq 0 ]; then + if [ "$NET" = "dhcp" ]; then + echo -e "${NETWORK}${BOLD}${DGN}IP Address: ${BGN}$NET${CL}" + break + else + if [[ "$NET" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}/([0-9]|[1-2][0-9]|3[0-2])$ ]]; then + echo -e "${NETWORK}${BOLD}${DGN}IP Address: ${BGN}$NET${CL}" + break + else + whiptail --backtitle "Proxmox VE Helper Scripts" --msgbox "$NET is an invalid IPv4 CIDR address. Please enter a valid IPv4 CIDR address or 'dhcp'" 8 58 + fi + fi + else + exit_script + fi + done + + if [ "$NET" != "dhcp" ]; then + while true; do + GATE1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Enter gateway IP address" 8 58 --title "Gateway IP" 3>&1 1>&2 2>&3) + if [ -z "$GATE1" ]; then + whiptail --backtitle "Proxmox VE Helper Scripts" --msgbox "Gateway IP address cannot be empty" 8 58 + elif [[ ! "$GATE1" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then + whiptail --backtitle "Proxmox VE Helper Scripts" --msgbox "Invalid IP address format" 8 58 + else + GATE=",gw=$GATE1" + echo -e "${GATEWAY}${BOLD}${DGN}Gateway IP Address: ${BGN}$GATE1${CL}" + break + fi + done + else + GATE="" + echo -e "${GATEWAY}${BOLD}${DGN}Gateway IP Address: ${BGN}Default${CL}" + fi + + if [ "$var_os" == "alpine" ]; then + APT_CACHER="" + APT_CACHER_IP="" + else + if APT_CACHER_IP=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set APT-Cacher IP (leave blank for none)" 8 58 --title "APT-Cacher IP" 3>&1 1>&2 2>&3); then + APT_CACHER="${APT_CACHER_IP:+yes}" + echo -e "${NETWORK}${BOLD}${DGN}APT-Cacher IP Address: ${BGN}${APT_CACHER_IP:-Default}${CL}" + else + exit_script + fi + fi + + if (whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "IPv6" --yesno "Disable IPv6?" 10 58); then + DISABLEIP6="yes" + else + DISABLEIP6="no" + fi + echo -e "${DISABLEIPV6}${BOLD}${DGN}Disable IPv6: ${BGN}$DISABLEIP6${CL}" + + if MTU1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Interface MTU Size (leave blank for default [The MTU of your selected vmbr, default is 1500])" 8 58 --title "MTU SIZE" 3>&1 1>&2 2>&3); then + if [ -z "$MTU1" ]; then + MTU1="Default" + MTU="" + else + MTU=",mtu=$MTU1" + fi + echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}$MTU1${CL}" + else + exit_script + fi + + if SD=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a DNS Search Domain (leave blank for HOST)" 8 58 --title "DNS Search Domain" 3>&1 1>&2 2>&3); then + if [ -z "$SD" ]; then + SX=Host + SD="" + else + SX=$SD + SD="-searchdomain=$SD" + fi + echo -e "${SEARCH}${BOLD}${DGN}DNS Search Domain: ${BGN}$SX${CL}" + else + exit_script + fi + + if NX=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a DNS Server IP (leave blank for HOST)" 8 58 --title "DNS SERVER IP" 3>&1 1>&2 2>&3); then + if [ -z "$NX" ]; then + NX=Host + NS="" + else + NS="-nameserver=$NX" + fi + echo -e "${NETWORK}${BOLD}${DGN}DNS Server IP Address: ${BGN}$NX${CL}" + else + exit_script + fi + + if MAC1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a MAC Address(leave blank for generated MAC)" 8 58 --title "MAC ADDRESS" 3>&1 1>&2 2>&3); then + if [ -z "$MAC1" ]; then + MAC1="Default" + MAC="" + else + MAC=",hwaddr=$MAC1" + echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}$MAC1${CL}" + fi + else + exit_script + fi + + if VLAN1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a Vlan(leave blank for no VLAN)" 8 58 --title "VLAN" 3>&1 1>&2 2>&3); then + if [ -z "$VLAN1" ]; then + VLAN1="Default" + VLAN="" + else + VLAN=",tag=$VLAN1" + fi + echo -e "${VLANTAG}${BOLD}${DGN}Vlan: ${BGN}$VLAN1${CL}" + else + exit_script + fi + + if ADV_TAGS=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Custom Tags?[If you remove all, there will be no tags!]" 8 58 "${TAGS}" --title "Advanced Tags" 3>&1 1>&2 2>&3); then + if [ -n "${ADV_TAGS}" ]; then + ADV_TAGS=$(echo "$ADV_TAGS" | tr -d '[:space:]') + TAGS="${ADV_TAGS}" + else + TAGS=";" + fi + echo -e "${NETWORK}${BOLD}${DGN}Tags: ${BGN}$TAGS${CL}" + else + exit_script + fi + + if [[ "$PW" == -password* ]]; then + if (whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "SSH ACCESS" --yesno "Enable Root SSH Access?" 10 58); then + SSH="yes" + else + SSH="no" + fi + echo -e "${ROOTSSH}${BOLD}${DGN}Root SSH Access: ${BGN}$SSH${CL}" + else + SSH="no" + echo -e "${ROOTSSH}${BOLD}${DGN}Root SSH Access: ${BGN}$SSH${CL}" + fi + + if [[ "${SSH}" == "yes" ]]; then + SSH_AUTHORIZED_KEY="$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "SSH Authorized key for root (leave empty for none)" 8 58 --title "SSH Key" 3>&1 1>&2 2>&3)" + + if [[ -z "${SSH_AUTHORIZED_KEY}" ]]; then + echo "Warning: No SSH key provided." + fi + else + SSH_AUTHORIZED_KEY="" + fi + if (whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "VERBOSE MODE" --yesno "Enable Verbose Mode?" 10 58); then + VERB="yes" + else + VERB="no" + fi + echo -e "${SEARCH}${BOLD}${DGN}Verbose Mode: ${BGN}$VERB${CL}" + + if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "ADVANCED SETTINGS COMPLETE" --yesno "Ready to create ${APP} LXC?" 10 58); then + echo -e "${CREATING}${BOLD}${RD}Creating a ${APP} LXC using the above advanced settings${CL}" + else + clear + header_info + echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Settings on node $PVEHOST_NAME${CL}" + advanced_settings + fi +} + +diagnostics_check() { + if ! [ -d "/usr/local/community-scripts" ]; then + mkdir -p /usr/local/community-scripts + fi + + if ! [ -f "/usr/local/community-scripts/diagnostics" ]; then + if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "DIAGNOSTICS" --yesno "Send Diagnostics of LXC Installation?\n\n(This only transmits data without user data, just RAM, CPU, LXC name, ...)" 10 58); then + cat </usr/local/community-scripts/diagnostics +DIAGNOSTICS=yes + +#This file is used to store the diagnostics settings for the Community-Scripts API. +#https://github.com/community-scripts/ProxmoxVE/discussions/1836 +#Your diagnostics will be sent to the Community-Scripts API for troubleshooting/statistical purposes. +#You can review the data at https://community-scripts.github.io/ProxmoxVE/data +#If you do not wish to send diagnostics, please set the variable 'DIAGNOSTICS' to "no" in /usr/local/community-scripts/diagnostics, or use the menue. +#This will disable the diagnostics feature. +#To send diagnostics, set the variable 'DIAGNOSTICS' to "yes" in /usr/local/community-scripts/diagnostics, or use the menue. +#This will enable the diagnostics feature. +#The following information will be sent: +#"ct_type" +#"disk_size" +#"core_count" +#"ram_size" +#"os_type" +#"os_version" +#"disableip6" +#"nsapp" +#"method" +#"pve_version" +#"status" +#If you have any concerns, please review the source code at /misc/build.func +EOF + DIAGNOSTICS="yes" + else + cat </usr/local/community-scripts/diagnostics +DIAGNOSTICS=no + +#This file is used to store the diagnostics settings for the Community-Scripts API. +#https://github.com/community-scripts/ProxmoxVE/discussions/1836 +#Your diagnostics will be sent to the Community-Scripts API for troubleshooting/statistical purposes. +#You can review the data at https://community-scripts.github.io/ProxmoxVE/data +#If you do not wish to send diagnostics, please set the variable 'DIAGNOSTICS' to "no" in /usr/local/community-scripts/diagnostics, or use the menue. +#This will disable the diagnostics feature. +#To send diagnostics, set the variable 'DIAGNOSTICS' to "yes" in /usr/local/community-scripts/diagnostics, or use the menue. +#This will enable the diagnostics feature. +#The following information will be sent: +#"ct_type" +#"disk_size" +#"core_count" +#"ram_size" +#"os_type" +#"os_version" +#"disableip6" +#"nsapp" +#"method" +#"pve_version" +#"status" +#If you have any concerns, please review the source code at /misc/build.func +EOF + DIAGNOSTICS="no" + fi + else + DIAGNOSTICS=$(awk -F '=' '/^DIAGNOSTICS/ {print $2}' /usr/local/community-scripts/diagnostics) + + fi + +} + +config_file() { + + whiptail --backtitle "Proxmox VE Helper Scripts" --msgbox --title "Default distribution for $APP" "${var_os} ${var_version} \n \nIf the default Linux distribution is not adhered to, script support will be discontinued. \n" 10 58 + + CONFIG_FILE="/opt/community-scripts/.settings" + + if [[ -f "/opt/community-scripts/${NSAPP}.conf" ]]; then + CONFIG_FILE="/opt/community-scripts/${NSAPP}.conf" + fi + + if CONFIG_FILE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set absolute path to config file" 8 58 "$CONFIG_FILE" --title "CONFIG FILE" 3>&1 1>&2 2>&3); then + if [[ ! -f "$CONFIG_FILE" ]]; then + echo -e "${CROSS}${RD}Config file not found, exiting script!.${CL}" + exit + else + echo -e "${INFO}${BOLD}${DGN}Using config File: ${BGN}$CONFIG_FILE${CL}" + base_settings + source "$CONFIG_FILE" + fi + fi + + if [[ "$var_os" == "debian" ]]; then + echo -e "${OS}${BOLD}${DGN}Operating System: ${BGN}$var_os${CL}" + if [[ "$var_version" == "11" ]]; then + echo -e "${OSVERSION}${BOLD}${DGN}Version: ${BGN}$var_version${CL}" + elif [[ "$var_version" == "12" ]]; then + echo -e "${OSVERSION}${BOLD}${DGN}Version: ${BGN}$var_version${CL}" + else + msg_error "Unknown setting for var_version, should be 11 or 12, was ${var_version}" + exit + fi + elif [[ "$var_os" == "ubuntu" ]]; then + echo -e "${OS}${BOLD}${DGN}Operating System: ${BGN}$var_os${CL}" + if [[ "$var_version" == "20.04" ]]; then + echo -e "${OSVERSION}${BOLD}${DGN}Version: ${BGN}$var_version${CL}" + elif [[ "$var_version" == "22.04" ]]; then + echo -e "${OSVERSION}${BOLD}${DGN}Version: ${BGN}$var_version${CL}" + elif [[ "$var_version" == "24.04" ]]; then + echo -e "${OSVERSION}${BOLD}${DGN}Version: ${BGN}$var_version${CL}" + elif [[ "$var_version" == "24.10" ]]; then + echo -e "${OSVERSION}${BOLD}${DGN}Version: ${BGN}$var_version${CL}" + else + msg_error "Unknown setting for var_version, should be 20.04, 22.04, 24.04 or 24.10, was ${var_version}" + exit + fi + else + msg_error "Unknown setting for var_os! should be debian or ubuntu, was ${var_os}" + exit + fi + + if [[ -n "$CT_ID" ]]; then + + if [[ "$CT_ID" =~ ^([0-9]{3,4})-([0-9]{3,4})$ ]]; then + MIN_ID=${BASH_REMATCH[1]} + MAX_ID=${BASH_REMATCH[2]} + + if ((MIN_ID >= MAX_ID)); then + msg_error "Invalid Container ID range. The first number must be smaller than the second number, was ${CT_ID}" + exit + fi + + LIST_OF_IDS=$(pvesh get /cluster/resources --type vm --output-format json | grep -oP '"vmid":\s*\K\d+') + + for ((ID = MIN_ID; ID <= MAX_ID; ID++)); do + if ! grep -q "^$ID$" <<<"$LIST_OF_IDS"; then + CT_ID=$ID + break + fi + done + + echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}$CT_ID${CL}" + + elif [[ "$CT_ID" =~ ^[0-9]+$ ]]; then + + LIST_OF_IDS=$(pvesh get /cluster/resources --type vm --output-format json | grep -oP '"vmid":\s*\K\d+') + if ! grep -q "^$CT_ID$" <<<"$LIST_OF_IDS"; then + echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}$CT_ID${CL}" + else + msg_error "Container ID $CT_ID already exists" + exit + fi + else + msg_error "Invalid Container ID format. Needs to be 0000-9999 or 0-9999, was ${CT_ID}" + exit + fi + fi + + if [[ "$CT_TYPE" -eq 0 ]]; then + CT_TYPE_DESC="Privileged" + elif [[ "$CT_TYPE" -eq 1 ]]; then + CT_TYPE_DESC="Unprivileged" + else + msg_error "Unknown setting for CT_TYPE, should be 1 or 0, was ${CT_TYPE}" + exit + fi + echo -e "${CONTAINERTYPE}${BOLD}${DGN}Container Type: ${BGN}$CT_TYPE_DESC${CL}" + + if [[ ! -z "$PW" ]]; then + + if [[ "$PW" == *" "* ]]; then + msg_error "Password cannot be empty" + exit + elif [[ ${#PW} -lt 5 ]]; then + msg_error "Password must be at least 5 characters long" + exit + else + echo -e "${VERIFYPW}${BOLD}${DGN}Root Password: ${BGN}********${CL}" + fi + PW="-password $PW" + else + PW="" + echo -e "${VERIFYPW}${BOLD}${DGN}Root Password: ${BGN}Automatic Login${CL}" + fi + echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}$CT_ID${CL}" + + if [[ ! -z "$HN" ]]; then + echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}$HN${CL}" + else + msg_error "Hostname cannot be empty" + exit + fi + + if [[ ! -z "$DISK_SIZE" ]]; then + if [[ "$DISK_SIZE" =~ ^-?[0-9]+$ ]]; then + echo -e "${DISKSIZE}${BOLD}${DGN}Disk Size: ${BGN}${DISK_SIZE} GB${CL}" + else + msg_error "DISK_SIZE must be an integer, was ${DISK_SIZE}" + exit + fi + else + msg_error "DISK_SIZE cannot be empty" + exit + fi + + if [[ ! -z "$CORE_COUNT" ]]; then + if [[ "$CORE_COUNT" =~ ^-?[0-9]+$ ]]; then + echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}${CORE_COUNT}${CL}" + else + msg_error "CORE_COUNT must be an integer, was ${CORE_COUNT}" + exit + fi + else + msg_error "CORE_COUNT cannot be empty" + exit + fi + + if [[ ! -z "$RAM_SIZE" ]]; then + if [[ "$RAM_SIZE" =~ ^-?[0-9]+$ ]]; then + echo -e "${RAMSIZE}${BOLD}${DGN}RAM Size: ${BGN}${RAM_SIZE} MiB${CL}" + else + msg_error "RAM_SIZE must be an integer, was ${RAM_SIZE}" + exit + fi + else + msg_error "RAM_SIZE cannot be empty" + exit + fi + + if [[ ! -z "$BRG" ]]; then + if grep -q "^iface ${BRG}" /etc/network/interfaces; then + echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}$BRG${CL}" + else + msg_error "Bridge '${BRG}' does not exist in /etc/network/interfaces" + exit + fi + else + msg_error "Bridge cannot be empty" + exit + fi + + local ip_cidr_regex='^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})/([0-9]{1,2})$' + local ip_regex='^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$' + + if [[ ! -z $NET ]]; then + if [ "$NET" == "dhcp" ]; then + echo -e "${NETWORK}${BOLD}${DGN}IP Address: ${BGN}DHCP${CL}" + echo -e "${GATEWAY}${BOLD}${DGN}Gateway IP Address: ${BGN}Default${CL}" + elif + [[ "$NET" =~ $ip_cidr_regex ]] + then + echo -e "${NETWORK}${BOLD}${DGN}IP Address: ${BGN}$NET${CL}" + else + msg_error "Invalid IP Address format. Needs to be 0.0.0.0/0, was ${NET}" + exit + fi + fi + if [ ! -z "$GATE" ]; then + if [[ "$GATE" =~ $ip_regex ]]; then + echo -e "${GATEWAY}${BOLD}${DGN}Gateway IP Address: ${BGN}$GATE${CL}" + GATE=",gw=$GATE" + else + msg_error "Invalid IP Address format for Gateway. Needs to be 0.0.0.0, was ${GATE}" + exit + fi + else + msg_error "Gateway IP Address cannot be empty" + exit + fi + + if [[ ! -z "$APT_CACHER_IP" ]]; then + if [[ "$APT_CACHER_IP" =~ $ip_regex ]]; then + APT_CACHER="yes" + echo -e "${NETWORK}${BOLD}${DGN}APT-CACHER IP Address: ${BGN}$APT_CACHER_IP${CL}" + else + msg_error "Invalid IP Address format for APT-Cacher. Needs to be 0.0.0.0, was ${APT_CACHER_IP}" + exit + fi + fi + + if [[ "$DISABLEIP6" == "yes" ]]; then + echo -e "${DISABLEIPV6}${BOLD}${DGN}Disable IPv6: ${BGN}Yes${CL}" + elif [[ "$DISABLEIP6" == "no" ]]; then + echo -e "${DISABLEIPV6}${BOLD}${DGN}Disable IPv6: ${BGN}No${CL}" + else + msg_error "Disable IPv6 needs to be 'yes' or 'no'" + exit + fi + + if [[ ! -z "$MTU" ]]; then + if [[ "$MTU" =~ ^-?[0-9]+$ ]]; then + echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}$MTU${CL}" + MTU=",mtu=$MTU" + else + msg_error "MTU must be an integer, was ${MTU}" + exit + fi + else + MTU="" + echo -e "${DEFAULT}${BOLD}${DGN}Interface MTU Size: ${BGN}Default${CL}" + + fi + + if [[ ! -z "$SD" ]]; then + echo -e "${SEARCH}${BOLD}${DGN}DNS Search Domain: ${BGN}$SD${CL}" + SD="-searchdomain=$SD" + else + SD="" + echo -e "${SEARCH}${BOLD}${DGN}DNS Search Domain: ${BGN}HOST${CL}" + fi + + if [[ ! -z "$NS" ]]; then + if [[ "$NS" =~ $ip_regex ]]; then + echo -e "${NETWORK}${BOLD}${DGN}DNS Server IP Address: ${BGN}$NS${CL}" + NS="-nameserver=$NS" + else + msg_error "Invalid IP Address format for DNS Server. Needs to be 0.0.0.0, was ${NS}" + exit + fi + else + NS="" + echo -e "${NETWORK}${BOLD}${DGN}DNS Server IP Address: ${BGN}HOST${CL}" + fi + + if [[ ! -z "$MAC" ]]; then + if [[ "$MAC" =~ ^([A-Fa-f0-9]{2}:){5}[A-Fa-f0-9]{2}$ ]]; then + echo -e "${MACADDRESS}${BOLD}${DGN}MAC Address: ${BGN}$MAC${CL}" + MAC=",hwaddr=$MAC" + else + msg_error "MAC Address must be in the format xx:xx:xx:xx:xx:xx, was ${MAC}" + exit + fi + fi + + if [[ ! -z "$VLAN" ]]; then + if [[ "$VLAN" =~ ^-?[0-9]+$ ]]; then + echo -e "${VLANTAG}${BOLD}${DGN}Vlan: ${BGN}$VLAN${CL}" + VLAN=",tag=$VLAN" + else + msg_error "VLAN must be an integer, was ${VLAN}" + exit + fi + fi + + if [[ ! -z "$TAGS" ]]; then + echo -e "${NETWORK}${BOLD}${DGN}Tags: ${BGN}$TAGS${CL}" + fi + + if [[ "$SSH" == "yes" ]]; then + echo -e "${ROOTSSH}${BOLD}${DGN}Root SSH Access: ${BGN}$SSH${CL}" + if [[ ! -z "$SSH_AUTHORIZED_KEY" ]]; then + echo -e "${ROOTSSH}${BOLD}${DGN}SSH Authorized Key: ${BGN}********************${CL}" + else + echo -e "${ROOTSSH}${BOLD}${DGN}SSH Authorized Key: ${BGN}None${CL}" + fi + elif [[ "$SSH" == "no" ]]; then + echo -e "${ROOTSSH}${BOLD}${DGN}Root SSH Access: ${BGN}$SSH${CL}" + else + msg_error "SSH needs to be 'yes' or 'no', was ${SSH}" + exit + fi + + if [[ "$VERB" == "yes" ]]; then + echo -e "${SEARCH}${BOLD}${DGN}Verbose Mode: ${BGN}$VERB${CL}" + elif [[ "$VERB" == "no" ]]; then + echo -e "${SEARCH}${BOLD}${DGN}Verbose Mode: ${BGN}No${CL}" + else + msg_error "Verbose Mode needs to be 'yes' or 'no', was ${VERB}" + exit + fi + + if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "ADVANCED SETTINGS WITH CONFIG FILE COMPLETE" --yesno "Ready to create ${APP} LXC?" 10 58); then + echo -e "${CREATING}${BOLD}${RD}Creating a ${APP} LXC using the above settings${CL}" + else + clear + header_info + echo -e "${INFO}${HOLD} ${GN}Using Config File on node $PVEHOST_NAME${CL}" + config_file + fi + +} + +install_script() { + pve_check + shell_check + root_check + arch_check + ssh_check + maxkeys_check + diagnostics_check + + if systemctl is-active -q ping-instances.service; then + systemctl -q stop ping-instances.service + fi + NEXTID=$(pvesh get /cluster/nextid) + timezone=$(cat /etc/timezone) + header_info + while true; do + + CHOICE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "SETTINGS" --menu "Choose an option:" \ + 18 60 6 \ + "1" "Default Settings" \ + "2" "Default Settings (with verbose)" \ + "3" "Advanced Settings" \ + "4" "Use Config File" \ + "5" "Diagnostic Settings" \ + "6" "Exit" --nocancel --default-item "1" 3>&1 1>&2 2>&3) + + if [ $? -ne 0 ]; then + echo -e "${CROSS}${RD} Menu canceled. Exiting.${CL}" + exit 0 + fi + + case $CHOICE in + 1) + header_info + echo -e "${DEFAULT}${BOLD}${BL}Using Default Settings on node $PVEHOST_NAME${CL}" + VERB="no" + METHOD="default" + base_settings "$VERB" + echo_default + break + ;; + 2) + header_info + echo -e "${DEFAULT}${BOLD}${BL}Using Default Settings on node $PVEHOST_NAME (${VERBOSE_CROPPED}Verbose)${CL}" + VERB="yes" + METHOD="default" + base_settings "$VERB" + echo_default + break + ;; + 3) + header_info + echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Settings on node $PVEHOST_NAME${CL}" + METHOD="advanced" + base_settings + advanced_settings + break + ;; + 4) + header_info + echo -e "${INFO}${HOLD} ${GN}Using Config File on node $PVEHOST_NAME${CL}" + METHOD="advanced" + base_settings + config_file + break + ;; + 5) + if [[ $DIAGNOSTICS == "yes" ]]; then + if whiptail --backtitle "Proxmox VE Helper Scripts" --title "DIAGNOSTICS SETTINGS" --yesno "Send Diagnostics of LXC Installation?\n\nCurrent setting: ${DIAGNOSTICS}" 10 58 \ + --yes-button "No" --no-button "Back"; then + DIAGNOSTICS="no" + sed -i 's/^DIAGNOSTICS=.*/DIAGNOSTICS=no/' /usr/local/community-scripts/diagnostics + whiptail --backtitle "Proxmox VE Helper Scripts" --title "DIAGNOSTICS SETTINGS" --msgbox "Diagnostics settings changed to ${DIAGNOSTICS}." 8 58 + fi + else + if whiptail --backtitle "Proxmox VE Helper Scripts" --title "DIAGNOSTICS SETTINGS" --yesno "Send Diagnostics of LXC Installation?\n\nCurrent setting: ${DIAGNOSTICS}" 10 58 \ + --yes-button "Yes" --no-button "Back"; then + DIAGNOSTICS="yes" + sed -i 's/^DIAGNOSTICS=.*/DIAGNOSTICS=yes/' /usr/local/community-scripts/diagnostics + whiptail --backtitle "Proxmox VE Helper Scripts" --title "DIAGNOSTICS SETTINGS" --msgbox "Diagnostics settings changed to ${DIAGNOSTICS}." 8 58 + fi + fi + + ;; + 6) + echo -e "${CROSS}${RD}Exiting.${CL}" + exit 0 + ;; + *) + echo -e "${CROSS}${RD}Invalid option, please try again.${CL}" + ;; + esac + done +} + +check_container_resources() { + # Check actual RAM & Cores + current_ram=$(free -m | awk 'NR==2{print $2}') + current_cpu=$(nproc) + + # Check whether the current RAM is less than the required RAM or the CPU cores are less than required + if [[ "$current_ram" -lt "$var_ram" ]] || [[ "$current_cpu" -lt "$var_cpu" ]]; then + echo -e "\n${INFO}${HOLD} ${GN}Required: ${var_cpu} CPU, ${var_ram}MB RAM ${CL}| ${RD}Current: ${current_cpu} CPU, ${current_ram}MB RAM${CL}" + echo -e "${YWB}Please ensure that the ${APP} LXC is configured with at least ${var_cpu} vCPU and ${var_ram} MB RAM for the build process.${CL}\n" + echo -ne "${INFO}${HOLD} May cause data loss! ${INFO} Continue update with under-provisioned LXC? " + read -r prompt + # Check if the input is 'yes', otherwise exit with status 1 + if [[ ! ${prompt,,} =~ ^(yes)$ ]]; then + echo -e "${CROSS}${HOLD} ${YWB}Exiting based on user input.${CL}" + exit 1 + fi + else + echo -e "" + fi +} + +check_container_storage() { + # Check if the /boot partition is more than 80% full + total_size=$(df /boot --output=size | tail -n 1) + local used_size=$(df /boot --output=used | tail -n 1) + usage=$((100 * used_size / total_size)) + if ((usage > 80)); then + # Prompt the user for confirmation to continue + echo -e "${INFO}${HOLD} ${YWB}Warning: Storage is dangerously low (${usage}%).${CL}" + echo -ne "Continue anyway? " + read -r prompt + # Check if the input is 'y' or 'yes', otherwise exit with status 1 + if [[ ! ${prompt,,} =~ ^(y|yes)$ ]]; then + echo -e "${CROSS}${HOLD}${YWB}Exiting based on user input.${CL}" + exit 1 + fi + fi +} + +start() { + source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/tools.func) + if command -v pveversion >/dev/null 2>&1; then + if ! (whiptail --backtitle "Proxmox VE Helper Scripts" --title "${APP} LXC" --yesno "This will create a New ${APP} LXC. Proceed?" 10 58); then + clear + exit_script + exit + fi + SPINNER_PID="" + install_script + fi + + if ! command -v pveversion >/dev/null 2>&1; then + CHOICE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "${APP} LXC Update/Setting" --menu \ + "Support/Update functions for ${APP} LXC. Choose an option:" \ + 12 60 3 \ + "1" "YES (Silent Mode)" \ + "2" "YES (Verbose Mode)" \ + "3" "NO (Cancel Update)" --nocancel --default-item "1" 3>&1 1>&2 2>&3) + + case "$CHOICE" in + 1) + VERB="no" + set_std_mode + ;; + 2) + VERB="yes" + set_std_mode + ;; + 3) + clear + exit_script + exit + ;; + esac + + SPINNER_PID="" + update_script + fi +} + +# This function collects user settings and integrates all the collected information. +build_container() { + # if [ "$VERB" == "yes" ]; then set -x; fi + + if [ "$CT_TYPE" == "1" ]; then + FEATURES="keyctl=1,nesting=1" + else + FEATURES="nesting=1" + fi + + if [[ $DIAGNOSTICS == "yes" ]]; then + post_to_api + fi + + TEMP_DIR=$(mktemp -d) + pushd "$TEMP_DIR" >/dev/null + if [ "$var_os" == "alpine" ]; then + export FUNCTIONS_FILE_PATH="$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/alpine-install.func)" + else + export FUNCTIONS_FILE_PATH="$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/install.func)" + fi + export RANDOM_UUID="$RANDOM_UUID" + export CACHER="$APT_CACHER" + export CACHER_IP="$APT_CACHER_IP" + export tz="$timezone" + export DISABLEIPV6="$DISABLEIP6" + export APPLICATION="$APP" + export app="$NSAPP" + export PASSWORD="$PW" + export VERBOSE="$VERB" + export SSH_ROOT="${SSH}" + export SSH_AUTHORIZED_KEY + export CTID="$CT_ID" + export CTTYPE="$CT_TYPE" + export PCT_OSTYPE="$var_os" + export PCT_OSVERSION="$var_version" + export PCT_DISK_SIZE="$DISK_SIZE" + export PCT_OPTIONS=" + -features $FEATURES + -hostname $HN + -tags $TAGS + $SD + $NS + -net0 name=eth0,bridge=$BRG$MAC,ip=$NET$GATE$VLAN$MTU + -onboot 1 + -cores $CORE_COUNT + -memory $RAM_SIZE + -unprivileged $CT_TYPE + $PW + " + # This executes create_lxc.sh and creates the container and .conf file + bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/create_lxc.sh)" $? + + LXC_CONFIG=/etc/pve/lxc/${CTID}.conf + if [ "$CT_TYPE" == "0" ]; then + cat <>"$LXC_CONFIG" +# USB passthrough +lxc.cgroup2.devices.allow: a +lxc.cap.drop: +lxc.cgroup2.devices.allow: c 188:* rwm +lxc.cgroup2.devices.allow: c 189:* rwm +lxc.mount.entry: /dev/serial/by-id dev/serial/by-id none bind,optional,create=dir +lxc.mount.entry: /dev/ttyUSB0 dev/ttyUSB0 none bind,optional,create=file +lxc.mount.entry: /dev/ttyUSB1 dev/ttyUSB1 none bind,optional,create=file +lxc.mount.entry: /dev/ttyACM0 dev/ttyACM0 none bind,optional,create=file +lxc.mount.entry: /dev/ttyACM1 dev/ttyACM1 none bind,optional,create=file +EOF + fi + + if [ "$CT_TYPE" == "0" ]; then + if [[ "$APP" == "Channels" || "$APP" == "Emby" || "$APP" == "ErsatzTV" || "$APP" == "Frigate" || "$APP" == "Jellyfin" || "$APP" == "Plex" || "$APP" == "Scrypted" || "$APP" == "Tdarr" || "$APP" == "Unmanic" || "$APP" == "Ollama" || "$APP" == "FileFlows" ]]; then + cat <>"$LXC_CONFIG" +# VAAPI hardware transcoding +lxc.cgroup2.devices.allow: c 226:0 rwm +lxc.cgroup2.devices.allow: c 226:128 rwm +lxc.cgroup2.devices.allow: c 29:0 rwm +lxc.mount.entry: /dev/fb0 dev/fb0 none bind,optional,create=file +lxc.mount.entry: /dev/dri dev/dri none bind,optional,create=dir +lxc.mount.entry: /dev/dri/renderD128 dev/dri/renderD128 none bind,optional,create=file +EOF + fi + else + if [[ "$APP" == "Channels" || "$APP" == "Emby" || "$APP" == "ErsatzTV" || "$APP" == "Frigate" || "$APP" == "Jellyfin" || "$APP" == "Plex" || "$APP" == "Scrypted" || "$APP" == "Tdarr" || "$APP" == "Unmanic" || "$APP" == "Ollama" || "$APP" == "FileFlows" ]]; then + if [[ -e "/dev/dri/renderD128" ]]; then + if [[ -e "/dev/dri/card0" ]]; then + cat <>"$LXC_CONFIG" +# VAAPI hardware transcoding +dev0: /dev/dri/card0,gid=44 +dev1: /dev/dri/renderD128,gid=104 +EOF + else + cat <>"$LXC_CONFIG" +# VAAPI hardware transcoding +dev0: /dev/dri/card1,gid=44 +dev1: /dev/dri/renderD128,gid=104 +EOF + fi + fi + fi + fi + + # This starts the container and executes -install.sh + msg_info "Starting LXC Container" + pct start "$CTID" + msg_ok "Started LXC Container" + if [ "$var_os" == "alpine" ]; then + sleep 3 + pct exec "$CTID" -- /bin/sh -c 'cat </etc/apk/repositories +http://dl-cdn.alpinelinux.org/alpine/latest-stable/main +http://dl-cdn.alpinelinux.org/alpine/latest-stable/community +EOF' + pct exec "$CTID" -- ash -c "apk add bash >/dev/null" + fi + lxc-attach -n "$CTID" -- bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/install/"$var_install".sh)" $? + +} + +# This function sets the description of the container. +description() { + IP=$(pct exec "$CTID" ip a s dev eth0 | awk '/inet / {print $2}' | cut -d/ -f1) + + # Generate LXC Description + DESCRIPTION=$( + cat < + + Logo + + +

${APP} LXC

+ +

+ + spend Coffee + +

+ + + + GitHub + + + + Discussions + + + + Issues + + +EOF + ) + + # Set Description in LXC + pct set "$CTID" -description "$DESCRIPTION" + + if [[ -f /etc/systemd/system/ping-instances.service ]]; then + systemctl start ping-instances.service + fi + + post_update_to_api "done" "none" +} + +set_std_mode() { + if [ "$VERB" = "yes" ]; then + STD="" + else + STD="silent" + fi +} + +# Silent execution function +silent() { + if [ "$VERB" = "no" ]; then + "$@" >/dev/null 2>&1 || return 1 + else + "$@" || return 1 + fi +} + +exit_script() { + exit_code=$? # Capture the exit status of the last executed command + #200 exit codes indicate error in create_lxc.sh + #100 exit codes indicate error in install.func + + if [ $exit_code -ne 0 ]; then + case $exit_code in + 100) post_update_to_api "failed" "100: Unexpected error in create_lxc.sh" ;; + 101) post_update_to_api "failed" "101: No network connection detected in create_lxc.sh" ;; + 200) post_update_to_api "failed" "200: LXC creation failed in create_lxc.sh" ;; + 201) post_update_to_api "failed" "201: Invalid Storage class in create_lxc.sh" ;; + 202) post_update_to_api "failed" "202: User aborted menu in create_lxc.sh" ;; + 203) post_update_to_api "failed" "203: CTID not set in create_lxc.sh" ;; + 204) post_update_to_api "failed" "204: PCT_OSTYPE not set in create_lxc.sh" ;; + 205) post_update_to_api "failed" "205: CTID cannot be less than 100 in create_lxc.sh" ;; + 206) post_update_to_api "failed" "206: CTID already in use in create_lxc.sh" ;; + 207) post_update_to_api "failed" "207: Template not found in create_lxc.sh" ;; + 208) post_update_to_api "failed" "208: Error downloading template in create_lxc.sh" ;; + 209) post_update_to_api "failed" "209: Container creation failed, but template is intact in create_lxc.sh" ;; + *) post_update_to_api "failed" "Unknown error, exit code: $exit_code in create_lxc.sh" ;; + esac + fi +} + +trap 'exit_script' EXIT +trap 'post_update_to_api "failed" "$BASH_COMMAND"' ERR +trap 'post_update_to_api "failed" "INTERRUPTED"' SIGINT +trap 'post_update_to_api "failed" "TERMINATED"' SIGTERM \ No newline at end of file diff --git a/misc/tools.func b/misc/tools.func new file mode 100644 index 0000000..8915bf4 --- /dev/null +++ b/misc/tools.func @@ -0,0 +1,903 @@ +#!/bin/bash +install_node_and_modules() { + local NODE_VERSION="${NODE_VERSION:-22}" + local NODE_MODULE="${NODE_MODULE:-}" + local CURRENT_NODE_VERSION="" + local NEED_NODE_INSTALL=false + + # Check if Node.js is already installed + if command -v node >/dev/null; then + CURRENT_NODE_VERSION="$(node -v | grep -oP '^v\K[0-9]+')" + if [[ "$CURRENT_NODE_VERSION" != "$NODE_VERSION" ]]; then + msg_info "Node.js version $CURRENT_NODE_VERSION found, replacing with $NODE_VERSION" + NEED_NODE_INSTALL=true + else + msg_ok "Node.js $NODE_VERSION already installed" + fi + else + msg_info "Node.js not found, installing version $NODE_VERSION" + NEED_NODE_INSTALL=true + fi + + # Install Node.js if required + if [[ "$NEED_NODE_INSTALL" == true ]]; then + $STD apt-get purge -y nodejs + rm -f /etc/apt/sources.list.d/nodesource.list /etc/apt/keyrings/nodesource.gpg + + mkdir -p /etc/apt/keyrings + + if ! curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | + gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg; then + msg_error "Failed to download or import NodeSource GPG key" + exit 1 + fi + + echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_${NODE_VERSION}.x nodistro main" \ + >/etc/apt/sources.list.d/nodesource.list + + if ! apt-get update >/dev/null 2>&1; then + msg_error "Failed to update APT repositories after adding NodeSource" + exit 1 + fi + + if ! apt-get install -y nodejs >/dev/null 2>&1; then + msg_error "Failed to install Node.js ${NODE_VERSION} from NodeSource" + exit 1 + fi + + msg_ok "Installed Node.js ${NODE_VERSION}" + fi + + export NODE_OPTIONS="--max-old-space-size=4096" + + # Install global Node modules + if [[ -n "$NODE_MODULE" ]]; then + IFS=',' read -ra MODULES <<<"$NODE_MODULE" + for mod in "${MODULES[@]}"; do + local MODULE_NAME MODULE_REQ_VERSION MODULE_INSTALLED_VERSION + if [[ "$mod" == *"@"* ]]; then + MODULE_NAME="${mod%@*}" + MODULE_REQ_VERSION="${mod#*@}" + else + MODULE_NAME="$mod" + MODULE_REQ_VERSION="latest" + fi + + # Check if the module is already installed + if npm list -g --depth=0 "$MODULE_NAME" >/dev/null 2>&1; then + MODULE_INSTALLED_VERSION="$(npm list -g --depth=0 "$MODULE_NAME" | grep "$MODULE_NAME@" | awk -F@ '{print $2}' | tr -d '[:space:]')" + if [[ "$MODULE_REQ_VERSION" != "latest" && "$MODULE_REQ_VERSION" != "$MODULE_INSTALLED_VERSION" ]]; then + msg_info "Updating $MODULE_NAME from v$MODULE_INSTALLED_VERSION to v$MODULE_REQ_VERSION" + if ! $STD npm install -g "${MODULE_NAME}@${MODULE_REQ_VERSION}"; then + msg_error "Failed to update $MODULE_NAME to version $MODULE_REQ_VERSION" + exit 1 + fi + elif [[ "$MODULE_REQ_VERSION" == "latest" ]]; then + msg_info "Updating $MODULE_NAME to latest version" + if ! $STD npm install -g "${MODULE_NAME}@latest"; then + msg_error "Failed to update $MODULE_NAME to latest version" + exit 1 + fi + else + msg_ok "$MODULE_NAME@$MODULE_INSTALLED_VERSION already installed" + fi + else + msg_info "Installing $MODULE_NAME@$MODULE_REQ_VERSION" + if ! $STD npm install -g "${MODULE_NAME}@${MODULE_REQ_VERSION}"; then + msg_error "Failed to install $MODULE_NAME@$MODULE_REQ_VERSION" + exit 1 + fi + fi + done + msg_ok "All requested Node modules have been processed" + fi +} + +install_postgresql() { + local PG_VERSION="${PG_VERSION:-16}" + local CURRENT_PG_VERSION="" + local DISTRO + local NEED_PG_INSTALL=false + DISTRO="$(awk -F'=' '/^VERSION_CODENAME=/{ print $NF }' /etc/os-release)" + + if command -v psql >/dev/null; then + CURRENT_PG_VERSION="$(psql -V | grep -oP '\s\K[0-9]+(?=\.)')" + if [[ "$CURRENT_PG_VERSION" != "$PG_VERSION" ]]; then + msg_info "PostgreSQL Version $CURRENT_PG_VERSION found, replacing with $PG_VERSION" + NEED_PG_INSTALL=true + fi + else + msg_info "PostgreSQL not found, installing version $PG_VERSION" + NEED_PG_INSTALL=true + fi + + if [[ "$NEED_PG_INSTALL" == true ]]; then + msg_info "Stopping PostgreSQL if running" + systemctl stop postgresql >/dev/null 2>&1 || true + + msg_info "Removing conflicting PostgreSQL packages" + $STD apt-get purge -y "postgresql*" + rm -f /etc/apt/sources.list.d/pgdg.list /etc/apt/trusted.gpg.d/postgresql.gpg + + msg_info "Setting up PostgreSQL Repository" + curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc | + gpg --dearmor -o /etc/apt/trusted.gpg.d/postgresql.gpg + + echo "deb https://apt.postgresql.org/pub/repos/apt ${DISTRO}-pgdg main" \ + >/etc/apt/sources.list.d/pgdg.list + + $STD apt-get update + $STD apt-get install -y "postgresql-${PG_VERSION}" + + msg_ok "Installed PostgreSQL ${PG_VERSION}" + fi +} + +install_mariadb() { + local MARIADB_VERSION="${MARIADB_VERSION:-latest}" + local DISTRO_CODENAME + DISTRO_CODENAME="$(awk -F= '/^VERSION_CODENAME=/{print $2}' /etc/os-release)" + + # grab dynamic latest LTS version + if [[ "$MARIADB_VERSION" == "latest" ]]; then + msg_info "Resolving latest MariaDB version" + MARIADB_VERSION=$(curl -fsSL https://mariadb.org | grep -oP 'MariaDB \K10\.[0-9]+' | head -n1) + if [[ -z "$MARIADB_VERSION" ]]; then + msg_error "Could not determine latest MariaDB version" + return 1 + fi + msg_ok "Latest MariaDB version is $MARIADB_VERSION" + fi + + local CURRENT_VERSION="" + if command -v mariadb >/dev/null; then + CURRENT_VERSION="$(mariadb --version | grep -oP 'Ver\s+\K[0-9]+\.[0-9]+')" + fi + + if [[ "$CURRENT_VERSION" == "$MARIADB_VERSION" ]]; then + msg_info "MariaDB $MARIADB_VERSION already installed, checking for upgrade" + $STD apt-get update + $STD apt-get install --only-upgrade -y mariadb-server mariadb-client + msg_ok "MariaDB $MARIADB_VERSION upgraded if applicable" + return 0 + fi + + if [[ -n "$CURRENT_VERSION" ]]; then + msg_info "Replacing MariaDB $CURRENT_VERSION with $MARIADB_VERSION (data will be preserved)" + $STD systemctl stop mariadb >/dev/null 2>&1 || true + $STD apt-get purge -y 'mariadb*' || true + rm -f /etc/apt/sources.list.d/mariadb.list /etc/apt/trusted.gpg.d/mariadb.gpg + else + msg_info "Installing MariaDB $MARIADB_VERSION" + fi + + msg_info "Setting up MariaDB Repository" + curl -fsSL "https://mariadb.org/mariadb_release_signing_key.asc" | + gpg --dearmor -o /etc/apt/trusted.gpg.d/mariadb.gpg + + echo "deb [signed-by=/etc/apt/trusted.gpg.d/mariadb.gpg] http://mirror.mariadb.org/repo/${MARIADB_VERSION}/debian ${DISTRO_CODENAME} main" \ + >/etc/apt/sources.list.d/mariadb.list + + $STD apt-get update + $STD apt-get install -y mariadb-server mariadb-client + + msg_ok "Installed MariaDB $MARIADB_VERSION" +} + +install_mysql() { + local MYSQL_VERSION="${MYSQL_VERSION:-8.0}" + local CURRENT_VERSION="" + local NEED_INSTALL=false + + if command -v mysql >/dev/null; then + CURRENT_VERSION="$(mysql --version | grep -oP 'Distrib\s+\K[0-9]+\.[0-9]+')" + if [[ "$CURRENT_VERSION" != "$MYSQL_VERSION" ]]; then + msg_info "MySQL $CURRENT_VERSION found, replacing with $MYSQL_VERSION" + NEED_INSTALL=true + else + msg_ok "MySQL $MYSQL_VERSION already installed" + fi + else + msg_info "MySQL not found, installing version $MYSQL_VERSION" + NEED_INSTALL=true + fi + + if [[ "$NEED_INSTALL" == true ]]; then + msg_info "Removing conflicting MySQL packages" + $STD systemctl stop mysql >/dev/null 2>&1 || true + $STD apt-get purge -y 'mysql*' + rm -f /etc/apt/sources.list.d/mysql.list /etc/apt/trusted.gpg.d/mysql.gpg + + msg_info "Setting up MySQL APT Repository" + DISTRO_CODENAME="$(awk -F= '/VERSION_CODENAME/ { print $2 }' /etc/os-release)" + curl -fsSL https://repo.mysql.com/RPM-GPG-KEY-mysql-2022 | gpg --dearmor -o /etc/apt/trusted.gpg.d/mysql.gpg + echo "deb [signed-by=/etc/apt/trusted.gpg.d/mysql.gpg] https://repo.mysql.com/apt/debian/ ${DISTRO_CODENAME} mysql-${MYSQL_VERSION}" \ + >/etc/apt/sources.list.d/mysql.list + + $STD apt-get update + $STD apt-get install -y mysql-server + + msg_ok "Installed MySQL $MYSQL_VERSION" + fi +} + +install_php() { + local PHP_VERSION="${PHP_VERSION:-8.4}" + local PHP_MODULE="${PHP_MODULE:-}" + local PHP_APACHE="${PHP_APACHE:-NO}" + local PHP_FPM="${PHP_FPM:-NO}" + local DEFAULT_MODULES="bcmath,cli,curl,gd,intl,mbstring,opcache,readline,xml,zip" + local COMBINED_MODULES + + local PHP_MEMORY_LIMIT="${PHP_MEMORY_LIMIT:-512M}" + local PHP_UPLOAD_MAX_FILESIZE="${PHP_UPLOAD_MAX_FILESIZE:-128M}" + local PHP_POST_MAX_SIZE="${PHP_POST_MAX_SIZE:-128M}" + local PHP_MAX_EXECUTION_TIME="${PHP_MAX_EXECUTION_TIME:-300}" + + # Merge default + user-defined modules + if [[ -n "$PHP_MODULE" ]]; then + COMBINED_MODULES="${DEFAULT_MODULES},${PHP_MODULE}" + else + COMBINED_MODULES="${DEFAULT_MODULES}" + fi + + # Deduplicate modules + COMBINED_MODULES=$(echo "$COMBINED_MODULES" | tr ',' '\n' | awk '!seen[$0]++' | paste -sd, -) + + local CURRENT_PHP + CURRENT_PHP=$(php -v 2>/dev/null | awk '/^PHP/{print $2}' | cut -d. -f1,2) + + if [[ "$CURRENT_PHP" != "$PHP_VERSION" ]]; then + $STD echo "PHP $CURRENT_PHP detected, migrating to PHP $PHP_VERSION" + if [[ ! -f /etc/apt/sources.list.d/php.list ]]; then + $STD curl -fsSLo /tmp/debsuryorg-archive-keyring.deb https://packages.sury.org/debsuryorg-archive-keyring.deb + $STD dpkg -i /tmp/debsuryorg-archive-keyring.deb + echo "deb [signed-by=/usr/share/keyrings/deb.sury.org-php.gpg] https://packages.sury.org/php/ $(lsb_release -sc) main" \ + >/etc/apt/sources.list.d/php.list + $STD apt-get update + fi + + $STD apt-get purge -y "php${CURRENT_PHP//./}"* || true + fi + + local MODULE_LIST="php${PHP_VERSION}" + IFS=',' read -ra MODULES <<<"$COMBINED_MODULES" + for mod in "${MODULES[@]}"; do + MODULE_LIST+=" php${PHP_VERSION}-${mod}" + done + + if [[ "$PHP_APACHE" == "YES" ]]; then + # Optionally disable old Apache PHP module + if [[ -f /etc/apache2/mods-enabled/php${CURRENT_PHP}.load ]]; then + $STD a2dismod php${CURRENT_PHP} || true + fi + fi + + if [[ "$PHP_FPM" == "YES" ]]; then + $STD systemctl stop php${CURRENT_PHP}-fpm || true + $STD systemctl disable php${CURRENT_PHP}-fpm || true + fi + + $STD apt-get install -y $MODULE_LIST + msg_ok "Installed PHP $PHP_VERSION with selected modules" + + if [[ "$PHP_APACHE" == "YES" ]]; then + $STD systemctl restart apache2 || true + fi + + if [[ "$PHP_FPM" == "YES" ]]; then + $STD systemctl enable php${PHP_VERSION}-fpm + $STD systemctl restart php${PHP_VERSION}-fpm + fi + + # Patch all relevant php.ini files + local PHP_INI_PATHS=() + PHP_INI_PATHS+=("/etc/php/${PHP_VERSION}/cli/php.ini") + [[ "$PHP_FPM" == "YES" ]] && PHP_INI_PATHS+=("/etc/php/${PHP_VERSION}/fpm/php.ini") + [[ "$PHP_APACHE" == "YES" ]] && PHP_INI_PATHS+=("/etc/php/${PHP_VERSION}/apache2/php.ini") + + for ini in "${PHP_INI_PATHS[@]}"; do + if [[ -f "$ini" ]]; then + msg_info "Patching $ini" + sed -i "s|^memory_limit = .*|memory_limit = ${PHP_MEMORY_LIMIT}|" "$ini" + sed -i "s|^upload_max_filesize = .*|upload_max_filesize = ${PHP_UPLOAD_MAX_FILESIZE}|" "$ini" + sed -i "s|^post_max_size = .*|post_max_size = ${PHP_POST_MAX_SIZE}|" "$ini" + sed -i "s|^max_execution_time = .*|max_execution_time = ${PHP_MAX_EXECUTION_TIME}|" "$ini" + msg_ok "Patched $ini" + fi + done +} + +install_composer() { + local COMPOSER_BIN="/usr/local/bin/composer" + export COMPOSER_ALLOW_SUPERUSER=1 + + # Check if composer is already installed + if [[ -x "$COMPOSER_BIN" ]]; then + local CURRENT_VERSION + CURRENT_VERSION=$("$COMPOSER_BIN" --version | awk '{print $3}') + msg_info "Composer $CURRENT_VERSION found, updating to latest" + else + msg_info "Composer not found, installing latest version" + fi + + # Download and install latest composer + curl -fsSL https://getcomposer.org/installer -o /tmp/composer-setup.php + php /tmp/composer-setup.php --install-dir=/usr/local/bin --filename=composer >/dev/null 2>&1 + + if [[ $? -ne 0 ]]; then + msg_error "Failed to install Composer" + return 1 + fi + + chmod +x "$COMPOSER_BIN" + msg_ok "Installed Composer $($COMPOSER_BIN --version | awk '{print $3}')" +} + +install_go() { + local ARCH + case "$(uname -m)" in + x86_64) ARCH="amd64" ;; + aarch64) ARCH="arm64" ;; + *) + msg_error "Unsupported architecture: $(uname -m)" + return 1 + ;; + esac + + # Determine version + if [[ -z "${GO_VERSION:-}" || "${GO_VERSION}" == "latest" ]]; then + GO_VERSION=$(curl -fsSL https://go.dev/VERSION?m=text | head -n1 | sed 's/^go//') + if [[ -z "$GO_VERSION" ]]; then + msg_error "Could not determine latest Go version" + return 1 + fi + msg_info "Detected latest Go version: $GO_VERSION" + fi + + local GO_BIN="/usr/local/bin/go" + local GO_INSTALL_DIR="/usr/local/go" + + if [[ -x "$GO_BIN" ]]; then + local CURRENT_VERSION + CURRENT_VERSION=$("$GO_BIN" version | awk '{print $3}' | sed 's/go//') + if [[ "$CURRENT_VERSION" == "$GO_VERSION" ]]; then + msg_ok "Go $GO_VERSION already installed" + return 0 + else + msg_info "Go $CURRENT_VERSION found, upgrading to $GO_VERSION" + rm -rf "$GO_INSTALL_DIR" + fi + else + msg_info "Installing Go $GO_VERSION" + fi + + local TARBALL="go${GO_VERSION}.linux-${ARCH}.tar.gz" + local URL="https://go.dev/dl/${TARBALL}" + local TMP_TAR=$(mktemp) + + curl -fsSL "$URL" -o "$TMP_TAR" || { + msg_error "Failed to download $TARBALL" + return 1 + } + + tar -C /usr/local -xzf "$TMP_TAR" + ln -sf /usr/local/go/bin/go /usr/local/bin/go + ln -sf /usr/local/go/bin/gofmt /usr/local/bin/gofmt + rm -f "$TMP_TAR" + + msg_ok "Installed Go $GO_VERSION" +} + +install_java() { + local JAVA_VERSION="${JAVA_VERSION:-21}" + local DISTRO_CODENAME + DISTRO_CODENAME=$(awk -F= '/VERSION_CODENAME/ { print $2 }' /etc/os-release) + local DESIRED_PACKAGE="temurin-${JAVA_VERSION}-jdk" + + # Add Adoptium repo if missing + if [[ ! -f /etc/apt/sources.list.d/adoptium.list ]]; then + msg_info "Setting up Adoptium Repository" + mkdir -p /etc/apt/keyrings + curl -fsSL "https://packages.adoptium.net/artifactory/api/gpg/key/public" | gpg --dearmor -o /etc/apt/trusted.gpg.d/adoptium.gpg + echo "deb [signed-by=/etc/apt/trusted.gpg.d/adoptium.gpg] https://packages.adoptium.net/artifactory/deb ${DISTRO_CODENAME} main" \ + >/etc/apt/sources.list.d/adoptium.list + $STD apt-get update + msg_ok "Set up Adoptium Repository" + fi + + # Detect currently installed temurin version + local INSTALLED_VERSION="" + if dpkg -l | grep -q "temurin-.*-jdk"; then + INSTALLED_VERSION=$(dpkg -l | awk '/temurin-.*-jdk/{print $2}' | grep -oP 'temurin-\K[0-9]+') + fi + + if [[ "$INSTALLED_VERSION" == "$JAVA_VERSION" ]]; then + msg_info "Temurin JDK $JAVA_VERSION already installed, updating if needed" + $STD apt-get update + $STD apt-get install --only-upgrade -y "$DESIRED_PACKAGE" + msg_ok "Updated Temurin JDK $JAVA_VERSION (if applicable)" + else + if [[ -n "$INSTALLED_VERSION" ]]; then + msg_info "Removing Temurin JDK $INSTALLED_VERSION" + $STD apt-get purge -y "temurin-${INSTALLED_VERSION}-jdk" + fi + + msg_info "Installing Temurin JDK $JAVA_VERSION" + $STD apt-get install -y "$DESIRED_PACKAGE" + msg_ok "Installed Temurin JDK $JAVA_VERSION" + fi +} + +install_mongodb() { + local MONGO_VERSION="${MONGO_VERSION:-8.0}" + local DISTRO_CODENAME + DISTRO_CODENAME=$(awk -F= '/VERSION_CODENAME/ { print $2 }' /etc/os-release) + local REPO_LIST="/etc/apt/sources.list.d/mongodb-org-${MONGO_VERSION}.list" + + # Aktuell installierte Major-Version ermitteln + local INSTALLED_VERSION="" + if command -v mongod >/dev/null; then + INSTALLED_VERSION=$(mongod --version | awk '/db version/{print $3}' | cut -d. -f1,2) + fi + + if [[ "$INSTALLED_VERSION" == "$MONGO_VERSION" ]]; then + msg_info "MongoDB $MONGO_VERSION already installed, checking for upgrade" + $STD apt-get update + $STD apt-get install --only-upgrade -y mongodb-org + msg_ok "MongoDB $MONGO_VERSION upgraded if needed" + return 0 + fi + + # Ältere Version entfernen (nur Packages, nicht Daten!) + if [[ -n "$INSTALLED_VERSION" ]]; then + msg_info "Replacing MongoDB $INSTALLED_VERSION with $MONGO_VERSION (data will be preserved)" + $STD systemctl stop mongod || true + $STD apt-get purge -y mongodb-org || true + rm -f /etc/apt/sources.list.d/mongodb-org-*.list + rm -f /etc/apt/trusted.gpg.d/mongodb-*.gpg + else + msg_info "Installing MongoDB $MONGO_VERSION" + fi + + # MongoDB Repo hinzufügen + curl -fsSL "https://pgp.mongodb.com/server-${MONGO_VERSION}.asc" | gpg --dearmor -o "/etc/apt/trusted.gpg.d/mongodb-${MONGO_VERSION}.gpg" + echo "deb [signed-by=/etc/apt/trusted.gpg.d/mongodb-${MONGO_VERSION}.gpg] https://repo.mongodb.org/apt/debian ${DISTRO_CODENAME}/mongodb-org/${MONGO_VERSION} main" \ + >"$REPO_LIST" + + $STD apt-get update + $STD apt-get install -y mongodb-org + + # Sicherstellen, dass Datenverzeichnis intakt bleibt + mkdir -p /var/lib/mongodb + chown -R mongodb:mongodb /var/lib/mongodb + + $STD systemctl enable mongod + $STD systemctl start mongod + msg_ok "MongoDB $MONGO_VERSION installed and started" +} + +fetch_and_deploy_gh_release() { + local repo="$1" + local app=$(echo ${APPLICATION,,} | tr -d ' ') + local api_url="https://api.github.com/repos/$repo/releases/latest" + local header=() + local attempt=0 + local max_attempts=3 + local api_response tag http_code + local current_version="" + local curl_timeout="--connect-timeout 10 --max-time 30" + + # Check if the app directory exists and if there's a version file + if [[ -f "/opt/${app}_version.txt" ]]; then + current_version=$(cat "/opt/${app}_version.txt") + $STD msg_info "Current version: $current_version" + fi + + # ensure that jq is installed + if ! command -v jq &>/dev/null; then + $STD msg_info "Installing jq..." + $STD apt-get update -qq &>/dev/null + $STD apt-get install -y jq &>/dev/null || { + msg_error "Failed to install jq" + return 1 + } + fi + + [[ -n "${GITHUB_TOKEN:-}" ]] && header=(-H "Authorization: token $GITHUB_TOKEN") + + until [[ $attempt -ge $max_attempts ]]; do + ((attempt++)) || true + $STD msg_info "[$attempt/$max_attempts] Fetching GitHub release for $repo...\n" + + api_response=$(curl $curl_timeout -fsSL -w "%{http_code}" -o /tmp/gh_resp.json "${header[@]}" "$api_url") + http_code="${api_response:(-3)}" + + if [[ "$http_code" == "404" ]]; then + msg_error "Repository $repo has no Release candidate (404)" + return 1 + fi + + if [[ "$http_code" != "200" ]]; then + $STD msg_info "Request failed with HTTP $http_code, retrying...\n" + sleep $((attempt * 2)) + continue + fi + + api_response=$(/dev/null; then + msg_error "Repository not found: $repo" + return 1 + fi + + tag=$(echo "$api_response" | jq -r '.tag_name // .name // empty') + [[ "$tag" =~ ^v[0-9] ]] && tag="${tag:1}" + + if [[ -z "$tag" ]]; then + $STD msg_info "Empty tag received, retrying...\n" + sleep $((attempt * 2)) + continue + fi + + $STD msg_ok "Found release: $tag for $repo" + break + done + + if [[ -z "$tag" ]]; then + msg_error "Failed to fetch release for $repo after $max_attempts attempts." + exit 1 + fi + + # Version comparison (if we already have this version, skip) + if [[ "$current_version" == "$tag" ]]; then + $STD msg_info "Already running the latest version ($tag). Skipping update." + return 0 + fi + + local version="$tag" + local base_url="https://github.com/$repo/releases/download/v$tag" + local tmpdir + tmpdir=$(mktemp -d) || return 1 + + # Extract list of assets from the Release API + local assets urls + assets=$(echo "$api_response" | jq -r '.assets[].browser_download_url') || true + + # Detect current architecture + local arch + if command -v dpkg &>/dev/null; then + arch=$(dpkg --print-architecture) + elif command -v uname &>/dev/null; then + case "$(uname -m)" in + x86_64) arch="amd64" ;; + aarch64) arch="arm64" ;; + armv7l) arch="armv7" ;; + armv6l) arch="armv6" ;; + *) arch="unknown" ;; + esac + else + arch="unknown" + fi + $STD msg_info "Detected system architecture: $arch" + + # Try to find a matching asset for our architecture + local url="" + for u in $assets; do + if [[ "$u" =~ $arch.*\.tar\.gz$ ]]; then + url="$u" + $STD msg_info "Found matching architecture asset: $url" + break + fi + done + + # Fallback to other architectures if our specific one isn't found + if [[ -z "$url" ]]; then + for u in $assets; do + if [[ "$u" =~ (x86_64|amd64|arm64|armv7|armv6).*\.tar\.gz$ ]]; then + url="$u" + $STD msg_info "Architecture-specific asset not found, using: $url" + break + fi + done + fi + + # Fallback to any tar.gz + if [[ -z "$url" ]]; then + for u in $assets; do + if [[ "$u" =~ \.tar\.gz$ ]]; then + url="$u" + $STD msg_info "Using generic tarball: $url" + break + fi + done + fi + + # Final fallback to GitHub source tarball + if [[ -z "$url" ]]; then + url="https://github.com/$repo/archive/refs/tags/$version.tar.gz" + $STD msg_info "Trying GitHub source tarball fallback: $url" + fi + + local filename="${url##*/}" + $STD msg_info "Downloading $url" + + if ! curl $curl_timeout -fsSL -o "$tmpdir/$filename" "$url"; then + msg_error "Failed to download release asset from $url" + rm -rf "$tmpdir" + return 1 + fi + + mkdir -p "/opt/$app" + + tar -xzf "$tmpdir/$filename" -C "$tmpdir" + local content_root + content_root=$(find "$tmpdir" -mindepth 1 -maxdepth 1 -type d) + if [[ $(echo "$content_root" | wc -l) -eq 1 ]]; then + cp -r "$content_root"/* "/opt/$app/" + else + cp -r "$tmpdir"/* "/opt/$app/" + fi + + echo "$version" >"/opt/${app}_version.txt" + $STD msg_ok "Deployed $app v$version to /opt/$app" + rm -rf "$tmpdir" +} + +setup_local_ip_helper() { + local BASE_DIR="/usr/local/community-scripts/ip-management" + local SCRIPT_PATH="$BASE_DIR/update_local_ip.sh" + local IP_FILE="/run/local-ip.env" + local DISPATCHER_SCRIPT="/etc/networkd-dispatcher/routable.d/10-update-local-ip.sh" + + mkdir -p "$BASE_DIR" + + # Install networkd-dispatcher if not present + if ! dpkg -s networkd-dispatcher >/dev/null 2>&1; then + apt-get update -qq + apt-get install -yq networkd-dispatcher + fi + + # Write update_local_ip.sh + cat <<'EOF' >"$SCRIPT_PATH" +#!/bin/bash +set -euo pipefail + +IP_FILE="/run/local-ip.env" +mkdir -p "$(dirname "$IP_FILE")" + +get_current_ip() { + local targets=("8.8.8.8" "1.1.1.1" "192.168.1.1" "10.0.0.1" "172.16.0.1" "default") + local ip + + for target in "${targets[@]}"; do + if [[ "$target" == "default" ]]; then + ip=$(ip route get 1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if ($i=="src") print $(i+1)}') + else + ip=$(ip route get "$target" 2>/dev/null | awk '{for(i=1;i<=NF;i++) if ($i=="src") print $(i+1)}') + fi + if [[ -n "$ip" ]]; then + echo "$ip" + return 0 + fi + done + + return 1 +} + +current_ip="$(get_current_ip)" + +if [[ -z "$current_ip" ]]; then + echo "[ERROR] Could not detect local IP" >&2 + exit 1 +fi + +if [[ -f "$IP_FILE" ]]; then + source "$IP_FILE" + [[ "$LOCAL_IP" == "$current_ip" ]] && exit 0 +fi + +echo "LOCAL_IP=$current_ip" > "$IP_FILE" +echo "[INFO] LOCAL_IP updated to $current_ip" +EOF + + chmod +x "$SCRIPT_PATH" + + # Install dispatcher hook + mkdir -p "$(dirname "$DISPATCHER_SCRIPT")" + cat <"$DISPATCHER_SCRIPT" +#!/bin/bash +$SCRIPT_PATH +EOF + + chmod +x "$DISPATCHER_SCRIPT" + systemctl enable --now networkd-dispatcher.service + + $STD msg_ok "LOCAL_IP helper installed using networkd-dispatcher" +} + +import_local_ip() { + local IP_FILE="/run/local-ip.env" + if [[ -f "$IP_FILE" ]]; then + # shellcheck disable=SC1090 + source "$IP_FILE" + fi + + if [[ -z "${LOCAL_IP:-}" ]]; then + get_current_ip() { + local targets=("8.8.8.8" "1.1.1.1" "192.168.1.1" "10.0.0.1" "172.16.0.1" "default") + local ip + + for target in "${targets[@]}"; do + if [[ "$target" == "default" ]]; then + ip=$(ip route get 1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if ($i=="src") print $(i+1)}') + else + ip=$(ip route get "$target" 2>/dev/null | awk '{for(i=1;i<=NF;i++) if ($i=="src") print $(i+1)}') + fi + if [[ -n "$ip" ]]; then + echo "$ip" + return 0 + fi + done + + return 1 + } + + LOCAL_IP="$(get_current_ip || true)" + if [[ -z "$LOCAL_IP" ]]; then + msg_error "Could not determine LOCAL_IP" + return 1 + fi + fi + + export LOCAL_IP +} + +function download_with_progress() { + local url="$1" + local output="$2" + if [ -n "$SPINNER_PID" ] && ps -p "$SPINNER_PID" >/dev/null; then kill "$SPINNER_PID" >/dev/null; fi + + if ! command -v pv &>/dev/null; then + $STD apt-get install -y pv + fi + set -o pipefail + + # Content-Length aus HTTP-Header holen + local content_length + content_length=$(curl -fsSLI "$url" | awk '/Content-Length/ {print $2}' | tr -d '\r' || true) + + if [[ -z "$content_length" ]]; then + #msg_warn "Content-Length not available, falling back to plain download" + if ! curl -fL# -o "$output" "$url"; then + msg_error "Download failed" + return 1 + fi + else + if ! curl -fsSL "$url" | pv -s "$content_length" >"$output"; then + msg_error "Download failed" + return 1 + fi + fi +} + +function setup_uv() { + $STD msg_info "Checking uv installation..." + UV_BIN="/usr/local/bin/uv" + TMP_DIR=$(mktemp -d) + ARCH=$(uname -m) + + if [[ "$ARCH" == "x86_64" ]]; then + UV_TAR="uv-x86_64-unknown-linux-gnu.tar.gz" + elif [[ "$ARCH" == "aarch64" ]]; then + UV_TAR="uv-aarch64-unknown-linux-gnu.tar.gz" + else + msg_error "Unsupported architecture: $ARCH" + rm -rf "$TMP_DIR" + return 1 + fi + + # get current github version + LATEST_VERSION=$(curl -s https://api.github.com/repos/astral-sh/uv/releases/latest | grep '"tag_name":' | cut -d '"' -f4 | sed 's/^v//') + if [[ -z "$LATEST_VERSION" ]]; then + msg_error "Could not fetch latest uv version from GitHub." + rm -rf "$TMP_DIR" + return 1 + fi + + # check if uv exists + if [[ -x "$UV_BIN" ]]; then + INSTALLED_VERSION=$($UV_BIN -V | awk '{print $2}') + if [[ "$INSTALLED_VERSION" == "$LATEST_VERSION" ]]; then + $STD msg_ok "uv is already at the latest version ($INSTALLED_VERSION)" + rm -rf "$TMP_DIR" + # set path + if [[ ":$PATH:" != *":/usr/local/bin:"* ]]; then + export PATH="/usr/local/bin:$PATH" + fi + return 0 + else + $STD msg_info "Updating uv from $INSTALLED_VERSION to $LATEST_VERSION" + fi + else + $STD msg_info "uv not found. Installing version $LATEST_VERSION" + fi + + # install or update uv + curl -fsSL "https://github.com/astral-sh/uv/releases/latest/download/${UV_TAR}" -o "$TMP_DIR/uv.tar.gz" + tar -xzf "$TMP_DIR/uv.tar.gz" -C "$TMP_DIR" + install -m 755 "$TMP_DIR"/*/uv "$UV_BIN" + rm -rf "$TMP_DIR" + + # set path + ensure_usr_local_bin_persist + msg_ok "uv installed/updated to $LATEST_VERSION" +} + +function ensure_usr_local_bin_persist() { + local PROFILE_FILE="/etc/profile.d/custom_path.sh" + + if [[ ! -f "$PROFILE_FILE" ]] && ! command -v pveversion &>/dev/null; then + echo 'export PATH="/usr/local/bin:$PATH"' >"$PROFILE_FILE" + chmod +x "$PROFILE_FILE" + fi +} + +function setup_gs() { + msg_info "Setup Ghostscript" + mkdir -p /tmp + TMP_DIR=$(mktemp -d) + CURRENT_VERSION=$(gs --version 2>/dev/null || echo "0") + + RELEASE_JSON=$(curl -fsSL https://api.github.com/repos/ArtifexSoftware/ghostpdl-downloads/releases/latest) + LATEST_VERSION=$(echo "$RELEASE_JSON" | grep '"tag_name":' | head -n1 | cut -d '"' -f4 | sed 's/^gs//') + LATEST_VERSION_DOTTED=$(echo "$RELEASE_JSON" | grep '"name":' | head -n1 | grep -o '[0-9]\+\.[0-9]\+\.[0-9]\+') + + if [[ -z "$LATEST_VERSION" ]]; then + msg_error "Could not determine latest Ghostscript version from GitHub." + rm -rf "$TMP_DIR" + return + fi + + if dpkg --compare-versions "$CURRENT_VERSION" ge "$LATEST_VERSION_DOTTED"; then + msg_ok "Ghostscript is already at version $CURRENT_VERSION" + rm -rf "$TMP_DIR" + return + fi + + msg_info "Installing/Updating Ghostscript to $LATEST_VERSION_DOTTED" + curl -fsSL "https://github.com/ArtifexSoftware/ghostpdl-downloads/releases/download/gs${LATEST_VERSION}/ghostscript-${LATEST_VERSION_DOTTED}.tar.gz" -o "$TMP_DIR/ghostscript.tar.gz" + + if ! tar -xzf "$TMP_DIR/ghostscript.tar.gz" -C "$TMP_DIR"; then + msg_error "Failed to extract Ghostscript archive." + rm -rf "$TMP_DIR" + return + fi + + cd "$TMP_DIR/ghostscript-${LATEST_VERSION_DOTTED}" || { + msg_error "Failed to enter Ghostscript source directory." + rm -rf "$TMP_DIR" + } + $STD apt-get install -y build-essential libpng-dev zlib1g-dev + ./configure >/dev/null && make && sudo make install >/dev/null + local EXIT_CODE=$? + hash -r + if [[ ! -x "$(command -v gs)" ]]; then + if [[ -x /usr/local/bin/gs ]]; then + ln -sf /usr/local/bin/gs /usr/bin/gs + fi + fi + + rm -rf "$TMP_DIR" + + if [[ $EXIT_CODE -eq 0 ]]; then + msg_ok "Ghostscript installed/updated to version $LATEST_VERSION_DOTTED" + else + msg_error "Ghostscript installation failed" + fi +} \ No newline at end of file diff --git a/tools/pve/add-lxc-iptag.sh b/tools/pve/add-lxc-iptag.sh new file mode 100644 index 0000000..feeeac3 --- /dev/null +++ b/tools/pve/add-lxc-iptag.sh @@ -0,0 +1,357 @@ +#!/usr/bin/env bash + +# Copyright (c) 2021-2025 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" +} + +# This function displays a spinner. +spinner() { + local frames=('⠋' '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏') + local spin_i=0 + local interval=0.1 + printf "\e[?25l" + + local color="${YWB}" + + while true; do + printf "\r ${color}%s${CL}" "${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 "${TAB}${YW}${HOLD}${msg}${HOLD}" + 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_error "Installation cancelled." + exit + ;; + *) msg_error "Please answer yes or no." ;; + esac +done + +if ! pveversion | grep -Eq "pve-manager/8\.[0-4](\.[0-9]+)*"; 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_ok "Setup IP-Tag Scripts" + +msg_info "Setup Default Config" +if [[ ! -f /opt/lxc-iptag/iptag.conf ]]; then + cat </opt/lxc-iptag/iptag.conf +# Configuration file for LXC IP tagging + +# List of allowed CIDRs +CIDR_LIST=( + 192.168.0.0/16 + 172.16.0.0/12 + 10.0.0.0/8 + 100.64.0.0/10 +) + +# 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 + cat </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" +SPINNER_PID="" +echo -e "\n${APP} installation completed successfully! ${CL}\n" \ No newline at end of file