#!/usr/bin/env bash # Verify that the effective user ID is 0 (root), terminating with an error otherwise. check_running_as_root() { if [ "$(id -u)" != "0" ]; then die "This command must be run as root." fi } # Detect and populate the running Linux distribution name in the global OS variable. detect_os() { if [ -f /etc/lsb-release ] && command -v lsb_release >/dev/null 2>&1; then OS=$(lsb_release -si) elif [ -f /etc/os-release ]; then OS=$(awk -F= '/^NAME/{print $2}' /etc/os-release | tr -d '"') elif [ -f /etc/redhat-release ]; then OS=$(awk '{print $1}' /etc/redhat-release) elif [ -f /etc/arch-release ]; then OS="Arch Linux" else die "Unsupported operating system" fi } # Check whether the detected operating system belongs to the Red Hat enterprise family. is_redhat_family_os() { [[ "${OS:-}" == "CentOS"* ]] || [[ "${OS:-}" == "AlmaLinux"* ]] || [[ "${OS:-}" == "Rocky"* ]] || [[ "${OS:-}" == "Red Hat"* ]] || [[ "${OS:-}" == "Oracle Linux"* ]] || [[ "${OS:-}" == "Amazon Linux"* ]] } # Select either dnf or yum as the active package manager for Red Hat distributions. select_redhat_package_manager() { if command -v dnf >/dev/null 2>&1; then PKG_MANAGER="dnf" elif command -v yum >/dev/null 2>&1; then PKG_MANAGER="yum" else die "Neither yum nor dnf was found. Please install packages manually." fi } # Attempt to install and enable the EPEL repository on Red Hat family distributions. enable_epel_if_available() { if $PKG_MANAGER install -y -q epel-release >/dev/null 2>&1; then return fi colorized_echo yellow "Could not enable EPEL automatically; continuing with configured repositories." } # Emit a warning when package manager repository metadata refresh cannot be completed. warn_package_metadata_refresh_failed() { colorized_echo yellow "Could not refresh package metadata; continuing with configured repositories." } # Identify the system package manager and refresh available repository metadata. detect_and_update_package_manager() { if [ -z "${OS:-}" ]; then detect_os fi colorized_echo blue "Updating package manager" if [[ "$OS" == "Ubuntu"* ]] || [[ "$OS" == "Debian"* ]]; then PKG_MANAGER="apt-get" $PKG_MANAGER update -qq >/dev/null 2>&1 || warn_package_metadata_refresh_failed elif is_redhat_family_os; then select_redhat_package_manager $PKG_MANAGER -y -q makecache >/dev/null 2>&1 || warn_package_metadata_refresh_failed enable_epel_if_available elif [[ "$OS" == "Fedora"* ]]; then PKG_MANAGER="dnf" $PKG_MANAGER -q -y makecache >/dev/null 2>&1 || warn_package_metadata_refresh_failed elif [[ "$OS" == "Arch Linux" ]] || [[ "$OS" == "Arch"* ]]; then PKG_MANAGER="pacman" $PKG_MANAGER -Sy --noconfirm --quiet >/dev/null 2>&1 || warn_package_metadata_refresh_failed elif [[ "$OS" == "openSUSE"* ]]; then PKG_MANAGER="zypper" $PKG_MANAGER refresh --quiet >/dev/null 2>&1 || warn_package_metadata_refresh_failed else die "Unsupported operating system" fi } # Attempt to install a package, returning non-zero on failure instead of # aborting. Use this when the caller wants to handle a failed install itself # (e.g. fall back to an alternative package name); most callers want # install_package, which aborts on failure. _try_install_package_impl() { local package="$1" if [ -z "${OS:-}" ]; then detect_os fi if [ -z "${PKG_MANAGER:-}" ]; then detect_and_update_package_manager fi colorized_echo blue "Installing $package" if [[ "$OS" == "Ubuntu"* ]] || [[ "$OS" == "Debian"* ]]; then $PKG_MANAGER -y -qq install "$package" >/dev/null 2>&1 elif is_redhat_family_os; then $PKG_MANAGER install -y -q "$package" >/dev/null 2>&1 elif [[ "$OS" == "Fedora"* ]]; then $PKG_MANAGER install -y -q "$package" >/dev/null 2>&1 elif [[ "$OS" == "Arch Linux" ]] || [[ "$OS" == "Arch"* ]]; then $PKG_MANAGER -S --noconfirm --quiet "$package" >/dev/null 2>&1 elif [[ "$OS" == "openSUSE"* ]]; then $PKG_MANAGER --quiet install -y "$package" >/dev/null 2>&1 else die "Unsupported operating system" fi } # Install a system package via the active package manager or exit with a failure message. install_package() { local pkg="$1" local cmd="$pkg" case "$pkg" in net-tools) cmd=netstat ;; esac # An already present binary never needs a repository. if command -v "$cmd" >/dev/null 2>&1; then return 0 fi # jq is required by nearly every code path; a static build ships with the # mirror so a minimal offline host can still finish the install. if [ "$pkg" = "jq" ] && _install_jq_static; then return 0 fi if try_install_package "$pkg"; then return 0 fi die "The offline mirror cannot install '$pkg' on this host. Install it manually and re-run." } # jq from a static binary, so the distro repositories are never required. _install_jq_static() { local arch dest=/usr/local/bin/jq case "$(uname -m)" in x86_64|amd64) arch=amd64 ;; aarch64|arm64) arch=arm64 ;; *) return 1 ;; esac colorized_echo blue "Installing jq from the mirror (static build)" if curl -fsSL --retry 3 --retry-delay 2 --connect-timeout 20 "https://git.nimbaha.org/pgnode/bin/jq/jq-linux-${arch}" -o "$dest.tmp"; then chmod 755 "$dest.tmp" && mv -f "$dest.tmp" "$dest" return $? fi rm -f "$dest.tmp" return 1 } # Verify or install an interactive terminal editor (nano or vi) and assign EDITOR. check_editor() { if [ -z "${EDITOR:-}" ]; then if command -v nano >/dev/null 2>&1; then EDITOR="nano" elif command -v vi >/dev/null 2>&1; then EDITOR="vi" else detect_os try_install_package nano || true EDITOR="nano" fi fi } # Identify the Linux machine architecture and assign the normalized ARCH variable. identify_the_operating_system_and_architecture() { if [[ "$(uname)" != "Linux" ]]; then die "error: This operating system is not supported." fi case "$(uname -m)" in i386 | i686) ARCH='32' ;; amd64 | x86_64) ARCH='64' ;; armv5tel) ARCH='arm32-v5' ;; armv6l) ARCH='arm32-v6' grep Features /proc/cpuinfo | grep -qw 'vfp' || ARCH='arm32-v5' ;; armv7 | armv7l) ARCH='arm32-v7a' grep Features /proc/cpuinfo | grep -qw 'vfp' || ARCH='arm32-v5' ;; armv8 | aarch64) ARCH='arm64-v8a' ;; mips) ARCH='mips32' ;; mipsle) ARCH='mips32le' ;; mips64) ARCH='mips64' lscpu | grep -q "Little Endian" && ARCH='mips64le' ;; mips64le) ARCH='mips64le' ;; ppc64) ARCH='ppc64' ;; ppc64le) ARCH='ppc64le' ;; riscv64) ARCH='riscv64' ;; s390x) ARCH='s390x' ;; *) die "error: The architecture is not supported." ;; esac } # Download and install the architecture-appropriate yq YAML processor binary into /usr/local/bin. install_yq() { local base_url="https://git.nimbaha.org/pgnode/github/mikefarah/yq/releases/latest/download" local yq_binary="" local yq_url="" local binary_tmp="" if command -v yq >/dev/null 2>&1; then colorized_echo green "yq is already installed." return fi identify_the_operating_system_and_architecture case "$ARCH" in 64 | x86_64) yq_binary="yq_linux_amd64" ;; arm32-v7a | arm32-v6 | arm32-v5 | armv7l) yq_binary="yq_linux_arm" ;; arm64-v8a | aarch64) yq_binary="yq_linux_arm64" ;; 32 | i386 | i686) yq_binary="yq_linux_386" ;; *) die "Unsupported architecture: $ARCH" ;; esac yq_url="${base_url}/${yq_binary}" colorized_echo blue "Downloading yq from ${yq_url}..." if ! command -v curl >/dev/null 2>&1 && ! command -v wget >/dev/null 2>&1; then colorized_echo yellow "Neither curl nor wget is installed. Attempting to install curl." install_package curl || die "Failed to install curl. Please install curl or wget manually." fi binary_tmp=$(create_temp_file "yq" ".bin") if command -v curl >/dev/null 2>&1; then curl -fsSL "$yq_url" -o "$binary_tmp" || die "Failed to download yq using curl. Please check your internet connection." elif command -v wget >/dev/null 2>&1; then wget -q -O "$binary_tmp" "$yq_url" || die "Failed to download yq using wget. Please check your internet connection." fi install -m 755 "$binary_tmp" /usr/local/bin/yq colorized_echo green "yq installed successfully!" if ! echo "$PATH" | grep -q "/usr/local/bin"; then export PATH="/usr/local/bin:$PATH" fi rm -f "$binary_tmp" } # --------------------------------------------------------------------------- # Temporary Iranian package mirrors # # Nothing below runs unless a package install has already failed, i.e. the host # cannot reach its configured repositories. The original configuration is # copied aside before a single byte is rewritten and an EXIT trap puts it back, # so the host is left exactly as it was found even if the installer dies or is # interrupted half way. # # The caller supplies the wrapper (see the end of this file's integration in # each patcher): it must call _pg_ir_enable only after its own normal install # path has failed once. # --------------------------------------------------------------------------- IR_SRC_BACKUP=/var/backups/ir-mirror-sources _pg_ir_mirror_active=0 # Self-contained so this block can be inlined into scripts that do not define # an is_redhat_family_os helper. _pg_ir_is_redhat() { case "${OS:-}" in CentOS*|AlmaLinux*|Rocky*|Red\ Hat*|Oracle*|Fedora*) return 0 ;; esac return 1 } # Probe for each base URL read from stdin; print the first that answers. _pg_ir_pick() { local suffix="$1" base while IFS= read -r base; do [ -n "$base" ] || continue if curl -fsSL --connect-timeout 6 --max-time 12 -o /dev/null -r 0-0 "$base$suffix" 2>/dev/null; then printf '%s\n' "$base" return 0 fi done return 1 } _pg_ir_backup() { mkdir -p "$IR_SRC_BACKUP" || return 1 rm -rf "$IR_SRC_BACKUP"/* 2>/dev/null if [ -f /etc/apt/sources.list ]; then cp -a /etc/apt/sources.list "$IR_SRC_BACKUP/sources.list" || return 1 fi if [ -d /etc/apt/sources.list.d ]; then mkdir -p "$IR_SRC_BACKUP/sources.list.d" cp -a /etc/apt/sources.list.d/. "$IR_SRC_BACKUP/sources.list.d/" 2>/dev/null || true fi if [ -d /etc/yum.repos.d ]; then mkdir -p "$IR_SRC_BACKUP/yum.repos.d" cp -a /etc/yum.repos.d/. "$IR_SRC_BACKUP/yum.repos.d/" 2>/dev/null || true fi return 0 } _pg_ir_restore() { [ "$_pg_ir_mirror_active" = 1 ] || return 0 _pg_ir_mirror_active=0 trap - EXIT INT TERM if [ -d "$IR_SRC_BACKUP/sources.list.d" ]; then rm -rf /etc/apt/sources.list.d mkdir -p /etc/apt/sources.list.d cp -a "$IR_SRC_BACKUP/sources.list.d/." /etc/apt/sources.list.d/ 2>/dev/null || true fi if [ -f "$IR_SRC_BACKUP/sources.list" ]; then cp -a "$IR_SRC_BACKUP/sources.list" /etc/apt/sources.list 2>/dev/null || true fi if [ -d "$IR_SRC_BACKUP/yum.repos.d" ]; then rm -rf /etc/yum.repos.d mkdir -p /etc/yum.repos.d cp -a "$IR_SRC_BACKUP/yum.repos.d/." /etc/yum.repos.d/ 2>/dev/null || true fi # Deliberately no `apt-get update` here: the original repositories are # unreachable by definition, so refreshing would just hang the exit. colorized_echo green "Package repositories restored to their original configuration." } _pg_ir_enable() { [ "$_pg_ir_mirror_active" = 1 ] && return 0 [ -n "${OS:-}" ] || detect_os local codename base sec rv arch comps codename=$(sed -n 's/^VERSION_CODENAME=//p' /etc/os-release 2>/dev/null | tr -d '"') if [ -z "$codename" ]; then colorized_echo yellow "No release codename found; cannot select an Iranian mirror." return 1 fi if [ "$OS" = "Ubuntu" ] || [ "${OS#Debian}" != "$OS" ]; then if [ "$OS" = "Ubuntu" ]; then base=$(_pg_ir_pick "/dists/$codename/Release" <<'CAND' https://mirror.arvancloud.ir/ubuntu https://repo.iut.ac.ir/ubuntu https://mirror.iranserver.com/ubuntu CAND ) else base=$(_pg_ir_pick "/dists/$codename/Release" <<'CAND' https://mirror.arvancloud.ir/debian https://repo.iut.ac.ir/debian CAND ) sec=$(_pg_ir_pick "/dists/$codename-security/Release" <<'CAND' https://mirror.arvancloud.ir/debian-security CAND ) fi if [ -z "$base" ]; then colorized_echo yellow "No reachable Iranian mirror for $OS $codename." return 1 fi _pg_ir_backup || return 1 mkdir -p /etc/apt/sources.list.d # Third-party lists may point abroad as well; park them for the duration. find /etc/apt/sources.list.d -maxdepth 1 -name '*.list' -exec rm -f {} + 2>/dev/null || true if [ "$OS" = "Ubuntu" ]; then cat > /etc/apt/sources.list < /etc/apt/sources.list <> /etc/apt/sources.list fi fi _pg_ir_mirror_active=1 trap '_pg_ir_restore' EXIT INT TERM colorized_echo yellow "Package repositories temporarily switched to $base; they will be restored on exit." apt-get update -qq >/dev/null 2>&1 || true return 0 fi if _pg_ir_is_redhat; then rv=$(rpm -E %{rhel} 2>/dev/null) case "$(uname -m)" in x86_64|amd64) arch=x86_64 ;; aarch64|arm64) arch=aarch64 ;; *) return 1 ;; esac [ -n "$rv" ] || return 1 base=$(_pg_ir_pick "/almalinux/$rv/BaseOS/$arch/os/repodata/repomd.xml" <<'CAND' https://mirror.iranserver.com/almalinux https://mirror.arvancloud.ir/almalinux CAND ) if [ -z "$base" ]; then colorized_echo yellow "No reachable Iranian mirror for $OS $rv." return 1 fi _pg_ir_backup || return 1 mkdir -p /etc/yum.repos.d rm -f /etc/yum.repos.d/*.repo cat > /etc/yum.repos.d/ir-temp-mirror.repo </dev/null 2>&1 || true return 0 fi colorized_echo yellow "No Iranian mirror recipe for $OS." return 1 } # Retry a failed install through a temporary Iranian mirror. This is the only # entry point: the mirror is never enabled speculatively. try_install_package() { local package="$1" if _try_install_package_impl "$package"; then return 0 fi colorized_echo yellow "Package repositories are unreachable; retrying through an Iranian mirror." _pg_ir_enable || return 1 _try_install_package_impl "$package" }