#!/usr/bin/env bash # serverkit.sh — interactive utility kit for Debian/Alpine servers # # Features: system overview, swap management, zram, ballooning check, # RAM/disk/latency/CPU/network benchmarks, BBR/TCP tuning, cleanup, # disk health, top consumers & failed services, diagnostics. # # Run with bash. On Alpine: apk add bash, then: bash serverkit.sh # Helper tools (mbw, ioping, sysbench, curl...) are offered for install # on demand; if you decline, portable fallbacks are used. set -u -o pipefail LC_ALL=C export LC_ALL # ------------------------------------------------------------------ # Config # ------------------------------------------------------------------ BENCH_DIR="/var/tmp/serverkit-bench.$$" SWAP_FILE="/swapfile" ZRAM_HELPER="/usr/local/sbin/serverkit-zram.sh" # ------------------------------------------------------------------ # UI helpers # ------------------------------------------------------------------ if [ -t 1 ]; then C_G=$'\033[1;32m'; C_Y=$'\033[1;33m'; C_R=$'\033[1;31m' C_B=$'\033[1;36m'; C_D=$'\033[2m'; C_0=$'\033[0m' else C_G=''; C_Y=''; C_R=''; C_B=''; C_D=''; C_0='' fi hr() { printf '%s\n' "--------------------------------------------------------------"; } title() { printf '\n%s== %s ==%s\n' "$C_B" "$1" "$C_0"; } ok() { printf '%s[ok]%s %s\n' "$C_G" "$C_0" "$1"; } warn() { printf '%s[!!]%s %s\n' "$C_Y" "$C_0" "$1"; } err() { printf '%s[XX]%s %s\n' "$C_R" "$C_0" "$1" >&2; } die() { err "$1"; exit 1; } info() { printf '%s%s%s\n' "$C_D" "$1" "$C_0"; } need_root() { [ "$(id -u)" -eq 0 ] || die "This action needs root. Re-run with sudo." } # Ask yes/no. Usage: confirm "question" (default: no) confirm() { local a printf '%s [y/N] ' "$1" read -r a case "$a" in [Yy]|[Yy][Ee][Ss]) return 0 ;; *) return 1 ;; esac } # ------------------------------------------------------------------ # Helpers: distro / packages / portable parsing # ------------------------------------------------------------------ . /etc/os-release 2>/dev/null DISTRO="${ID:-unknown}" pkg_install() { # pkg_install "explanation" pkg1 pkg2 ... local what="$1"; shift local missing=() p for p in "$@"; do command -v "$p" >/dev/null 2>&1 || missing+=("$p") done [ "${#missing[@]}" -eq 0 ] && return 0 warn "Missing: ${missing[*]} — needed for: $what" if ! confirm "Install ${missing[*]} with the system package manager?"; then return 1 fi need_root || return 1 case "$DISTRO" in debian|ubuntu) apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y "${missing[@]}" ;; alpine) apk add --no-cache "${missing[@]}" ;; *) err "Unknown distro '$DISTRO'. Install manually: ${missing[*]}" return 1 ;; esac } # Parse dd's stderr line ("512 MB/s" or "512.4MB/s") -> MiB/s (approx) parse_dd_speed() { printf '%s\n' "$1" | grep -o '[0-9.]\+ *[kMG]B/s' | tail -n 1 | awk '{ v = $1; u = $2; sub(/B\/s/, "", u); m = 1 if (u == "kB" || u == "KB") m = 1 / 1024 else if (u == "MB") m = 1 else if (u == "GB") m = 1024 printf "%.0f", v * m }' } bytes_to_human() { awk -v b="$1" 'BEGIN{ s="B"; if (b>=1024){b/=1024;s="K"} if (b>=1024){b/=1024;s="M"} if (b>=1024){b/=1024;s="G"}; printf "%.1f%s", b, s}' } # True if GNU procps ps is available (supports --sort) have_gnu_ps() { ps --version >/dev/null 2>&1 } # ------------------------------------------------------------------ # 1. System overview # ------------------------------------------------------------------ overview() { title "System overview" local os memline swapline os="unknown" [ -r /etc/os-release ] && os="$(. /etc/os-release; echo "${PRETTY_NAME:-$ID}")" printf '%-22s %s\n' "OS:" "$os" printf '%-22s %s\n' "Kernel:" "$(uname -r) ($(uname -m))" printf '%-22s %s\n' "Hostname:" "$(hostname 2>/dev/null || echo '?')" local up="" read -r up < /proc/uptime printf '%-22s %s\n' "Uptime:" "$(awk -v s="${up%% *}" 'BEGIN{ d=int(s/86400); h=int((s%86400)/3600); m=int((s%3600)/60); printf "%dd %dh %dm", d, h, m}')" local model="?" cores=1 model="$(grep -m1 'model name' /proc/cpuinfo | cut -d: -f2 | sed 's/^ *//')" [ -n "$model" ] || model="$(uname -m)" cores="$(nproc 2>/dev/null || grep -c ^processor /proc/cpuinfo)" printf '%-22s %s (%s core%s)\n' "CPU:" "$model" "$cores" "$([ "$cores" -eq 1 ] || echo s)" read -r memline swapline </dev/null | awk '/^Mem:/ {print $2 " total / " $3 " used / " $7 " available"} /^Swap:/ {print $2 " total / " $3 " used"}') EOF printf '%-22s %s\n' "Memory:" "${memline:-unavailable (free -h missing)}" printf '%-22s %s\n' "Swap:" "${swapline:-none}" read -r memline < /proc/loadavg printf '%-22s %s\n' "Load avg:" "${memline%% *}" local virt="bare metal/unknown" if grep -qi '^flags.* hypervisor' /proc/cpuinfo 2>/dev/null; then virt="VM (hypervisor flag)" if grep -qi 'amazon' /sys/class/dmi/id/product_name 2>/dev/null; then virt="AWS EC2" elif grep -qi 'google' /sys/class/dmi/id/product_name 2>/dev/null; then virt="GCP" elif grep -qi 'virtualbox' /sys/class/dmi/id/product_name 2>/dev/null; then virt="VirtualBox" fi elif grep -q 'QEMU\|KVM\|VMware\|Microsoft Corporation' /sys/class/dmi/id/product_name 2>/dev/null; then virt="$(cat /sys/class/dmi/id/product_name)" fi printf '%-22s %s\n' "Virtualization:" "$virt" info "Filesystems:" df -h 2>/dev/null | grep -Ev 'tmpfs|devtmpfs|udev|squashfs|^Filesystem' | while read -r line; do [ -n "$line" ] && printf ' %s\n' "$line" done } # ------------------------------------------------------------------ # 2. Swap management # ------------------------------------------------------------------ swap_status() { title "Swap status" local sw_total sw_total="$(awk '/^SwapTotal:/ {print $2}' /proc/meminfo)" if [ -z "$sw_total" ] || [ "$sw_total" -eq 0 ]; then warn "No swap is active." else free -h 2>/dev/null | awk '/^Swap:/ {printf "Swap: %s total, %s used, %s free\n", $2, $3, $4}' info "Active swap devices/files (/proc/swaps):" sed 's/^/ /' /proc/swaps fi info "Current sysctl values:" printf ' vm.swappiness = %s\n' "$(cat /proc/sys/vm/swappiness 2>/dev/null || echo '?')" printf ' vm.vfs_cache_pressure = %s\n' "$(cat /proc/sys/vm/vfs_cache_pressure 2>/dev/null || echo '?')" printf ' vm.overcommit_memory = %s\n' "$(cat /proc/sys/vm/overcommit_memory 2>/dev/null || echo '?')" } swap_add() { need_root title "Add swapfile ($SWAP_FILE)" if grep -q "^$SWAP_FILE " /proc/swaps; then warn "$SWAP_FILE is already active."; return fi if [ -e "$SWAP_FILE" ]; then warn "$SWAP_FILE exists but is not active." if confirm "Fix permissions and swap it on?"; then chmod 600 "$SWAP_FILE" && swapon "$SWAP_FILE" && ok "Enabled." || err "swapon failed." fi return fi printf 'Swap size (e.g. 2G, 512M): ' read -r size echo "$size" | grep -Eq '^[0-9]+[KMG]$' || die "Invalid size '$size'. Use e.g. 2G" info "Creating $size swapfile (fallocate, falling back to dd)..." if ! fallocate -l "$size" "$SWAP_FILE" 2>/dev/null; then warn "fallocate failed (common on some VPS filesystems); using dd (slower)..." local count count="$(echo "$size" | sed 's/[KMG]//')" case "$size" in *K) count=$(( count / 1024 )); [ "$count" -eq 0 ] && count=1 ;; *G) count=$(( count * 1024 )) ;; esac dd if=/dev/zero of="$SWAP_FILE" bs=1M count="$count" 2>&1 | tail -n 1 fi chmod 600 "$SWAP_FILE" mkswap "$SWAP_FILE" || die "mkswap failed" swapon "$SWAP_FILE" || die "swapon failed (check: chattr +C on btrfs, filesystem support)" ok "Swap enabled." if [ -f /etc/fstab ] && grep -q "^$SWAP_FILE" /etc/fstab; then info "$SWAP_FILE already in /etc/fstab." elif confirm "Add $SWAP_FILE to /etc/fstab (persistent across reboots)?"; then echo "$SWAP_FILE none swap sw 0 0" >> /etc/fstab ok "Added to /etc/fstab." fi confirm "Tune swappiness now?" && swap_tune } swap_remove() { need_root title "Remove swapfile ($SWAP_FILE)" if swapoff "$SWAP_FILE" 2>/dev/null; then ok "Deactivated." else warn "Could not deactivate (not active or in use)." fi if [ -f "$SWAP_FILE" ] && confirm "Delete $SWAP_FILE?"; then rm -f "$SWAP_FILE" && ok "Deleted." fi if grep -q "^$SWAP_FILE" /etc/fstab 2>/dev/null && confirm "Remove fstab entry?"; then sed -i "\#^$SWAP_FILE #d" /etc/fstab && ok "Removed from /etc/fstab." fi } swap_tune() { need_root title "Tune swap / VM sysctls" printf 'swappiness [current %s] (10 = prefer RAM, 60 = default): ' "$(cat /proc/sys/vm/swappiness 2>/dev/null)" read -r sw sw="${sw:-10}" echo "$sw" | grep -Eq '^[0-9]+$' || die "Not a number." sysctl -w vm.swappiness="$sw" >/dev/null 2>&1 \ && ok "vm.swappiness=$sw (runtime)" \ || err "Could not set swappiness." echo "vm.swappiness=$sw" > /etc/sysctl.d/99-serverkit-swap.conf 2>/dev/null \ && ok "Persistent in /etc/sysctl.d/99-serverkit-swap.conf" \ || warn "Could not write /etc/sysctl.d (not persistent)." } swap_menu() { swap_status echo info "1) Add swapfile 2) Remove swapfile 3) Tune swappiness 0) Back" printf 'Choice: ' read -r c case "$c" in 1) swap_add ;; 2) swap_remove ;; 3) swap_tune ;; esac } # ------------------------------------------------------------------ # 3. Zram # ------------------------------------------------------------------ zram_status() { title "Zram status" local m size found=0 if grep -q zram /proc/swaps 2>/dev/null; then ok "zram is active as swap:" sed 's/^/ /' /proc/swaps | grep zram found=1 elif grep -qw zram /proc/modules 2>/dev/null; then info "zram module loaded but not used as swap." found=1 fi for m in /sys/block/zram*/; do [ -e "$m" ] || continue size="$(cat "$m/disksize" 2>/dev/null)" if [ -n "$size" ] && [ "$size" -gt 0 ] 2>/dev/null; then printf ' %s: %s (%s)\n' "$m" "$(bytes_to_human "$size")" \ "$(cat "$m/comp_algorithm" 2>/dev/null)" fi done if [ "$found" -eq 0 ]; then info "zram = compressed RAM used as fast swap. Great for low-RAM VPSes:" info "roughly 2-3x your RAM usable before touching slower disk swap." confirm "Set up zram swap now (size = RAM, lz4)?" && zram_setup fi } write_zram_helper() { cat > "$ZRAM_HELPER" <<'EOF' #!/bin/sh # serverkit zram helper modprobe zram 2>/dev/null grep -q '^/dev/zram0 ' /proc/swaps && exit 0 [ -e /sys/block/zram0/disksize ] || exit 1 echo reset > /sys/block/zram0/reset 2>/dev/null echo "$(awk '/^MemTotal:/ {print $2 * 1024}' /proc/meminfo)" > /sys/block/zram0/disksize || exit 1 echo lz4 > /sys/block/zram0/comp_algorithm 2>/dev/null echo lzo-rle > /sys/block/zram0/comp_algorithm 2>/dev/null mkswap /dev/zram0 >/dev/null 2>&1 || exit 1 swapon -p 100 /dev/zram0 EOF chmod +x "$ZRAM_HELPER" } zram_setup() { need_root local mem_kb disksize if command -v modprobe >/dev/null 2>&1; then modprobe zram || die "Could not load zram module (kernel support missing?)." fi [ -e /dev/zram0 ] || die "/dev/zram0 not found after modprobe." if grep -q "^/dev/zram0 " /proc/swaps; then warn "zram0 already in use as swap."; return fi swapoff /dev/zram0 2>/dev/null mem_kb="$(awk '/^MemTotal:/ {print $2}' /proc/meminfo)" disksize=$(( mem_kb * 1024 )) # bytes == RAM size echo reset > /sys/block/zram0/reset 2>/dev/null echo "$disksize" > /sys/block/zram0/disksize || die "Failed to set disksize." echo lz4 > /sys/block/zram0/comp_algorithm 2>/dev/null || echo lzo-rle > /sys/block/zram0/comp_algorithm 2>/dev/null mkswap /dev/zram0 >/dev/null swapon -p 100 /dev/zram0 || die "swapon /dev/zram0 failed." ok "zram swap active ($(bytes_to_human "$disksize")), priority 100 (preferred over disk swap)." if confirm "Persist zram setup across reboots?"; then write_zram_helper if command -v systemctl >/dev/null 2>&1; then cat > /etc/systemd/system/serverkit-zram.service <<'EOF' [Unit] Description=zram swap (serverkit) After=local-fs.target [Service] Type=oneshot RemainAfterExit=yes ExecStart=/usr/local/sbin/serverkit-zram.sh ExecStop=/sbin/swapoff /dev/zram0 [Install] WantedBy=multi-user.target EOF systemctl daemon-reload && systemctl enable --now serverkit-zram.service \ && ok "systemd service 'serverkit-zram' enabled." elif command -v rc-update >/dev/null 2>&1; then cat > /etc/init.d/serverkit-zram <<'EOF' #!/sbin/openrc-run description="zram swap (serverkit)" depend() { after localmount; } start() { ebegin "Enabling zram swap" /usr/local/sbin/serverkit-zram.sh eend $? } stop() { ebegin "Disabling zram swap"; swapoff /dev/zram0 2>/dev/null; eend 0; } EOF chmod +x /etc/init.d/serverkit-zram rc-update add serverkit-zram default && ok "OpenRC service 'serverkit-zram' enabled." else warn "No service manager found; helper written to $ZRAM_HELPER (call it from rc.local)." fi fi } # ------------------------------------------------------------------ # 4. Ballooning check # ------------------------------------------------------------------ balloon_check() { title "Memory ballooning check" local found=0 if grep -qw virtio_balloon /proc/modules || [ -e /sys/module/virtio_balloon ]; then ok "virtio_balloon active (KVM/QEMU host can reclaim memory)." found=1 fi if grep -qw hv_balloon /proc/modules || [ -e /sys/module/hv_balloon ]; then ok "Hyper-V dynamic memory (hv_balloon) active." found=1 fi if command -v vmware-toolbox-cmd >/dev/null 2>&1; then ok "VMware tools present — ballooning status: $(vmware-toolbox-cmd stat balloon 2>/dev/null || echo 'unknown')" found=1 elif grep -qi vmware /sys/class/dmi/id/product_name 2>/dev/null; then warn "VMware guest without open-vm-tools: ballooning will not work." found=1 fi if [ "$found" -eq 0 ]; then if grep -qi hypervisor /proc/cpuinfo 2>/dev/null; then warn "You are in a VM but no ballooning driver was detected." info "If the host needs to reclaim memory: modprobe virtio_balloon" else info "Looks like bare metal — ballooning is a VM concept, not applicable." fi fi } # ------------------------------------------------------------------ # Benchmarks (shared) # ------------------------------------------------------------------ bench_prepare() { mkdir -p "$BENCH_DIR" 2>/dev/null || die "Cannot create $BENCH_DIR" trap 'rm -rf "$BENCH_DIR"' EXIT } bench_cleanup() { rm -rf "$BENCH_DIR" 2>/dev/null trap - EXIT } drop_caches() { if [ "$(id -u)" -eq 0 ]; then sync echo 3 > /proc/sys/vm/drop_caches 2>/dev/null else info "(not root: results may be inflated by page cache)" fi } # ------------------------------------------------------------------ # 5. RAM bandwidth # ------------------------------------------------------------------ ram_bench() { title "RAM bandwidth test" if pkg_install "memory bandwidth test" mbw; then local total=0 i=0 r info "Running mbw 256MB memcpy (3 runs)..." while [ "$i" -lt 3 ]; do r="$(mbw -q -n 1 256 2>/dev/null | awk '/^AVG/ {print $2; exit}')" [ -n "$r" ] || { warn "mbw output not understood."; return; } total="$(awk -v t="$total" -v v="$r" 'BEGIN{print t + v}')" i=$((i + 1)) done ok "Average: $(awk -v t="$total" -v n="$i" 'BEGIN{printf "%.0f MiB/s", t/n}')" else info "mbw not available — dd fallback (write to page cache, rough estimate):" local out out="$(dd if=/dev/zero of="$BENCH_DIR/ramtest" bs=1M count=512 2>&1 | tail -n 1)" local sp sp="$(parse_dd_speed "$out")" [ -n "$sp" ] && ok "~${sp} MiB/s (includes allocation, underestimates true bandwidth)" || echo " $out" rm -f "$BENCH_DIR/ramtest" fi info "Typical VPS: 2,000–20,000 MiB/s. Far below that = noisy neighbor / throttling." } # ------------------------------------------------------------------ # 6. Disk throughput # ------------------------------------------------------------------ disk_bench() { title "Disk throughput test" local dir count avail out wb rb printf 'Directory to test [default: %s on /]: ' "$BENCH_DIR" read -r dir dir="${dir:-$BENCH_DIR}" mkdir -p "$dir" 2>/dev/null || dir="$BENCH_DIR" avail="$(df -k "$dir" 2>/dev/null | awk 'NR==2 {print $4}')" count=1024 if [ -n "$avail" ] && [ "$avail" -lt 2200000 ]; then count=256 warn "Low free space: using a 256 MiB test file." fi info "Writing ${count} MiB to $dir (synced)..." if dd if=/dev/zero of="$dir/bench.dat" bs=1M count="$count" conv=fsync 2>"$BENCH_DIR/w.err"; then : else info "conv=fsync not supported here, retrying plain write..." dd if=/dev/zero of="$dir/bench.dat" bs=1M count="$count" 2>>"$BENCH_DIR/w.err" || die "dd write failed" fi out="$(tail -n 1 "$BENCH_DIR/w.err")" wb="$(parse_dd_speed "$out")" [ -n "$wb" ] && ok "Sequential write: ${wb} MiB/s" || echo " $out" drop_caches info "Reading ${count} MiB back (caches dropped)..." dd if="$dir/bench.dat" of=/dev/null bs=1M 2>"$BENCH_DIR/r.err" || true out="$(tail -n 1 "$BENCH_DIR/r.err")" rb="$(parse_dd_speed "$out")" [ -n "$rb" ] && ok "Sequential read: ${rb} MiB/s" || echo " $out" rm -f "$dir/bench.dat" "$BENCH_DIR/w.err" "$BENCH_DIR/r.err" info "Typical: NVMe 1,000–3,500 | SATA SSD 400–550 | cloud disk 100–300 | HDD 80–150 MiB/s" } # ------------------------------------------------------------------ # 7. Disk latency # ------------------------------------------------------------------ latency_bench() { title "Disk latency test" local dir="${1:-}" if [ -z "$dir" ]; then printf 'Directory to test [default: %s on /]: ' "$BENCH_DIR" read -r dir dir="${dir:-$BENCH_DIR}" fi mkdir -p "$dir" 2>/dev/null || dir="$BENCH_DIR" if pkg_install "latency test" ioping; then info "20 uncached 4K requests to $dir:" ioping -c 20 -s 4k "$dir" 2>/dev/null | tail -n 3 ok "Reference: local SSD 0.05–0.5 ms | network cloud disk 0.5–5 ms | NFS 5–20 ms" return fi info "ioping not available — dd fallback (200 individually-synced 4K writes)..." drop_caches if dd if=/dev/zero of="$dir/lat.dat" bs=4k count=200 oflag=dsync 2>"$BENCH_DIR/lat.err"; then tail -n 1 "$BENCH_DIR/lat.err" | sed 's/^/ /' else info "oflag=dsync unsupported; plain 4K write rate instead:" dd if=/dev/zero of="$dir/lat.dat" bs=4k count=5000 2>&1 | tail -n 1 | sed 's/^/ /' fi rm -f "$dir/lat.dat" "$BENCH_DIR/lat.err" info "For the dsync test: per-request latency ≈ (time) / 200 requests. SSD: well under 5 ms." } # ------------------------------------------------------------------ # 8. CPU benchmark # ------------------------------------------------------------------ cpu_bench() { title "CPU benchmark" local cores cores="$(nproc 2>/dev/null || echo 1)" if pkg_install "CPU benchmark" sysbench; then info "Single-core (sysbench cpu, 10s)..." sysbench cpu --threads=1 --time=10 run 2>/dev/null | awk '/total number of events/ {print " events/s: " $NF}' info "All-core ($cores threads, 10s)..." sysbench cpu --threads="$cores" --time=10 run 2>/dev/null | awk '/total number of events/ {print " events/s: " $NF}' ok "events/s is comparable across machines using the same sysbench version." else info "sysbench not available — pure-bash fallback (single core, 10s):" local t0 n=0 x=1 t0="$(date +%s)" while [ "$(( $(date +%s) - t0 ))" -lt 10 ]; do x=$(( x * 1103515245 + 12345 )) n=$(( n + 1 )) done ok "Score: $n iterations/10s (single core). Run on another box to compare." info "Ballpark on a modern x86 cloud vCPU: 8–15M iterations." fi } # ------------------------------------------------------------------ # 9. Network speed test # ------------------------------------------------------------------ net_bench() { title "Network speed test" if command -v iperf3 >/dev/null 2>&1; then info "iperf3 found (run 'iperf3 -s' on the remote host first)." printf 'Remote host to test (blank to skip iperf): ' read -r host if [ -n "$host" ]; then iperf3 -c "$host" -t 10 2>&1 | tail -n 8 return fi fi if ! pkg_install "download speed test" curl; then err "No curl available for the download test." return fi printf 'Test file URL (blank = Hetzner 100MB): ' read -r url [ -n "$url" ] || url="https://speed.hetzner.de/100MB.bin" info "Downloading $url ..." local out speed mbps size_mb secs out="$(curl -o /dev/null -sS --max-time 120 \ -w '%{speed_download} %{size_download} %{time_total}' "$url")" || { err "curl failed: $out"; return; } speed="$(echo "$out" | awk '{print $1}')" mbps="$(awk -v s="$speed" 'BEGIN{printf "%.1f", s * 8 / 1000000}')" size_mb="$(echo "$out" | awk '{printf "%.0f", $2 / 1048576}')" secs="$(echo "$out" | awk '{printf "%.1f", $3}')" ok "Download: ${mbps} Mbit/s (${size_mb} MB in ${secs}s, single stream)" info "Single-stream TCP — parallel transfers may be faster; route matters." } # ------------------------------------------------------------------ # 10. TCP tuning (BBR) # ------------------------------------------------------------------ bbr_tune() { need_root title "TCP tuning — BBR + sane buffers" local cc cc="$(cat /proc/sys/net/ipv4/tcp_congestion_control 2>/dev/null)" info "Current: ${cc:-unknown} available: $(cat /proc/sys/net/ipv4/tcp_available_congestion_control 2>/dev/null || echo '?')" if [ "$cc" = "bbr" ]; then ok "BBR already active." return fi confirm "Enable BBR + fq (better throughput on lossy/high-latency links)?" || return cat > /etc/sysctl.d/99-serverkit-tcp.conf <<'EOF' # serverkit TCP tuning net.core.default_qdisc=fq net.ipv4.tcp_congestion_control=bbr net.core.rmem_max=16777216 net.core.wmem_max=16777216 net.ipv4.tcp_rmem=4096 87380 16777216 net.ipv4.tcp_wmem=4096 65536 16777216 net.ipv4.tcp_slow_start_after_idle=0 EOF if command -v sysctl >/dev/null 2>&1 && sysctl --system >/dev/null 2>&1; then : else sysctl -w net.core.default_qdisc=fq >/dev/null 2>&1 sysctl -w net.ipv4.tcp_congestion_control=bbr >/dev/null 2>&1 fi cc="$(cat /proc/sys/net/ipv4/tcp_congestion_control 2>/dev/null)" if [ "$cc" = "bbr" ]; then ok "BBR active (persistent in /etc/sysctl.d/99-serverkit-tcp.conf)." else warn "bbr not active — kernel may lack TCP_BBR (older kernel). Try a newer kernel." fi } # ------------------------------------------------------------------ # 11. Cleanup # ------------------------------------------------------------------ cleanup() { need_root title "Cleanup" local before after freed_kb before="$(df -k / | awk 'NR==2 {print $4}')" case "$DISTRO" in debian|ubuntu) info "apt: autoremove + clean..." apt-get autoremove -y -qq >/dev/null 2>&1 apt-get clean -qq 2>/dev/null ;; alpine) info "apk: cache clean..." apk cache clean 2>/dev/null || rm -rf /var/cache/apk/* 2>/dev/null ;; esac if [ -d /var/log/journal ] && command -v journalctl >/dev/null 2>&1; then if confirm "Vacuum systemd journal to 100M?"; then journalctl --vacuum-size=100M 2>&1 | tail -n 2 fi fi info "Biggest items under /var/log:" du -ak /var/log 2>/dev/null | sort -rn | head -n 10 | awk 'NR>1 {printf " %8.1f MiB %s\n", $1/1024, substr($0, index($0, $2))}' if command -v docker >/dev/null 2>&1; then if confirm "Run 'docker system prune -f' (unused images/containers/networks)?"; then docker system prune -f 2>&1 | tail -n 1 fi fi after="$(df -k / | awk 'NR==2 {print $4}')" freed_kb=$(( after - before )) [ "$freed_kb" -lt 0 ] && freed_kb=0 ok "Freed ~$(( freed_kb / 1024 )) MiB on /" } # ------------------------------------------------------------------ # 12. Disk health # ------------------------------------------------------------------ disk_health() { title "Disk health" info "TRIM (SSDs):" if command -v fstrim >/dev/null 2>&1; then local m out for m in / /home /var; do [ -d "$m" ] || continue grep -q " $m " /proc/mounts || continue if out="$(fstrim -v "$m" 2>&1)"; then printf ' %s: %s\n' "$m" "$out" else warn " TRIM skipped/failed on $m: $out" fi done if command -v systemctl >/dev/null 2>&1; then if systemctl is-enabled fstrim.timer >/dev/null 2>&1; then ok "fstrim.timer enabled (weekly TRIM)." else info "fstrim.timer not enabled — consider: systemctl enable --now fstrim.timer" fi fi else info " fstrim not available (Alpine: apk add util-linux-misc)." fi echo info "Inode usage (100% = too many files, not bytes):" df -i 2>/dev/null | grep -Ev 'tmpfs|devtmpfs|udev' | sed 's/^/ /' echo info "SMART health:" if command -v smartctl >/dev/null 2>&1; then local d for d in /dev/sd? /dev/vd? /dev/nvme? /dev/nvme?n?; do [ -b "$d" ] || continue printf ' %s: %s\n' "$d" \ "$(smartctl -H "$d" 2>/dev/null | grep -iE 'overall-health|smart health' | sed 's/^ *//')" done else info " smartctl not installed (smartmontools). Skipping." fi echo info "Top space hogs under /var:" du -xk /var 2>/dev/null | sort -rn | head -n 8 | awk '{printf " %8.1f MiB %s\n", $1/1024, substr($0, index($0, $2))}' } # ------------------------------------------------------------------ # 13. Top consumers & failed services # ------------------------------------------------------------------ top_procs() { title "Top consumers" info "Top 10 by memory:" if have_gnu_ps; then ps aux --sort=-rss | head -n 11 | awk 'NR>1 {printf " %-8s %5s%% %7.1f MiB %s\n", $1, $4, $6/1024, substr($0, index($0,$11), 60)}' else ps -o pid,user,rss,args 2>/dev/null | sort -k3 -rn | head -n 11 | awk 'NR>1 {printf " %-8s %7.1f MiB %s\n", $2, $3/1024, substr($0, index($0,$4), 60)}' fi echo info "Top 10 by CPU:" if have_gnu_ps; then ps aux --sort=-%cpu | head -n 11 | awk 'NR>1 {printf " %-8s %5s%% %s\n", $1, $3, substr($0, index($0,$11), 70)}' else top -b -n 1 2>/dev/null | head -n 16 | sed 's/^/ /' fi echo info "Failed services:" if command -v systemctl >/dev/null 2>&1; then local failed failed="$(systemctl list-units --state=failed --no-legend --no-pager 2>/dev/null)" if [ -n "$failed" ]; then printf '%s\n' "$failed" | sed 's/^/ /' else ok " none" fi elif command -v rc-status >/dev/null 2>&1; then local crashed crashed="$(rc-status --crashed 2>/dev/null | grep -i crashed)" if [ -n "$crashed" ]; then printf '%s\n' "$crashed" | sed 's/^/ /' else ok " none crashed" fi else info " no service manager detected" fi } # ------------------------------------------------------------------ # 14. Diagnostics: net, time, limits, OOM # ------------------------------------------------------------------ diagnostics() { title "Diagnostics" info "Public IP:" if command -v curl >/dev/null 2>&1; then timeout 8 curl -4 -s https://api.ipify.org && echo elif command -v wget >/dev/null 2>&1; then timeout 8 wget -qO- https://api.ipify.org && echo else warn " no curl/wget available" fi echo info "DNS resolution:" if command -v getent >/dev/null 2>&1 && getent hosts example.com >/dev/null 2>&1; then ok " example.com -> $(getent hosts example.com | awk '{print $1; exit}')" elif command -v nslookup >/dev/null 2>&1; then nslookup example.com 2>&1 | sed 's/^/ /' else warn " no getent/nslookup available" fi echo info "Gateway latency:" local gw gw="$(ip route show default 2>/dev/null | awk '{print $3; exit}')" if [ -n "$gw" ]; then ping -c 3 "$gw" 2>&1 | tail -n 2 | sed 's/^/ /' else warn " no default route" fi echo info "Time sync:" local timed=0 if command -v timedatectl >/dev/null 2>&1; then timedatectl status 2>/dev/null | grep -Ei 'System clock|NTP service|synchronized' | sed 's/^/ /' timed=1 fi if command -v chronyc >/dev/null 2>&1; then chronyc tracking 2>/dev/null | grep -Ei 'Stratum|System time' | sed 's/^/ /' timed=1 fi [ "$timed" -eq 0 ] && warn " no timedatectl/chronyc found — check time sync manually!" echo info "Limits:" printf ' open files (hard limit): %s\n' "$(ulimit -Hn)" printf ' inotify watches: %s\n' "$(cat /proc/sys/fs/inotify/max_user_watches 2>/dev/null || echo '?')" printf ' fs.file-max: %s\n' "$(cat /proc/sys/fs/file-max 2>/dev/null || echo '?')" info " (low fd limits are a classic cause of 'too many open files' outages)" echo info "OOM killer events:" local oom oom="$(dmesg 2>/dev/null | grep -i 'killed process' | tail -n 5)" if [ -n "$oom" ]; then printf '%s\n' "$oom" | sed 's/^/ /' warn " OOM killer has run — consider adding swap/zram or more RAM." else ok " none found in dmesg" fi } # ------------------------------------------------------------------ # Main menu # ------------------------------------------------------------------ main_menu() { while true; do hr printf '%sServerKit%s — %s (%s)\n\n' "$C_B" "$C_0" "$DISTRO" \ "$([ "$(id -u)" -eq 0 ] && echo root || echo "user $(whoami)")" cat <<'MENU' 1) System overview 2) Swap (status / add / remove / tune) 3) Zram setup & status 4) Ballooning check 5) Benchmark: RAM bandwidth 6) Benchmark: disk throughput 7) Benchmark: disk latency 8) Benchmark: CPU 9) Benchmark: network speed 10) TCP tuning (BBR) 11) Cleanup (free disk space) 12) Disk health (TRIM, SMART, inodes) 13) Top processes & failed services 14) Diagnostics (net, time, limits, OOM) 0) Exit MENU printf 'Choice: ' read -r choice case "$choice" in 1) overview ;; 2) swap_menu ;; 3) zram_status ;; 4) balloon_check ;; 5) bench_prepare; ram_bench; bench_cleanup ;; 6) bench_prepare; disk_bench; bench_cleanup ;; 7) bench_prepare; latency_bench; bench_cleanup ;; 8) bench_prepare; cpu_bench; bench_cleanup ;; 9) bench_prepare; net_bench; bench_cleanup ;; 10) bbr_tune ;; 11) cleanup ;; 12) disk_health ;; 13) top_procs ;; 14) diagnostics ;; 0) echo "Bye."; exit 0 ;; *) err "Unknown option." ;; esac echo info "Press Enter to continue..." read -r _ done } main_menu