15c42e1f24
- add `require_disk_space` function to lib/common.sh with dedup logic for shared filesystems - gate cloudpanel, coolify, pterodactyl installs behind a 2–3 GB disk check - gate uptime-kuma, proxmox update, harden, update-server, and server-benchmark behind 300–1024 MB disk checks - fail early with a clear error before apt installs or config writes can leave the system in a partial state
295 lines
11 KiB
Bash
295 lines
11 KiB
Bash
#!/bin/bash
|
|
|
|
# LXS - Common library
|
|
# Sourced by lxs.sh and all sub-scripts. Provides colors, UI helpers, loggers,
|
|
# spinner, OS guards, and shared utilities (public IP, password generation,
|
|
# apt lock wait, root re-exec, apt non-interactive setup).
|
|
# Repo: https://git.hyko.cx/hykocx/lxs
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
# Colors — minimal palette: red, cyan, white (+ gray as a white shade)
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
RED='\033[38;2;255;64;64m' # errors, destructive actions
|
|
CYAN='\033[38;2;0;229;255m' # accents, titles, OK, info
|
|
WHITE='\033[38;2;240;240;240m' # primary text
|
|
GRAY='\033[38;2;140;140;140m' # secondary text, comments, separators
|
|
NC='\033[0m'
|
|
BOLD='\033[1m'
|
|
DIM='\033[2m'
|
|
|
|
# Legacy aliases — older sub-scripts still reference these. Map onto CYAN
|
|
# so the palette stays at red/cyan/white without breaking them.
|
|
YELLOW="$CYAN"
|
|
GREEN="$CYAN"
|
|
MAGENTA="$CYAN"
|
|
PURPLE="$CYAN"
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
# UI helpers — title + horizontal rule, no closed box.
|
|
#
|
|
# Layout:
|
|
# show_box_top "TITLE" ["RIGHT_TAG"] → TITLE [ RIGHT ]
|
|
# ─────────────────────────────────
|
|
# show_box_mid "SECTION" → ─ SECTION ─────────────────────
|
|
# show_box_bottom → ───────────────────────────────
|
|
# show_separator → ─── (light gray divider)
|
|
# show_menu_item "01" "LABEL" "desc" → [01] LABEL // desc
|
|
# show_prompt → >
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
_lxs_term_cols() {
|
|
local cols
|
|
cols=$(tput cols 2>/dev/null || echo 80)
|
|
[ "$cols" -gt 100 ] && cols=100
|
|
[ "$cols" -lt 60 ] && cols=60
|
|
echo "$cols"
|
|
}
|
|
|
|
show_box_top() {
|
|
local title="$1" right="${2:-}"
|
|
local cols pad_len pad
|
|
cols=$(_lxs_term_cols)
|
|
if [ -n "$right" ]; then
|
|
pad_len=$(( cols - 2 - ${#title} - ${#right} - 4 ))
|
|
[ "$pad_len" -lt 1 ] && pad_len=1
|
|
pad=$(printf ' %.0s' $(seq 1 "$pad_len"))
|
|
printf " ${CYAN}${BOLD}%s${NC}%s${GRAY}[ ${CYAN}%s${GRAY} ]${NC}\n" \
|
|
"$title" "$pad" "$right"
|
|
else
|
|
printf " ${CYAN}${BOLD}%s${NC}\n" "$title"
|
|
fi
|
|
_lxs_hr "$cols"
|
|
}
|
|
|
|
show_box_mid() {
|
|
local title="$1"
|
|
local cols fill_len fill
|
|
cols=$(_lxs_term_cols)
|
|
fill_len=$(( cols - ${#title} - 4 ))
|
|
[ "$fill_len" -lt 2 ] && fill_len=2
|
|
fill=$(printf '─%.0s' $(seq 1 "$fill_len"))
|
|
printf "${GRAY}─ ${CYAN}${BOLD}%s${NC} ${GRAY}%s${NC}\n" "$title" "$fill"
|
|
}
|
|
|
|
show_box_bottom() {
|
|
_lxs_hr "$(_lxs_term_cols)"
|
|
}
|
|
|
|
show_separator() {
|
|
_lxs_hr "$(_lxs_term_cols)"
|
|
}
|
|
|
|
_lxs_hr() {
|
|
local cols=$1 fill
|
|
fill=$(printf '─%.0s' $(seq 1 "$cols"))
|
|
printf "${GRAY}%s${NC}\n" "$fill"
|
|
}
|
|
|
|
show_title() {
|
|
local title="$1"
|
|
local subtitle="${2:-}"
|
|
echo ""
|
|
if [ -n "$subtitle" ]; then
|
|
show_box_top "$title" "$subtitle"
|
|
else
|
|
show_box_top "$title"
|
|
fi
|
|
}
|
|
|
|
# Render a menu line. Pass "exit" as 4th arg to color the key red.
|
|
# show_menu_item "01" "APPLICATIONS" "deploy stacks"
|
|
# show_menu_item "00" "EXIT" "quit shell" exit
|
|
show_menu_item() {
|
|
local key="$1" label="$2" desc="${3:-}" kind="${4:-}"
|
|
local key_color="${CYAN}"
|
|
[ "$kind" = "exit" ] && key_color="${RED}"
|
|
if [ -n "$desc" ]; then
|
|
printf " ${key_color}[%s]${NC} ${WHITE}${BOLD}%-18s${NC} ${GRAY}// %s${NC}\n" \
|
|
"$key" "$label" "$desc"
|
|
else
|
|
printf " ${key_color}[%s]${NC} ${WHITE}${BOLD}%s${NC}\n" "$key" "$label"
|
|
fi
|
|
}
|
|
|
|
show_prompt() {
|
|
echo -e -n " ${CYAN}>${NC} "
|
|
}
|
|
|
|
info() { echo -e "${CYAN}[..]${NC} $*"; }
|
|
ok() { echo -e "${CYAN}[OK]${NC} $*"; }
|
|
warn() { echo -e "${CYAN}[!!]${NC} $*"; }
|
|
err() { echo -e "${RED}[KO]${NC} $*" >&2; }
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
# Spinner — runs a shell command, redirects output to a log file, shows
|
|
# a spinner with a label, prints ✓/✗ on completion. Returns the command's
|
|
# exit code.
|
|
#
|
|
# Usage:
|
|
# run_spinner "Installing foo..." "apt-get install -y foo"
|
|
#
|
|
# Log file: ${LXS_LOG_FILE:-/tmp/lxs.log}
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
run_spinner() {
|
|
local message=$1
|
|
shift
|
|
local command="$*"
|
|
local spinstr='|/-\'
|
|
local log="${LXS_LOG_FILE:-/tmp/lxs.log}"
|
|
|
|
echo -e "${CYAN}[..] ${message}${NC}"
|
|
eval "$command" > "$log" 2>&1 &
|
|
local pid=$!
|
|
|
|
while kill -0 "$pid" 2>/dev/null; do
|
|
local temp=${spinstr#?}
|
|
printf "\r${CYAN}[%c.]${NC} ${message}" "$spinstr"
|
|
spinstr=$temp${spinstr%"$temp"}
|
|
sleep 0.15
|
|
done
|
|
|
|
wait "$pid"
|
|
local exit_code=$?
|
|
if [ $exit_code -eq 0 ]; then
|
|
printf "\r${CYAN}[OK]${NC} ${message}\n"
|
|
else
|
|
printf "\r${RED}[KO]${NC} ${message}\n"
|
|
fi
|
|
return $exit_code
|
|
}
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
# Guards
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
require_debian_ubuntu() {
|
|
if ! grep -qiE 'debian|ubuntu' /etc/os-release 2>/dev/null; then
|
|
err "This script supports Debian/Ubuntu only."
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# Verify enough free disk space before doing apt installs or writing config.
|
|
# Fails early with a clear message so we don't get half-installed packages or
|
|
# files truncated mid-write when the disk is full.
|
|
#
|
|
# Usage:
|
|
# require_disk_space # 500MB on /var (apt cache + lists)
|
|
# require_disk_space 1024 # 1024MB on /var
|
|
# require_disk_space 1024 /var /opt # 1024MB on each listed path
|
|
#
|
|
# Duplicate filesystems (e.g. /var on the same fs as /) are checked once.
|
|
require_disk_space() {
|
|
local min_mb=${1:-500}
|
|
shift 2>/dev/null || true
|
|
local paths=("$@")
|
|
[ ${#paths[@]} -eq 0 ] && paths=(/var)
|
|
|
|
local path avail fs seen=" " rc=0
|
|
for path in "${paths[@]}"; do
|
|
fs=$(df -P "$path" 2>/dev/null | awk 'NR==2 {print $1}')
|
|
if [ -z "$fs" ]; then
|
|
err "Cannot read disk usage for ${path}"
|
|
rc=1
|
|
continue
|
|
fi
|
|
[[ "$seen" == *" $fs "* ]] && continue
|
|
seen="$seen$fs "
|
|
|
|
avail=$(df -Pm "$path" 2>/dev/null | awk 'NR==2 {print $4}')
|
|
if [ "${avail:-0}" -lt "$min_mb" ]; then
|
|
err "Not enough disk space on ${path} (${fs}): ${avail}MB free, ${min_mb}MB required."
|
|
rc=1
|
|
fi
|
|
done
|
|
return $rc
|
|
}
|
|
|
|
# Re-exec the current script via sudo if not already root. Preserves env and
|
|
# arguments. Call near the top of a sub-script:
|
|
# require_root "$0" "$@"
|
|
require_root() {
|
|
[ "$EUID" -eq 0 ] && return 0
|
|
local self=$1
|
|
shift
|
|
exec sudo -E "$self" "$@"
|
|
}
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
# Network / random / apt helpers
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
get_public_ip() {
|
|
local ip=""
|
|
ip=$(curl -s --max-time 5 ifconfig.me 2>/dev/null) || \
|
|
ip=$(curl -s --max-time 5 icanhazip.com 2>/dev/null) || \
|
|
ip=$(curl -s --max-time 5 ipinfo.io/ip 2>/dev/null) || \
|
|
ip=$(hostname -I 2>/dev/null | awk '{print $1}')
|
|
echo "$ip"
|
|
}
|
|
|
|
# Generate a random alphanumeric string. Default length: 32.
|
|
generate_password() {
|
|
local length=${1:-32}
|
|
LC_ALL=C tr -dc 'a-zA-Z0-9' </dev/urandom | head -c "$length"
|
|
}
|
|
|
|
# Configure apt/debconf for fully non-interactive runs. Safe to call multiple
|
|
# times. No-op when apt is absent.
|
|
apt_noninteractive() {
|
|
command -v apt >/dev/null 2>&1 || return 0
|
|
export DEBIAN_FRONTEND=noninteractive
|
|
export NEEDRESTART_MODE=a
|
|
export NEEDRESTART_SUSPEND=1
|
|
echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections 2>/dev/null || true
|
|
}
|
|
|
|
# Add a UFW allow rule, but only if UFW is installed AND active. No-op
|
|
# otherwise — so app installers can declare the ports they need without
|
|
# forcing UFW on hosts that don't use it.
|
|
#
|
|
# Usage:
|
|
# ufw_allow 8000/tcp "Coolify dashboard"
|
|
# ufw_allow 80/tcp
|
|
ufw_allow() {
|
|
command -v ufw >/dev/null 2>&1 || return 0
|
|
ufw status 2>/dev/null | grep -q "Status: active" || return 0
|
|
local rule=$1 comment=${2:-}
|
|
if [ -n "$comment" ]; then
|
|
ufw allow "$rule" comment "$comment" >/dev/null
|
|
else
|
|
ufw allow "$rule" >/dev/null
|
|
fi
|
|
ok "UFW :: allowed ${rule}${comment:+ (${comment})}"
|
|
}
|
|
|
|
# Wait for other apt/dpkg processes to release their locks. Up to 120s.
|
|
wait_for_apt() {
|
|
command -v apt >/dev/null 2>&1 || return 0
|
|
|
|
local max_wait=120
|
|
local wait_count=0
|
|
local lock_detected=false
|
|
|
|
while fuser /var/lib/dpkg/lock-frontend >/dev/null 2>&1 || \
|
|
fuser /var/lib/dpkg/lock >/dev/null 2>&1 || \
|
|
fuser /var/lib/apt/lists/lock >/dev/null 2>&1; do
|
|
|
|
[ "$lock_detected" = false ] && lock_detected=true && \
|
|
echo -e "${YELLOW}[!!] Waiting for other package manager to finish...${NC}"
|
|
|
|
wait_count=$((wait_count + 1))
|
|
[ $wait_count -ge $max_wait ] && \
|
|
echo -e "${RED}[KO] Timeout waiting for package manager${NC}" && return 1
|
|
|
|
printf "\r${GRAY}[..] Waiting... (%ds/%ds)${NC}" $wait_count $max_wait
|
|
sleep 1
|
|
done
|
|
|
|
[ "$lock_detected" = true ] && \
|
|
echo -e "\n${GREEN}[OK] Package manager is now available${NC}"
|
|
return 0
|
|
}
|