# 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, arch 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}" # Normalize to lowercase: some distros ship mixed-case IDs (e.g. openEuler ID="openEuler") OS_TYPE="${OS_TYPE,,}" 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" ;; arch | archlinux) OS_TYPE="arch" OS_FAMILY="arch" PKG_MANAGER="pacman" ;; 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 pacman &>/dev/null; then pacman -Sy --noconfirm curl &>/dev/null elif command -v emerge &>/dev/null; then # Gentoo stage3 has no curl and no portage tree on first boot. # Sync portage (webrsync = fast snapshot) then prefer binary package. emerge-webrsync --quiet &>/dev/null || emerge --sync --quiet &>/dev/null emerge --quiet --getbinpkg --usepkg net-misc/curl &>/dev/null || emerge --quiet net-misc/curl &>/dev/null fi fi # Last resort: if curl still missing but wget is available (Gentoo stage3), # use wget for the bootstrap source fetch so we don't fail immediately. local _fetch if command -v curl &>/dev/null; then _fetch="curl -fsSL" elif command -v wget &>/dev/null; then _fetch="wget -qO-" else echo "ERROR: neither curl nor wget available — cannot bootstrap" >&2 exit 1 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 <($_fetch "$COMMUNITY_SCRIPTS_URL/misc/core.func") source <($_fetch "$COMMUNITY_SCRIPTS_URL/misc/error_handler.func") load_functions catch_errors get_lxc_ip } # Run bootstrap and OS detection _bootstrap detect_os # Persist diagnostics setting inside container (exported from build.func) # so addon scripts running later can find the user's choice if [[ ! -f /usr/local/community-scripts/diagnostics ]]; then mkdir -p /usr/local/community-scripts echo "DIAGNOSTICS=${DIAGNOSTICS:-no}" >/usr/local/community-scripts/diagnostics fi # ------------------------------------------------------------------------------ # post_progress_to_api() # # - Lightweight progress ping from inside the container # - Updates the existing telemetry record status # - Arguments: # * $1: status (optional, default: "configuring") # - Signals that the installation is actively progressing (not stuck) # - Fire-and-forget: never blocks or fails the script # - Only executes if DIAGNOSTICS=yes and RANDOM_UUID is set # ------------------------------------------------------------------------------ post_progress_to_api() { command -v curl &>/dev/null || return 0 [[ "${DIAGNOSTICS:-no}" == "no" ]] && return 0 [[ -z "${RANDOM_UUID:-}" ]] && return 0 local progress_status="${1:-configuring}" curl -fsS -m 5 -X POST "https://telemetry.community-scripts.org/telemetry" \ -H "Content-Type: application/json" \ -d "{\"random_id\":\"${RANDOM_UUID}\",\"execution_id\":\"${EXECUTION_ID:-${RANDOM_UUID}}\",\"type\":\"lxc\",\"nsapp\":\"${app:-unknown}\",\"status\":\"${progress_status}\"}" &>/dev/null || true } # ============================================================================== # SECTION 2: PACKAGE MANAGER ABSTRACTION # ============================================================================== # ------------------------------------------------------------------------------ # pkg_update() # # Updates package manager cache/database # ------------------------------------------------------------------------------ pkg_update() { # Safety: re-detect if PKG_MANAGER doesn't match available commands if [[ "$PKG_MANAGER" == "apt" ]] && ! command -v apt-get &>/dev/null; then msg_warn "PKG_MANAGER='apt' but apt-get not found (OS: ${OS_TYPE:-unknown}) — re-detecting" detect_os fi if [[ "$PKG_MANAGER" == "apk" ]] && ! command -v apk &>/dev/null; then msg_warn "PKG_MANAGER='apk' but apk not found (OS: ${OS_TYPE:-unknown}) — re-detecting" detect_os fi case "$PKG_MANAGER" in apt) if ! $STD apt-get update; then local failed_mirror failed_mirror=$(grep -m1 -oP '(?<=URIs: https?://)[^/]+' /etc/apt/sources.list.d/debian.sources 2>/dev/null || grep -m1 -oP '(?<=deb https?://)[^/]+' /etc/apt/sources.list 2>/dev/null || echo "unknown") msg_warn "apt-get update failed (${failed_mirror}), trying alternate mirrors..." local distro distro=$(. /etc/os-release 2>/dev/null && echo "$ID" || echo "debian") local eu_mirrors us_mirrors ap_mirrors if [[ "$distro" == "ubuntu" ]]; then eu_mirrors="de.archive.ubuntu.com fr.archive.ubuntu.com se.archive.ubuntu.com nl.archive.ubuntu.com it.archive.ubuntu.com ch.archive.ubuntu.com mirrors.xtom.de" us_mirrors="us.archive.ubuntu.com archive.ubuntu.com mirrors.edge.kernel.org mirror.csclub.uwaterloo.ca mirrors.ocf.berkeley.edu mirror.math.princeton.edu" ap_mirrors="au.archive.ubuntu.com jp.archive.ubuntu.com kr.archive.ubuntu.com tw.archive.ubuntu.com mirror.aarnet.edu.au" else 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" 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" ap_mirrors="ftp.au.debian.org ftp.jp.debian.org ftp.tw.debian.org ftp.kr.debian.org ftp.hk.debian.org ftp.sg.debian.org mirror.aarnet.edu.au mirror.nitc.ac.in" fi 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: http://${m}/|g; s|deb http[s]*://[^/]*/|deb http://${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 ${m} failed (hash mismatch)" return 1 elif echo "$out" | grep -qi "SSL\|certificate"; then msg_warn "Mirror ${m} failed (SSL/certificate error)" return 1 elif echo "$out" | grep -q "^E:"; then msg_warn "Mirror ${m} failed (apt-get update error)" return 1 else msg_ok "CDN set to ${m}: tests passed" 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" fi done echo "$result" | xargs } local apt_ok=false # Phase 1: Scan global mirrors first (independent of local CDN issues) local others_ok others_ok=$(_scan_reachable "$others") local others_pick others_pick=$(printf '%s\n' $others_ok | shuf | head -3 | xargs) for mirror in $others_pick; do msg_custom "${INFO}" "${YW}" "Attempting mirror: ${mirror}" if _try_apt_mirror "$mirror"; then apt_ok=true break fi done # Phase 2: Try primary mirror if [[ "$apt_ok" != true ]]; then local primary if [[ "$distro" == "ubuntu" ]]; then primary="archive.ubuntu.com" else primary="ftp.debian.org" fi if timeout 2 bash -c "echo >/dev/tcp/$primary/80" 2>/dev/null; then msg_custom "${INFO}" "${YW}" "Attempting mirror: ${primary}" if _try_apt_mirror "$primary"; then apt_ok=true fi fi fi # Phase 3: Fall back to regional mirrors if [[ "$apt_ok" != true ]]; then local regional_ok regional_ok=$(_scan_reachable "$regional") local regional_pick regional_pick=$(printf '%s\n' $regional_ok | shuf | head -3 | xargs) for mirror in $regional_pick; do msg_custom "${INFO}" "${YW}" "Attempting 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)." if [[ "$distro" == "ubuntu" ]]; then msg_warn "Find Ubuntu mirrors at: https://launchpad.net/ubuntu/+archivemirrors" else msg_warn "Find Debian mirrors at: https://www.debian.org/mirror/list" fi while true; do read -rp " Enter a mirror hostname (or 'skip' to abort): " custom_mirror /dev/tcp/$m/80" 2>/dev/null; then msg_custom "${INFO}" "${YW}" "Attempting mirror: ${m}" cat </etc/apk/repositories http://$m/alpine/latest-stable/main http://$m/alpine/latest-stable/community EOF if $STD apk update; then msg_ok "CDN set to ${m}: tests passed" apk_ok=true break else msg_warn "Mirror ${m} failed" fi fi done if [[ "$apk_ok" != true ]]; then msg_error "All Alpine mirrors failed. Check network or try again later." return 1 fi fi ;; dnf) # Speed up DNF: enable parallel downloads if not already configured if [[ -f /etc/dnf/dnf.conf ]] && ! grep -q 'max_parallel_downloads' /etc/dnf/dnf.conf; then echo 'max_parallel_downloads=10' >>/etc/dnf/dnf.conf fi # openEuler: default repos point to repo.openeuler.org (China). # Pick the fastest reachable mirror based on timezone to avoid cross-continent latency. if [[ "${OS_TYPE:-}" == "openeuler" ]] && ls /etc/yum.repos.d/*.repo &>/dev/null 2>&1; then if grep -ql 'repo\.openeuler\.org\|repo\.openeuler\.openatom\.cn' /etc/yum.repos.d/*.repo 2>/dev/null; then local _oe_tz _oe_mirror _oe_candidates=() _oe_tz=$(cat /etc/timezone 2>/dev/null || timedatectl show --property=Timezone --value 2>/dev/null || echo "UTC") # Build candidate list, best-first for the region case "$_oe_tz" in Europe/* | Arctic/*) _oe_candidates=( "https://ftp.agdsn.de/openeuler/" # Germany (TU Dresden) "https://mirrors.dotsrc.org/openeuler/" # Denmark "https://mirror.accum.se/mirror/openeuler.org/" # Sweden "https://ftp.belnet.be/mirror/openeuler/" # Belgium ) ;; America/*) # No dedicated Americas mirror yet — European mirrors still beat China _oe_candidates=( "https://ftp.agdsn.de/openeuler/" "https://mirrors.dotsrc.org/openeuler/" "https://ftp.belnet.be/mirror/openeuler/" ) ;; Asia/Singapore | Asia/Kuala_Lumpur | Asia/Jakarta | Asia/Bangkok | Asia/Ho_Chi_Minh) _oe_candidates=( "https://mirror.freedif.org/openEuler/" # Singapore "https://linux.domainesia.com/openeuler/" # Indonesia "https://mirrors.aliyun.com/openeuler/" # China (fast in SEA) ) ;; Asia/* | Australia/* | Pacific/*) _oe_candidates=( "https://mirrors.aliyun.com/openeuler/" "https://mirrors.huaweicloud.com/openeuler/" "https://mirror.freedif.org/openEuler/" ) ;; *) # Unknown timezone — try EU mirrors before falling back to origin _oe_candidates=( "https://ftp.agdsn.de/openeuler/" "https://mirrors.dotsrc.org/openeuler/" ) ;; esac # Pick first mirror that responds within 3 s _oe_mirror="" for _m in "${_oe_candidates[@]}"; do if curl -sf --head --max-time 3 "$_m" &>/dev/null; then _oe_mirror="$_m" break fi done if [[ -n "$_oe_mirror" ]]; then sed -i -E \ "s|https?://repo\.openeuler\.org/|${_oe_mirror}|g; s|https?://repo\.openeuler\.openatom\.cn/|${_oe_mirror}|g" \ /etc/yum.repos.d/*.repo fi fi fi $STD dnf makecache ;; yum) $STD yum makecache ;; zypper) $STD zypper refresh ;; pacman) $STD pacman -Sy --noconfirm ;; emerge) $STD emerge --sync ;; *) msg_error "Unknown package manager: $PKG_MANAGER" return 1 ;; esac } # ------------------------------------------------------------------------------ # pkg_upgrade() # # Upgrades all installed packages # ------------------------------------------------------------------------------ pkg_upgrade() { # Safety: re-detect if PKG_MANAGER doesn't match available commands if [[ "$PKG_MANAGER" == "apt" ]] && ! command -v apt-get &>/dev/null; then msg_warn "PKG_MANAGER='apt' but apt-get not found (OS: ${OS_TYPE:-unknown}) — re-detecting" detect_os fi 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 ;; pacman) $STD pacman -Su --noconfirm ;; 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[@]}" ;; pacman) $STD pacman -S --noconfirm "${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[@]}" ;; pacman) $STD pacman -R --noconfirm "${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 ;; pacman) $STD pacman -Sc --noconfirm ;; 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 </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" # Fix Debian 13 LXC template bug where / is owned by nobody # Only attempt in privileged containers (unprivileged cannot chown /) if [[ "$(stat -c '%U' /)" != "root" ]]; then (chown root:root / 2>/dev/null) || true fi # 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 # Fix locale warnings from perl/apt (e.g. Devuan inherits host locale but hasn't generated it) # C.UTF-8 is always available without locale-gen on any Linux system if [[ "$PKG_MANAGER" == "apt" ]]; then export DEBIAN_FRONTEND=noninteractive export LC_ALL=C.UTF-8 export LANG=C.UTF-8 export LANGUAGE=C.UTF-8 grep -qxF 'LC_ALL=C.UTF-8' /etc/environment 2>/dev/null || echo -e 'LC_ALL=C.UTF-8\nLANG=C.UTF-8' >>/etc/environment fi # Arch Linux: pacman 7+ uses Landlock sandboxing for the 'alpm' user, which # requires kernel features unavailable in unprivileged LXC containers. # Disabling DownloadUser falls back to running as root (safe inside an LXC). if [[ "$PKG_MANAGER" == "pacman" && -f /etc/pacman.conf ]]; then sed -i 's/^\s*DownloadUser\s*=.*/#&/' /etc/pacman.conf grep -q '^DisableSandbox' /etc/pacman.conf || sed -i '/^\[options\]/a DisableSandbox' /etc/pacman.conf # Initialize pacman keyring (required for signature verification on first # upgrade). The Arch base template ships without an initialized keyring. if [[ ! -f /etc/pacman.d/gnupg/pubring.gpg ]] || [[ ! -s /etc/pacman.d/gnupg/pubring.gpg ]]; then $STD pacman-key --init $STD pacman-key --populate archlinux fi fi # 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)" post_progress_to_api } # ------------------------------------------------------------------------------ # 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? " 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" "api.github.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 local _proxy_raw="${CACHER_IP}" local _proxy_host _proxy_port _proxy_url # Parse host and port from URL or plain IP/hostname _proxy_host=$(echo "$_proxy_raw" | sed -e 's|https\?://||' -e 's|/.*||' | cut -d: -f1) _proxy_port=$(echo "$_proxy_raw" | sed -e 's|https\?://||' -e 's|/.*||' | cut -s -d: -f2) if [[ "$_proxy_raw" =~ ^https?:// ]]; then # Full URL provided — use as-is for proxy output, extract port for nc check _proxy_url="$_proxy_raw" _proxy_port="${_proxy_port:-80}" else # Legacy: plain IP or hostname — default to http + port 3142 _proxy_port="${_proxy_port:-3142}" _proxy_url="http://${_proxy_raw}:${_proxy_port}" fi cat </usr/local/bin/apt-proxy-detect.sh #!/bin/bash if nc -w1 -z "${_proxy_host}" ${_proxy_port}; then echo -n "${_proxy_url}" else echo -n "DIRECT" fi EOF chmod +x /usr/local/bin/apt-proxy-detect.sh fi # Re-detect OS to ensure PKG_MANAGER is correct (guards against stale env) detect_os # 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" post_progress_to_api # Source appropriate tools.func based on OS local tools_content case "$OS_FAMILY" in alpine) tools_content=$(curl -fsSL "$COMMUNITY_SCRIPTS_URL/misc/alpine-tools.func") || { msg_error "Failed to download alpine-tools.func" exit 115 } ;; *) tools_content=$(curl -fsSL "$COMMUNITY_SCRIPTS_URL/misc/tools.func") || { msg_error "Failed to download tools.func" exit 115 } ;; esac source /dev/stdin <<<"$tools_content" if ! declare -f fetch_and_deploy_gh_release >/dev/null 2>&1; then msg_error "tools.func loaded but incomplete — missing expected functions" exit 115 fi } # ============================================================================== # SECTION 6: MOTD & SSH CONFIGURATION # ============================================================================== # ------------------------------------------------------------------------------ # motd_ssh() # # Configures Message of the Day and SSH settings # ------------------------------------------------------------------------------ motd_ssh() { # Set 256-color mode only for SSH sessions; LXC console gets TERM from agetty. # Forcing TERM=xterm-256color on the LXC noVNC console can break readline's # window-size detection and cause CSI 6n response leaks ("R;80R" garbage). if ! grep -q '__cs_term_setup' /root/.bashrc 2>/dev/null; then cat >>/root/.bashrc <<'EOF' # __cs_term_setup: community-scripts terminal init if [ -n "${SSH_CONNECTION:-}${SSH_TTY:-}" ]; then export TERM='xterm-256color' fi EOF fi # For LXC login shells (bash_profile runs before readline initialises): # lock TERM=linux on the physical console so readline never queries CPR. # This must run in bash_profile, not bashrc, because login shells source # bash_profile first — readline reads TERM before any profile.d script runs. if ! grep -q '__cs_console_term' /root/.bash_profile 2>/dev/null; then cat >>/root/.bash_profile <<'EOF' # __cs_console_term: lock TERM=linux on LXC noVNC console to prevent R;80R garbage # (readline 8.2+ sends ESC[6n if TERM supports CPR; linux terminfo has no CPR cap) if [ -z "${SSH_CONNECTION:-}${SSH_TTY:-}" ]; then _ct=$(tty 2>/dev/null) case "$_ct" in /dev/console|/dev/tty[0-9]*) export TERM=linux ;; esac unset _ct fi EOF fi # 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 '"' || true) os_version=$(grep ^VERSION_ID /etc/os-release | cut -d= -f2 | tr -d '"' || true) [[ -z "$os_name" ]] && os_name="$OS_TYPE" [[ -z "$os_version" ]] && os_version="${OS_VERSION:-rolling}" fi # Create MOTD profile script local PROFILE_FILE="/etc/profile.d/00_lxc-details.sh" cat >"$PROFILE_FILE" </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 # openSUSE's /etc/bash.bashrc sources profile.d via `[ -x ]`, not `[ -r ]`, # so the MOTD script must be executable to be loaded on every distro. chmod +x "$PROFILE_FILE" # Belt-and-suspenders: also write a static /etc/motd. PAM's pam_motd reads # this on every login regardless of how /etc/profile.d is sourced, so the # MOTD is shown even on distros where bash login profile loading is quirky # (Fedora, Arch, openSUSE, etc.). Static fields only — dynamic IP/hostname # come from the profile.d script above when it's sourced. cat >/etc/motd </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 ;; pacman) 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 post_progress_to_api } # ============================================================================== # 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 # In LXC there are TWO possible getty paths: # - container-getty@1.service -> /dev/tty1 (Proxmox web noVNC console) # - console-getty.service -> /dev/console (pct console / serial) # We configure both with --autologin --noissue (prevents banner spam from # rapid respawns) so whichever one the user opens works. local _agetty_opts='--autologin root --noclear --noissue --keep-baud' 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 </etc/systemd/system/console-getty.service.d/override.conf </dev/null || true fi # Fedora/RHEL: /etc/profile.d/vte*.sh sets a PROMPT_COMMAND that, combined # with broken TIOCGWINSZ in LXC noVNC, causes CSI 6n responses to leak as # "R;80R" garbage at the shell prompt. Disable by renaming (chmod -x is # ineffective; /etc/profile sources by readability, not executable bit). # Versioned names exist on Fedora 43+ (vte-2.91.sh). shopt -s nullglob for _f in /etc/profile.d/vte*.sh /etc/profile.d/vte*.csh; do mv "$_f" "${_f}.disabled" 2>/dev/null || true done shopt -u nullglob # LXC pseudo-tty often reports winsize 0x0; bash readline then falls back # to CSI 6n cursor-position queries whose responses leak as input. # Setting COLUMNS/LINES + stty makes readline skip the query entirely. cat >/etc/profile.d/00-lxc-term-size.sh <<'EOF' # community-scripts: ensure sane terminal size in LXC console if [ -t 0 ] && [ -t 1 ]; then _sz=$(stty size 2>/dev/null) if [ -z "$_sz" ] || [ "${_sz% *}" = "0" ]; then stty rows 24 cols 80 2>/dev/null export LINES=24 COLUMNS=80 fi unset _sz fi EOF chmod +x /etc/profile.d/00-lxc-term-size.sh systemctl daemon-reload # Restart only what's currently active to avoid spawning duplicate gettys systemctl is-active container-getty@1.service &>/dev/null && systemctl restart container-getty@1.service &>/dev/null || true systemctl is-active console-getty.service &>/dev/null && systemctl restart console-getty.service &>/dev/null || true touch /root/.hushlogin ;; 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 SysVinit systems: modify inittab for auto-login on tty1 + console if [[ -f /etc/inittab ]]; then cp /etc/inittab /etc/inittab.bak 2>/dev/null || true # Replace the tty1 getty line with an autologin version. # Devuan 5 ships: 1:2345:respawn:/sbin/agetty --noclear tty1 38400 linux sed -i 's|^\(1:[0-9]*:respawn:\).*[ag]etty.*tty1.*|1:2345:respawn:/sbin/agetty --autologin root --noclear tty1 38400 linux|' /etc/inittab # Add/replace console entry (pct console maps to /dev/console in some configs) sed -i '/^[^#]*:.*:respawn:.*[ag]etty.*console/d' /etc/inittab sed -i '/^# LXC console autologin/d' /etc/inittab cat >>/etc/inittab <<'EOF' # LXC console autologin (added by community-scripts) co:2345:respawn:/sbin/agetty --autologin root --noclear console 115200 linux EOF # IMPORTANT: signal init to load the new inittab into memory FIRST, # then kill the running getty — init will respawn it using the new config. # (Killing first causes init to respawn with the OLD in-memory config.) telinit q &>/dev/null || init q &>/dev/null || kill -HUP 1 &>/dev/null || true sleep 1 pkill -f '[ag]etty.*tty1' &>/dev/null || true pkill -f '[ag]etty.*console' &>/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 post_progress_to_api } # ============================================================================== # 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" } # ------------------------------------------------------------------------------ # cleanup_lxc() # # Overrides core.func version to support all OS types (not just Alpine/Debian) # Cleans package caches and removes unnecessary packages after installation # ------------------------------------------------------------------------------ cleanup_lxc() { pkg_clean }