Files
ProxmoxVEDHelperScripts/misc/install.func
CanbiZ (MickLesk) 9db2169ee0 Improve apt mirror scanning and retry logic
Make APT mirror selection more robust by scanning for reachable mirrors and retrying installs with clearer failure reasons. In misc/build.func added mirror_exit, try_mirrors and scan_reachable helpers; prefer regional mirrors (pick up to 3), fall back to ftp.debian.org, then try global mirrors. Improved detection/reporting of hash mismatches vs apt-get errors and return codes, and use a short reachability TCP check before attempting updates. In misc/install.func added a reachable-mirror scan phase, count/report reachable mirrors, and provide clearer log messages when apt-get update/install fails. These changes aim to reduce failures caused by unreachable mirrors or CDN synchronization issues.
2026-03-26 14:46:27 +01:00

1161 lines
34 KiB
Bash

# Copyright (c) 2021-2026 community-scripts ORG
# Author: tteck (tteckster)
# Co-Author: MickLesk
# Co-Author: michelroegl-brunner
# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE
# ==============================================================================
# INSTALL.FUNC - UNIFIED CONTAINER INSTALLATION & SETUP
# ==============================================================================
#
# All-in-One install.func supporting official Proxmox LXC templates:
# - Debian, Ubuntu, Devuan (apt, systemd/sysvinit)
# - Alpine (apk, OpenRC)
# - Fedora, Rocky, AlmaLinux, CentOS Stream (dnf, systemd)
# - openSUSE (zypper, systemd)
# - Gentoo (emerge, OpenRC)
# - openEuler (dnf, systemd)
#
# Supported templates (pveam available):
# almalinux-9/10, alpine-3.x, centos-9-stream, debian-12/13,
# devuan-5, fedora-42, gentoo-current, openeuler-25,
# opensuse-15.x, rockylinux-9/10, ubuntu-22.04/24.04/25.04
#
# Features:
# - Automatic OS detection
# - Unified package manager abstraction
# - Init system abstraction (systemd/OpenRC/sysvinit)
# - Network connectivity verification
# - MOTD and SSH configuration
# - Container customization
#
# ==============================================================================
# ==============================================================================
# SECTION 1: INITIALIZATION & OS DETECTION
# ==============================================================================
# Global variables for OS detection
OS_TYPE="" # debian, ubuntu, devuan, alpine, fedora, rocky, alma, centos, opensuse, gentoo, openeuler
OS_FAMILY="" # debian, alpine, rhel, suse, gentoo
OS_VERSION="" # Version number
PKG_MANAGER="" # apt, apk, dnf, yum, zypper, emerge
INIT_SYSTEM="" # systemd, openrc, sysvinit
# ------------------------------------------------------------------------------
# detect_os()
#
# Detects the operating system and sets global variables:
# OS_TYPE, OS_FAMILY, OS_VERSION, PKG_MANAGER, INIT_SYSTEM
# ------------------------------------------------------------------------------
detect_os() {
if [[ -f /etc/os-release ]]; then
# shellcheck disable=SC1091
. /etc/os-release
OS_TYPE="${ID:-unknown}"
OS_VERSION="${VERSION_ID:-unknown}"
elif [[ -f /etc/alpine-release ]]; then
OS_TYPE="alpine"
OS_VERSION=$(cat /etc/alpine-release)
elif [[ -f /etc/debian_version ]]; then
OS_TYPE="debian"
OS_VERSION=$(cat /etc/debian_version)
elif [[ -f /etc/redhat-release ]]; then
OS_TYPE="centos"
OS_VERSION=$(grep -oE '[0-9]+\.[0-9]+' /etc/redhat-release | head -1)
elif [[ -f /etc/arch-release ]]; then
OS_TYPE="arch"
OS_VERSION="rolling"
elif [[ -f /etc/gentoo-release ]]; then
OS_TYPE="gentoo"
OS_VERSION=$(cat /etc/gentoo-release | grep -oE '[0-9.]+')
else
OS_TYPE="unknown"
OS_VERSION="unknown"
fi
# Normalize OS type and determine family
case "$OS_TYPE" in
debian)
OS_FAMILY="debian"
PKG_MANAGER="apt"
;;
ubuntu)
OS_FAMILY="debian"
PKG_MANAGER="apt"
;;
devuan)
OS_FAMILY="debian"
PKG_MANAGER="apt"
;;
alpine)
OS_FAMILY="alpine"
PKG_MANAGER="apk"
;;
fedora)
OS_FAMILY="rhel"
PKG_MANAGER="dnf"
;;
rocky | rockylinux)
OS_TYPE="rocky"
OS_FAMILY="rhel"
PKG_MANAGER="dnf"
;;
alma | almalinux)
OS_TYPE="alma"
OS_FAMILY="rhel"
PKG_MANAGER="dnf"
;;
centos)
OS_FAMILY="rhel"
# CentOS 7 uses yum, 8+ uses dnf
if [[ "${OS_VERSION%%.*}" -ge 8 ]]; then
PKG_MANAGER="dnf"
else
PKG_MANAGER="yum"
fi
;;
rhel)
OS_FAMILY="rhel"
PKG_MANAGER="dnf"
;;
openeuler)
OS_FAMILY="rhel"
PKG_MANAGER="dnf"
;;
opensuse* | sles)
OS_TYPE="opensuse"
OS_FAMILY="suse"
PKG_MANAGER="zypper"
;;
gentoo)
OS_FAMILY="gentoo"
PKG_MANAGER="emerge"
;;
*)
OS_FAMILY="unknown"
PKG_MANAGER="unknown"
;;
esac
# Detect init system
if command -v systemctl &>/dev/null && [[ -d /run/systemd/system ]]; then
INIT_SYSTEM="systemd"
elif command -v rc-service &>/dev/null || [[ -d /etc/init.d && -f /sbin/openrc ]]; then
INIT_SYSTEM="openrc"
elif [[ -f /etc/inittab ]]; then
INIT_SYSTEM="sysvinit"
else
INIT_SYSTEM="unknown"
fi
}
# ------------------------------------------------------------------------------
# Bootstrap: Ensure curl is available and source core functions
# ------------------------------------------------------------------------------
_bootstrap() {
# Minimal bootstrap to get curl installed
if ! command -v curl &>/dev/null; then
printf "\r\e[2K%b" '\033[93m Setup Source \033[m' >&2
if command -v apt-get &>/dev/null; then
apt-get update &>/dev/null && apt-get install -y curl &>/dev/null
elif command -v apk &>/dev/null; then
apk update &>/dev/null && apk add curl &>/dev/null
elif command -v dnf &>/dev/null; then
dnf install -y curl &>/dev/null
elif command -v yum &>/dev/null; then
yum install -y curl &>/dev/null
elif command -v zypper &>/dev/null; then
zypper install -y curl &>/dev/null
elif command -v emerge &>/dev/null; then
emerge --quiet net-misc/curl &>/dev/null
fi
fi
# Configurable base URL for development — override with COMMUNITY_SCRIPTS_URL
COMMUNITY_SCRIPTS_URL="${COMMUNITY_SCRIPTS_URL:-https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main}"
# Source core functions
source <(curl -fsSL "$COMMUNITY_SCRIPTS_URL/misc/core.func")
source <(curl -fsSL "$COMMUNITY_SCRIPTS_URL/misc/error_handler.func")
load_functions
catch_errors
get_lxc_ip
}
# Run bootstrap and OS detection
_bootstrap
detect_os
# ==============================================================================
# SECTION 2: PACKAGE MANAGER ABSTRACTION
# ==============================================================================
# ------------------------------------------------------------------------------
# pkg_update()
#
# Updates package manager cache/database
# ------------------------------------------------------------------------------
pkg_update() {
case "$PKG_MANAGER" in
apt)
if ! $STD apt-get update; then
msg_warn "apt-get update failed, trying alternate mirrors..."
local eu_mirrors="ftp.de.debian.org ftp.fr.debian.org ftp.nl.debian.org ftp.uk.debian.org ftp.ch.debian.org ftp.se.debian.org ftp.it.debian.org ftp.fau.de ftp.halifax.rwth-aachen.de debian.mirror.lrz.de mirror.init7.net debian.ethz.ch mirrors.dotsrc.org debian.mirrors.ovh.net"
local us_mirrors="ftp.us.debian.org ftp.ca.debian.org debian.csail.mit.edu mirrors.ocf.berkeley.edu mirrors.wikimedia.org debian.osuosl.org mirror.cogentco.com"
local ap_mirrors="ftp.au.debian.org ftp.jp.debian.org ftp.tw.debian.org ftp.kr.debian.org ftp.in.debian.org ftp.hk.debian.org ftp.sg.debian.org mirror.aarnet.edu.au mirror.nitc.ac.in"
local tz regional others
tz=$(cat /etc/timezone 2>/dev/null || echo "UTC")
case "$tz" in
Europe/* | Arctic/*)
regional="$eu_mirrors"
others="$us_mirrors $ap_mirrors"
;;
America/*)
regional="$us_mirrors"
others="$eu_mirrors $ap_mirrors"
;;
Asia/* | Australia/* | Pacific/*)
regional="$ap_mirrors"
others="$eu_mirrors $us_mirrors"
;;
*)
regional=""
others="$eu_mirrors $us_mirrors $ap_mirrors"
;;
esac
echo 'Acquire::By-Hash "no";' >/etc/apt/apt.conf.d/99no-by-hash
_try_apt_mirror() {
local m=$1
for src in /etc/apt/sources.list.d/debian.sources /etc/apt/sources.list; do
[[ -f "$src" ]] && sed -i "s|URIs: http[s]*://[^/]*/|URIs: https://${m}/|g; s|deb http[s]*://[^/]*/|deb https://${m}/|g" "$src"
done
rm -rf /var/lib/apt/lists/*
local out
out=$(apt-get update 2>&1)
if echo "$out" | grep -qi "hashsum\|hash sum"; then
msg_warn "Mirror failed: ${m} (hash mismatch)"
echo "$out" | grep -i "hash" | head -3 | sed 's/^/ /'
return 1
elif echo "$out" | grep -q "^E:"; then
msg_warn "Mirror failed: ${m} (apt-get update error)"
echo "$out" | grep "^E:" | head -3 | sed 's/^/ /'
return 1
else
msg_ok "Using mirror: ${m}"
return 0
fi
}
_scan_reachable() {
local result=""
for m in $1; do
if timeout 2 bash -c "echo >/dev/tcp/$m/80" 2>/dev/null; then
result="$result $m"
else
msg_info "Mirror skip: ${m} (unreachable)"
fi
done
echo "$result" | xargs
}
local apt_ok=false
# Phase 1: Scan regional mirrors, pick 3 random, try them
msg_info "Scanning regional mirrors..."
local regional_ok
regional_ok=$(_scan_reachable "$regional")
local regional_pick
regional_pick=$(printf '%s\n' $regional_ok | shuf | head -3 | xargs)
local r_count
r_count=$(echo "$regional_pick" | wc -w)
msg_info "Found ${r_count} regional mirrors to try"
for mirror in $regional_pick; do
msg_info "Trying mirror: ${mirror}"
if _try_apt_mirror "$mirror"; then
apt_ok=true
break
fi
done
# Phase 2: Try ftp.debian.org
if [[ "$apt_ok" != true ]]; then
if timeout 2 bash -c "echo >/dev/tcp/ftp.debian.org/80" 2>/dev/null; then
msg_info "Trying mirror: ftp.debian.org"
if _try_apt_mirror "ftp.debian.org"; then
apt_ok=true
fi
fi
fi
# Phase 3: Scan rest-of-world mirrors, pick 3 random, try them
if [[ "$apt_ok" != true ]]; then
msg_info "Scanning global mirrors..."
local others_ok
others_ok=$(_scan_reachable "$others")
local others_pick
others_pick=$(printf '%s\n' $others_ok | shuf | head -3 | xargs)
local o_count
o_count=$(echo "$others_pick" | wc -w)
msg_info "Found ${o_count} global mirrors to try"
for mirror in $others_pick; do
msg_info "Trying mirror: ${mirror}"
if _try_apt_mirror "$mirror"; then
apt_ok=true
break
fi
done
fi
# Phase 4: All auto mirrors failed, prompt user
if [[ "$apt_ok" != true ]]; then
msg_warn "Multiple mirrors failed (possible CDN synchronization issue)."
msg_info "Find Debian mirrors at: https://www.debian.org/mirror/list"
while true; do
read -rp " Enter a Debian mirror hostname (or 'skip' to abort): " custom_mirror </dev/tty
[[ -z "$custom_mirror" ]] && continue
[[ "$custom_mirror" == "skip" ]] && break
[[ ! "$custom_mirror" =~ ^[a-zA-Z0-9._-]+$ ]] && {
msg_warn "Invalid hostname format."
continue
}
if _try_apt_mirror "$custom_mirror"; then
apt_ok=true
break
fi
msg_warn "Mirror '${custom_mirror}' also failed. Try another or type 'skip'."
done
fi
if [[ "$apt_ok" != true ]]; then
msg_error "All mirrors failed. Check network or try again later."
return 1
fi
fi
;;
apk)
$STD apk update
;;
dnf)
$STD dnf makecache
;;
yum)
$STD yum makecache
;;
zypper)
$STD zypper refresh
;;
emerge)
$STD emerge --sync
;;
*)
msg_error "Unknown package manager: $PKG_MANAGER"
return 1
;;
esac
}
# ------------------------------------------------------------------------------
# pkg_upgrade()
#
# Upgrades all installed packages
# ------------------------------------------------------------------------------
pkg_upgrade() {
case "$PKG_MANAGER" in
apt)
$STD apt-get -o Dpkg::Options::="--force-confold" -y dist-upgrade
;;
apk)
$STD apk -U upgrade
;;
dnf)
$STD dnf -y upgrade
;;
yum)
$STD yum -y update
;;
zypper)
$STD zypper -n update
;;
emerge)
$STD emerge --quiet --update --deep @world
;;
*)
msg_error "Unknown package manager: $PKG_MANAGER"
return 1
;;
esac
}
# ------------------------------------------------------------------------------
# pkg_install(packages...)
#
# Installs one or more packages
# Arguments:
# packages - List of packages to install
# ------------------------------------------------------------------------------
pkg_install() {
local packages=("$@")
[[ ${#packages[@]} -eq 0 ]] && return 0
case "$PKG_MANAGER" in
apt)
$STD apt-get install -y "${packages[@]}"
;;
apk)
$STD apk add --no-cache "${packages[@]}"
;;
dnf)
$STD dnf install -y "${packages[@]}"
;;
yum)
$STD yum install -y "${packages[@]}"
;;
zypper)
$STD zypper install -y "${packages[@]}"
;;
emerge)
$STD emerge --quiet "${packages[@]}"
;;
*)
msg_error "Unknown package manager: $PKG_MANAGER"
return 1
;;
esac
}
# ------------------------------------------------------------------------------
# pkg_remove(packages...)
#
# Removes one or more packages
# ------------------------------------------------------------------------------
pkg_remove() {
local packages=("$@")
[[ ${#packages[@]} -eq 0 ]] && return 0
case "$PKG_MANAGER" in
apt)
$STD apt-get remove -y "${packages[@]}"
;;
apk)
$STD apk del "${packages[@]}"
;;
dnf)
$STD dnf remove -y "${packages[@]}"
;;
yum)
$STD yum remove -y "${packages[@]}"
;;
zypper)
$STD zypper remove -y "${packages[@]}"
;;
emerge)
$STD emerge --quiet --unmerge "${packages[@]}"
;;
*)
msg_error "Unknown package manager: $PKG_MANAGER"
return 1
;;
esac
}
# ------------------------------------------------------------------------------
# pkg_clean()
#
# Cleans package manager cache to free space
# ------------------------------------------------------------------------------
pkg_clean() {
case "$PKG_MANAGER" in
apt)
$STD apt-get autoremove -y
$STD apt-get autoclean
;;
apk)
$STD apk cache clean
;;
dnf)
$STD dnf clean all
$STD dnf autoremove -y
;;
yum)
$STD yum clean all
;;
zypper)
$STD zypper clean
;;
emerge)
$STD emerge --quiet --depclean
;;
*)
return 0
;;
esac
}
# ==============================================================================
# SECTION 3: SERVICE/INIT SYSTEM ABSTRACTION
# ==============================================================================
# ------------------------------------------------------------------------------
# svc_enable(service)
#
# Enables a service to start at boot
# ------------------------------------------------------------------------------
svc_enable() {
local service="$1"
[[ -z "$service" ]] && return 1
case "$INIT_SYSTEM" in
systemd)
$STD systemctl enable "$service"
;;
openrc)
$STD rc-update add "$service" default
;;
sysvinit)
if command -v update-rc.d &>/dev/null; then
$STD update-rc.d "$service" defaults
elif command -v chkconfig &>/dev/null; then
$STD chkconfig "$service" on
fi
;;
*)
msg_warn "Unknown init system, cannot enable $service"
return 1
;;
esac
}
# ------------------------------------------------------------------------------
# svc_disable(service)
#
# Disables a service from starting at boot
# ------------------------------------------------------------------------------
svc_disable() {
local service="$1"
[[ -z "$service" ]] && return 1
case "$INIT_SYSTEM" in
systemd)
$STD systemctl disable "$service"
;;
openrc)
$STD rc-update del "$service" default 2>/dev/null || true
;;
sysvinit)
if command -v update-rc.d &>/dev/null; then
$STD update-rc.d "$service" remove
elif command -v chkconfig &>/dev/null; then
$STD chkconfig "$service" off
fi
;;
*)
return 1
;;
esac
}
# ------------------------------------------------------------------------------
# svc_start(service)
#
# Starts a service immediately
# ------------------------------------------------------------------------------
svc_start() {
local service="$1"
[[ -z "$service" ]] && return 1
case "$INIT_SYSTEM" in
systemd)
$STD systemctl start "$service"
;;
openrc)
$STD rc-service "$service" start
;;
sysvinit)
$STD /etc/init.d/"$service" start
;;
*)
return 1
;;
esac
}
# ------------------------------------------------------------------------------
# svc_stop(service)
#
# Stops a running service
# ------------------------------------------------------------------------------
svc_stop() {
local service="$1"
[[ -z "$service" ]] && return 1
case "$INIT_SYSTEM" in
systemd)
$STD systemctl stop "$service"
;;
openrc)
$STD rc-service "$service" stop
;;
sysvinit)
$STD /etc/init.d/"$service" stop
;;
*)
return 1
;;
esac
}
# ------------------------------------------------------------------------------
# svc_restart(service)
#
# Restarts a service
# ------------------------------------------------------------------------------
svc_restart() {
local service="$1"
[[ -z "$service" ]] && return 1
case "$INIT_SYSTEM" in
systemd)
$STD systemctl restart "$service"
;;
openrc)
$STD rc-service "$service" restart
;;
sysvinit)
$STD /etc/init.d/"$service" restart
;;
*)
return 1
;;
esac
}
# ------------------------------------------------------------------------------
# svc_status(service)
#
# Gets service status (returns 0 if running)
# ------------------------------------------------------------------------------
svc_status() {
local service="$1"
[[ -z "$service" ]] && return 1
case "$INIT_SYSTEM" in
systemd)
systemctl is-active --quiet "$service"
;;
openrc)
rc-service "$service" status &>/dev/null
;;
sysvinit)
/etc/init.d/"$service" status &>/dev/null
;;
*)
return 1
;;
esac
}
# ------------------------------------------------------------------------------
# svc_reload_daemon()
#
# Reloads init system daemon configuration (for systemd)
# ------------------------------------------------------------------------------
svc_reload_daemon() {
case "$INIT_SYSTEM" in
systemd)
$STD systemctl daemon-reload
;;
*)
# Other init systems don't need this
return 0
;;
esac
}
# ==============================================================================
# SECTION 4: NETWORK & CONNECTIVITY
# ==============================================================================
# ------------------------------------------------------------------------------
# get_ip()
#
# Gets the primary IPv4 address of the container
# Returns: IP address string
# ------------------------------------------------------------------------------
get_ip() {
local ip=""
# Try hostname -I first (most common)
if command -v hostname &>/dev/null; then
ip=$(hostname -I 2>/dev/null | awk '{print $1}' || true)
fi
# Fallback to ip command
if [[ -z "$ip" ]] && command -v ip &>/dev/null; then
ip=$(ip -4 addr show scope global | awk '/inet /{print $2}' | cut -d/ -f1 | head -1)
fi
# Fallback to ifconfig
if [[ -z "$ip" ]] && command -v ifconfig &>/dev/null; then
ip=$(ifconfig | grep -Eo 'inet (addr:)?([0-9]*\.){3}[0-9]*' | grep -Eo '([0-9]*\.){3}[0-9]*' | grep -v '127.0.0.1' | head -1)
fi
echo "$ip"
}
# ------------------------------------------------------------------------------
# verb_ip6()
#
# Configures IPv6 based on IPV6_METHOD variable
# If IPV6_METHOD=disable: disables IPv6 via sysctl
# ------------------------------------------------------------------------------
verb_ip6() {
set_std_mode # Set STD mode based on VERBOSE
if [[ "${IPV6_METHOD:-}" == "disable" ]]; then
msg_info "Disabling IPv6 (this may affect some services)"
mkdir -p /etc/sysctl.d
cat >/etc/sysctl.d/99-disable-ipv6.conf <<EOF
# Disable IPv6 (set by community-scripts)
net.ipv6.conf.all.disable_ipv6 = 1
net.ipv6.conf.default.disable_ipv6 = 1
net.ipv6.conf.lo.disable_ipv6 = 1
EOF
$STD sysctl -p /etc/sysctl.d/99-disable-ipv6.conf
# For OpenRC, ensure sysctl runs at boot
if [[ "$INIT_SYSTEM" == "openrc" ]]; then
$STD rc-update add sysctl default 2>/dev/null || true
fi
msg_ok "Disabled IPv6"
fi
}
# ------------------------------------------------------------------------------
# setting_up_container()
#
# Initial container setup:
# - Verifies network connectivity
# - Removes Python EXTERNALLY-MANAGED restrictions
# - Disables network wait services
# ------------------------------------------------------------------------------
setting_up_container() {
msg_info "Setting up Container OS"
# Wait for network
local i
for ((i = RETRY_NUM; i > 0; i--)); do
if [[ -n "$(get_ip)" ]]; then
break
fi
echo 1>&2 -en "${CROSS}${RD} No Network! "
sleep "$RETRY_EVERY"
done
if [[ -z "$(get_ip)" ]]; then
echo 1>&2 -e "\n${CROSS}${RD} No Network After $RETRY_NUM Tries${CL}"
echo -e "${NETWORK}Check Network Settings"
exit 1
fi
# Remove Python EXTERNALLY-MANAGED restriction (Debian 12+, Ubuntu 23.04+)
rm -rf /usr/lib/python3.*/EXTERNALLY-MANAGED 2>/dev/null || true
# Disable network wait services for faster boot
case "$INIT_SYSTEM" in
systemd)
systemctl disable -q --now systemd-networkd-wait-online.service 2>/dev/null || true
;;
esac
msg_ok "Set up Container OS"
msg_ok "Network Connected: ${BL}$(get_ip)"
}
# ------------------------------------------------------------------------------
# network_check()
#
# Comprehensive network connectivity check for IPv4 and IPv6
# Tests connectivity to DNS servers and verifies DNS resolution
# ------------------------------------------------------------------------------
network_check() {
set +e
trap - ERR
local ipv4_connected=false
local ipv6_connected=false
sleep 1
# Check IPv4 connectivity
if ping -c 1 -W 1 1.1.1.1 &>/dev/null || ping -c 1 -W 1 8.8.8.8 &>/dev/null || ping -c 1 -W 1 9.9.9.9 &>/dev/null; then
msg_ok "IPv4 Internet Connected"
ipv4_connected=true
else
msg_error "IPv4 Internet Not Connected"
fi
# Check IPv6 connectivity (if ping6 exists)
if command -v ping6 &>/dev/null; then
if ping6 -c 1 -W 1 2606:4700:4700::1111 &>/dev/null || ping6 -c 1 -W 1 2001:4860:4860::8888 &>/dev/null; then
msg_ok "IPv6 Internet Connected"
ipv6_connected=true
else
msg_error "IPv6 Internet Not Connected"
fi
fi
# Prompt if both fail
if [[ $ipv4_connected == false && $ipv6_connected == false ]]; then
read -r -p "No Internet detected, would you like to continue anyway? <y/N> " prompt
if [[ "${prompt,,}" =~ ^(y|yes)$ ]]; then
echo -e "${INFO}${RD}Expect Issues Without Internet${CL}"
else
echo -e "${NETWORK}Check Network Settings"
exit 1
fi
fi
# DNS resolution checks
local GIT_HOSTS=("github.com" "raw.githubusercontent.com" "git.community-scripts.org")
local GIT_STATUS="Git DNS:"
local DNS_FAILED=false
for HOST in "${GIT_HOSTS[@]}"; do
local RESOLVEDIP
RESOLVEDIP=$(getent hosts "$HOST" 2>/dev/null | awk '{ print $1 }' | head -n1)
if [[ -z "$RESOLVEDIP" ]]; then
GIT_STATUS+=" $HOST:(${DNSFAIL:-FAIL})"
DNS_FAILED=true
else
GIT_STATUS+=" $HOST:(${DNSOK:-OK})"
fi
done
if [[ "$DNS_FAILED" == true ]]; then
fatal "$GIT_STATUS"
else
msg_ok "$GIT_STATUS"
fi
# Verify APT repository DNS resolution (detect Tailscale MagicDNS / broken resolver)
if command -v apt-get &>/dev/null; then
local APT_DNS_OK=true
for REPO_HOST in "deb.debian.org" "archive.ubuntu.com"; do
if ! getent hosts "$REPO_HOST" &>/dev/null; then
APT_DNS_OK=false
break
fi
done
if [[ "$APT_DNS_OK" == false ]]; then
msg_warn "APT repository DNS resolution failed, injecting public DNS servers"
echo -e "nameserver 8.8.8.8\nnameserver 1.1.1.1" >/etc/resolv.conf
fi
fi
set -e
trap 'error_handler $LINENO "$BASH_COMMAND"' ERR
}
# ==============================================================================
# SECTION 5: OS UPDATE & PACKAGE MANAGEMENT
# ==============================================================================
# ------------------------------------------------------------------------------
# update_os()
#
# Updates container OS and sources appropriate tools.func
# ------------------------------------------------------------------------------
update_os() {
msg_info "Updating Container OS"
# Configure APT cacher proxy if enabled (Debian/Ubuntu only)
if [[ "$PKG_MANAGER" == "apt" && "${CACHER:-}" == "yes" ]]; then
echo 'Acquire::http::Proxy-Auto-Detect "/usr/local/bin/apt-proxy-detect.sh";' >/etc/apt/apt.conf.d/00aptproxy
cat <<EOF >/usr/local/bin/apt-proxy-detect.sh
#!/bin/bash
if nc -w1 -z "${CACHER_IP}" 3142; then
echo -n "http://${CACHER_IP}:3142"
else
echo -n "DIRECT"
fi
EOF
chmod +x /usr/local/bin/apt-proxy-detect.sh
fi
# Update and upgrade
pkg_update
pkg_upgrade
# Remove Python EXTERNALLY-MANAGED restriction
rm -rf /usr/lib/python3.*/EXTERNALLY-MANAGED 2>/dev/null || true
msg_ok "Updated Container OS"
# Source appropriate tools.func based on OS
case "$OS_FAMILY" in
alpine)
source <(curl -fsSL "$COMMUNITY_SCRIPTS_URL/misc/alpine-tools.func")
;;
*)
source <(curl -fsSL "$COMMUNITY_SCRIPTS_URL/misc/tools.func")
;;
esac
}
# ==============================================================================
# SECTION 6: MOTD & SSH CONFIGURATION
# ==============================================================================
# ------------------------------------------------------------------------------
# motd_ssh()
#
# Configures Message of the Day and SSH settings
# ------------------------------------------------------------------------------
motd_ssh() {
# Set terminal to 256-color mode
grep -qxF "export TERM='xterm-256color'" /root/.bashrc 2>/dev/null || echo "export TERM='xterm-256color'" >>/root/.bashrc
# Get OS information
local os_name="$OS_TYPE"
local os_version="$OS_VERSION"
if [[ -f /etc/os-release ]]; then
os_name=$(grep ^NAME /etc/os-release | cut -d= -f2 | tr -d '"')
os_version=$(grep ^VERSION_ID /etc/os-release | cut -d= -f2 | tr -d '"')
fi
# Create MOTD profile script
local PROFILE_FILE="/etc/profile.d/00_lxc-details.sh"
cat >"$PROFILE_FILE" <<EOF
echo ""
echo -e "${BOLD:-}${YW:-}${APPLICATION:-Container} LXC Container - DEV Repository${CL:-}"
echo -e "${RD:-}WARNING: This is a DEVELOPMENT version (ProxmoxVED). Do NOT use in production!${CL:-}"
echo -e "${YW:-} OS: ${GN:-}${os_name} - Version: ${os_version}${CL:-}"
echo -e "${YW:-} Hostname: ${GN:-}\$(hostname)${CL:-}"
echo -e "${YW:-} IP Address: ${GN:-}\$(hostname -I 2>/dev/null | awk '{print \$1}' || ip -4 addr show scope global | awk '/inet /{print \$2}' | cut -d/ -f1 | head -1)${CL:-}"
echo -e "${YW:-} Repository: ${GN:-}https://github.com/community-scripts/ProxmoxVED${CL:-}"
echo ""
EOF
# Disable default MOTD scripts (Debian/Ubuntu)
[[ -d /etc/update-motd.d ]] && chmod -x /etc/update-motd.d/* 2>/dev/null || true
# Configure SSH root access if requested
if [[ "${SSH_ROOT:-}" == "yes" ]]; then
# Ensure SSH server is installed
if [[ ! -f /etc/ssh/sshd_config ]]; then
msg_info "Installing SSH server"
case "$PKG_MANAGER" in
apt)
pkg_install openssh-server
;;
apk)
pkg_install openssh
rc-update add sshd default 2>/dev/null || true
;;
dnf | yum)
pkg_install openssh-server
;;
zypper)
pkg_install openssh
;;
emerge)
pkg_install net-misc/openssh
;;
esac
msg_ok "Installed SSH server"
fi
local sshd_config="/etc/ssh/sshd_config"
if [[ -f "$sshd_config" ]]; then
sed -i "s/#PermitRootLogin prohibit-password/PermitRootLogin yes/g" "$sshd_config"
sed -i "s/PermitRootLogin prohibit-password/PermitRootLogin yes/g" "$sshd_config"
case "$INIT_SYSTEM" in
systemd)
svc_restart sshd 2>/dev/null || svc_restart ssh 2>/dev/null || true
;;
openrc)
svc_enable sshd 2>/dev/null || true
svc_start sshd 2>/dev/null || true
;;
*)
svc_restart sshd 2>/dev/null || true
;;
esac
fi
fi
}
# ==============================================================================
# SECTION 7: CONTAINER CUSTOMIZATION
# ==============================================================================
# ------------------------------------------------------------------------------
# customize()
#
# Customizes container for passwordless login and creates update script
# ------------------------------------------------------------------------------
customize() {
if [[ "${PASSWORD:-}" == "" ]]; then
msg_info "Customizing Container"
# Remove root password for auto-login
passwd -d root &>/dev/null || true
case "$INIT_SYSTEM" in
systemd)
# Mask services that block boot in LXC containers
# systemd-homed-firstboot.service hangs waiting for user input on Fedora
systemctl mask systemd-homed-firstboot.service &>/dev/null || true
systemctl mask systemd-homed.service &>/dev/null || true
# Configure console-getty for auto-login in LXC containers
# console-getty.service is THE service that handles /dev/console in LXC
# It's present on all systemd distros but not enabled by default on Fedora/RHEL
if [[ -f /usr/lib/systemd/system/console-getty.service ]]; then
mkdir -p /etc/systemd/system/console-getty.service.d
cat >/etc/systemd/system/console-getty.service.d/override.conf <<'EOF'
[Service]
ExecStart=
ExecStart=-/sbin/agetty --autologin root --noclear --keep-baud 115200,38400,9600 - $TERM
EOF
# Enable console-getty for LXC web console (required on Fedora/RHEL)
systemctl enable console-getty.service &>/dev/null || true
fi
# Also configure container-getty@1 (Debian/Ubuntu default in LXC)
if [[ -f /usr/lib/systemd/system/container-getty@.service ]]; then
mkdir -p /etc/systemd/system/container-getty@1.service.d
cat >/etc/systemd/system/container-getty@1.service.d/override.conf <<'EOF'
[Service]
ExecStart=
ExecStart=-/sbin/agetty --autologin root --noclear --keep-baud tty%I 115200,38400,9600 - $TERM
EOF
fi
# Reload systemd and restart getty services to apply auto-login
systemctl daemon-reload
systemctl restart console-getty.service &>/dev/null || true
systemctl restart container-getty@1.service &>/dev/null || true
;;
openrc)
# Alpine/Gentoo: modify inittab for auto-login
if [[ -f /etc/inittab ]]; then
sed -i 's|^tty1::respawn:.*|tty1::respawn:/sbin/agetty --autologin root --noclear tty1 38400 linux|' /etc/inittab
fi
touch /root/.hushlogin
;;
sysvinit)
# Devuan/older systems - modify inittab for auto-login
# Devuan 5 (daedalus) uses SysVinit with various inittab formats
# LXC can use /dev/console OR /dev/tty1 depending on how pct console connects
if [[ -f /etc/inittab ]]; then
# Backup original inittab
cp /etc/inittab /etc/inittab.bak 2>/dev/null || true
# Enable autologin on tty1 (for direct access) - handle various formats
# Devuan uses format: 1:2345:respawn:/sbin/getty 38400 tty1
sed -i 's|^\(1:[0-9]*:respawn:\).*getty.*tty1.*|1:2345:respawn:/sbin/agetty --autologin root --noclear tty1 38400 linux|' /etc/inittab
# CRITICAL: Add/replace console entry for LXC - this is what pct console uses!
# Remove any existing console entries first (commented or not)
sed -i '/^[^#]*:.*:respawn:.*getty.*console/d' /etc/inittab
sed -i '/^# LXC console autologin/d' /etc/inittab
# Add new console entry for LXC at the end
echo "" >>/etc/inittab
echo "# LXC console autologin (added by community-scripts)" >>/etc/inittab
echo "co:2345:respawn:/sbin/agetty --autologin root --noclear console 115200 linux" >>/etc/inittab
# Force a reload of inittab and respawn ALL getty processes
# Kill ALL getty processes to force respawn with new autologin settings
pkill -9 -f '[ag]etty' &>/dev/null || true
# Small delay to let init notice the dead processes
sleep 1
# Reload inittab - try multiple methods
telinit q &>/dev/null || init q &>/dev/null || kill -HUP 1 &>/dev/null || true
fi
touch /root/.hushlogin
;;
esac
msg_ok "Customized Container"
fi
# Create update script
# Use var_os for OS-based containers, otherwise use app name
local update_script_name="${var_os:-$app}"
echo "bash -c \"\$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/ct/${update_script_name}.sh)\"" >/usr/bin/update
chmod +x /usr/bin/update
# Inject SSH authorized keys if provided
if [[ -n "${SSH_AUTHORIZED_KEY:-}" ]]; then
mkdir -p /root/.ssh
echo "${SSH_AUTHORIZED_KEY}" >/root/.ssh/authorized_keys
chmod 700 /root/.ssh
chmod 600 /root/.ssh/authorized_keys
fi
}
# ==============================================================================
# SECTION 8: UTILITY FUNCTIONS
# ==============================================================================
# ------------------------------------------------------------------------------
# validate_tz(timezone)
#
# Validates if a timezone is valid
# Returns: 0 if valid, 1 if invalid
# ------------------------------------------------------------------------------
validate_tz() {
local tz="$1"
[[ -f "/usr/share/zoneinfo/$tz" ]]
}
# ------------------------------------------------------------------------------
# set_timezone(timezone)
#
# Sets container timezone
# ------------------------------------------------------------------------------
set_timezone() {
local tz="$1"
if validate_tz "$tz"; then
ln -sf "/usr/share/zoneinfo/$tz" /etc/localtime
echo "$tz" >/etc/timezone 2>/dev/null || true
# Update tzdata if available
case "$PKG_MANAGER" in
apt)
dpkg-reconfigure -f noninteractive tzdata 2>/dev/null || true
;;
esac
msg_ok "Timezone set to $tz"
else
msg_warn "Invalid timezone: $tz"
fi
}
# ------------------------------------------------------------------------------
# os_info()
#
# Prints detected OS information (for debugging)
# ------------------------------------------------------------------------------
os_info() {
echo "OS Type: $OS_TYPE"
echo "OS Family: $OS_FAMILY"
echo "OS Version: $OS_VERSION"
echo "Pkg Manager: $PKG_MANAGER"
echo "Init System: $INIT_SYSTEM"
}