Reset shell command hash in spinner and make sleep resilient to shells without redirected sleep, preventing stale PATH lookups and failures in background subshells. Improve Gentoo bootstrap by syncing portage (emerge-webrsync or emerge --sync), preferring binary packages (--getbinpkg --usepkg) before falling back to source emerge, and add a fallback fetcher: prefer curl but use wget if curl is unavailable; fail with a clear error if neither is present. Replace direct curl sourcing with a configurable _fetch command to support the wget fallback.
1395 lines
44 KiB
Bash
1395 lines
44 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, 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}"
|
|
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/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)
|
|
if ! $STD apk update; then
|
|
msg_warn "apk update failed (dl-cdn.alpinelinux.org), trying alternate mirrors..."
|
|
local alpine_mirrors="mirror.init7.net ftp.halifax.rwth-aachen.de mirrors.edge.kernel.org alpine.mirror.wearetriple.com mirror.leaseweb.com uk.alpinelinux.org dl-2.alpinelinux.org dl-4.alpinelinux.org"
|
|
local apk_ok=false
|
|
for m in $(printf '%s\n' $alpine_mirrors | shuf); do
|
|
if timeout 2 bash -c "echo >/dev/tcp/$m/80" 2>/dev/null; then
|
|
msg_custom "${INFO}" "${YW}" "Attempting mirror: ${m}"
|
|
cat <<EOF >/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)
|
|
$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 <<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"
|
|
|
|
# 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? <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" "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 <<EOF >/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
|
|
|
|
# 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" <<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
|
|
;;
|
|
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 <<EOF
|
|
[Service]
|
|
ExecStart=
|
|
ExecStart=-/sbin/agetty ${_agetty_opts} tty%I 115200,38400,9600 - \$TERM
|
|
EOF
|
|
fi
|
|
|
|
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 ${_agetty_opts} 115200,38400,9600 - \$TERM
|
|
EOF
|
|
# Enable console-getty for pct console (not enabled by default on Fedora/RHEL)
|
|
systemctl enable console-getty.service &>/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 644 /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
|
|
}
|