a1a17e81a1
Includes updated JS challenge scripts with Claude-User whitelist, same-site referer bypass, Blackbox-Exporter allowed bot, and all new exporters, cheat sheets, and automation scripts.
438 lines
15 KiB
Bash
438 lines
15 KiB
Bash
#!/usr/bin/env bash
|
|
|
|
#########################################################################################
|
|
#### motd-generator.sh — Generate a dynamic MOTD with system stats and health info ####
|
|
#### Shows hostname, IP, uptime, disk, load, memory, updates, and service status ####
|
|
#### Dry-run by default for --install — use --force to write to update-motd.d ####
|
|
#### ####
|
|
#### Author: Phil Connor ####
|
|
#### Contact: contact@mylinux.work ####
|
|
#### License: MIT ####
|
|
#### Version 1.02 ####
|
|
#### ####
|
|
#### Usage: ####
|
|
#### ./motd-generator.sh ####
|
|
#### ./motd-generator.sh --plain ####
|
|
#### ./motd-generator.sh --install --force ####
|
|
#### ####
|
|
#### See --help for all options. ####
|
|
#########################################################################################
|
|
|
|
set -euo pipefail
|
|
|
|
# ── Defaults ──────────────────────────────────────────────────────────
|
|
VERBOSE="${VERBOSE:-false}"
|
|
COLOR="${COLOR:-auto}"
|
|
DRY_RUN="${DRY_RUN:-true}"
|
|
MODE="${MODE:-display}"
|
|
PLAIN="${PLAIN:-false}"
|
|
MOTD_TARGET="${MOTD_TARGET:-/etc/update-motd.d/99-custom}"
|
|
|
|
# ── State ─────────────────────────────────────────────────────────────
|
|
SCRIPT_NAME="$(basename "$0")"
|
|
readonly SCRIPT_NAME
|
|
|
|
# ── Colors ────────────────────────────────────────────────────────────
|
|
setup_colors() {
|
|
if [[ "$COLOR" == "never" ]]; then
|
|
RED="" GREEN="" YELLOW="" CYAN="" BOLD="" DIM="" RESET=""
|
|
return
|
|
fi
|
|
if [[ "$COLOR" == "always" ]] || [[ -t 1 ]]; then
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[0;33m'
|
|
CYAN='\033[0;36m'
|
|
BOLD='\033[1m'
|
|
DIM='\033[2m'
|
|
RESET='\033[0m'
|
|
else
|
|
RED="" GREEN="" YELLOW="" CYAN="" BOLD="" DIM="" RESET=""
|
|
fi
|
|
}
|
|
|
|
# ── Logging ───────────────────────────────────────────────────────────
|
|
log() { echo -e "${CYAN}[INFO]${RESET} $*"; }
|
|
warn() { echo -e "${YELLOW}[WARN]${RESET} $*" >&2; }
|
|
err() { echo -e "${RED}[ERROR]${RESET} $*" >&2; }
|
|
verbose() { if [[ "$VERBOSE" == "true" ]]; then echo -e "${DIM}[DEBUG]${RESET} $*"; fi; }
|
|
|
|
# ── Data Gathering ───────────────────────────────────────────────────
|
|
|
|
get_hostname() {
|
|
hostname -f 2>/dev/null || hostname
|
|
}
|
|
|
|
get_primary_ip() {
|
|
local ip=""
|
|
if command -v ip &>/dev/null; then
|
|
ip=$(ip route get 1.1.1.1 2>/dev/null | grep -oP 'src \K[\d.]+' | head -1)
|
|
fi
|
|
if [[ -z "$ip" ]] && command -v hostname &>/dev/null; then
|
|
ip=$(hostname -I 2>/dev/null | awk '{print $1}')
|
|
fi
|
|
echo "${ip:-N/A}"
|
|
}
|
|
|
|
get_uptime() {
|
|
uptime -p 2>/dev/null || uptime | sed 's/.*up //' | sed 's/, [0-9]* user.*//'
|
|
}
|
|
|
|
get_disk_usage() {
|
|
df / 2>/dev/null | tail -1 | awk '{print $5}' | tr -d '%'
|
|
}
|
|
|
|
get_load_average() {
|
|
cut -d' ' -f1-3 /proc/loadavg 2>/dev/null || echo "N/A"
|
|
}
|
|
|
|
get_memory_usage() {
|
|
if [[ -f /proc/meminfo ]]; then
|
|
local total_kb avail_kb
|
|
total_kb=$(awk '/^MemTotal:/ {print $2}' /proc/meminfo)
|
|
avail_kb=$(awk '/^MemAvailable:/ {print $2}' /proc/meminfo)
|
|
if [[ "$total_kb" -gt 0 ]]; then
|
|
local pct
|
|
pct=$(awk "BEGIN { printf \"%.0f\", ($total_kb - $avail_kb) * 100 / $total_kb }")
|
|
local total_mb=$(( total_kb / 1024 ))
|
|
local used_mb=$(( (total_kb - avail_kb) / 1024 ))
|
|
echo "${pct}|${used_mb}|${total_mb}"
|
|
return
|
|
fi
|
|
fi
|
|
echo "0|0|0"
|
|
}
|
|
|
|
get_pending_updates() {
|
|
if command -v apt-get &>/dev/null; then
|
|
apt list --upgradable 2>/dev/null | grep -c "upgradable" || true
|
|
elif command -v dnf &>/dev/null; then
|
|
dnf check-update --quiet 2>/dev/null | grep -cE "^\S" || true
|
|
elif command -v yum &>/dev/null; then
|
|
yum check-update --quiet 2>/dev/null | grep -cE "^\S" || true
|
|
else
|
|
echo "N/A"
|
|
fi
|
|
}
|
|
|
|
get_logged_in_users() {
|
|
who 2>/dev/null | wc -l
|
|
}
|
|
|
|
get_failed_services() {
|
|
if command -v systemctl &>/dev/null; then
|
|
systemctl --no-legend --state=failed 2>/dev/null | wc -l
|
|
else
|
|
echo "N/A"
|
|
fi
|
|
}
|
|
|
|
# ── Color Thresholds ─────────────────────────────────────────────────
|
|
|
|
threshold_color() {
|
|
local value="$1"
|
|
local warn_at="${2:-75}"
|
|
local crit_at="${3:-90}"
|
|
|
|
if [[ "$value" -ge "$crit_at" ]]; then
|
|
echo "$RED"
|
|
elif [[ "$value" -ge "$warn_at" ]]; then
|
|
echo "$YELLOW"
|
|
else
|
|
echo "$GREEN"
|
|
fi
|
|
}
|
|
|
|
load_color() {
|
|
local load_1m="$1"
|
|
local cpus
|
|
cpus=$(nproc 2>/dev/null || echo 1)
|
|
local pct
|
|
pct=$(awk "BEGIN { printf \"%.0f\", ($load_1m / $cpus) * 100 }")
|
|
threshold_color "$pct" 75 90
|
|
}
|
|
|
|
# ── MOTD Output ──────────────────────────────────────────────────────
|
|
|
|
generate_motd_plain() {
|
|
local host ip up disk_pct load mem_info mem_pct mem_used mem_total
|
|
local updates users_count failed_count
|
|
|
|
host=$(get_hostname)
|
|
ip=$(get_primary_ip)
|
|
up=$(get_uptime)
|
|
disk_pct=$(get_disk_usage)
|
|
load=$(get_load_average)
|
|
mem_info=$(get_memory_usage)
|
|
mem_pct=$(echo "$mem_info" | cut -d'|' -f1)
|
|
mem_used=$(echo "$mem_info" | cut -d'|' -f2)
|
|
mem_total=$(echo "$mem_info" | cut -d'|' -f3)
|
|
updates=$(get_pending_updates)
|
|
users_count=$(get_logged_in_users)
|
|
failed_count=$(get_failed_services)
|
|
|
|
local dc mc lc
|
|
dc=$(threshold_color "$disk_pct" 75 90)
|
|
mc=$(threshold_color "$mem_pct" 75 90)
|
|
local load_1m
|
|
load_1m=$(echo "$load" | awk '{print $1}')
|
|
lc=$(load_color "$load_1m")
|
|
|
|
echo -e " ${BOLD}Hostname:${RESET} $host"
|
|
echo -e " ${BOLD}IP Address:${RESET} $ip"
|
|
echo -e " ${BOLD}Uptime:${RESET} $up"
|
|
echo -e " ${BOLD}Disk (root):${RESET} ${dc}${disk_pct}%${RESET}"
|
|
echo -e " ${BOLD}Load Average:${RESET} ${lc}${load}${RESET}"
|
|
echo -e " ${BOLD}Memory:${RESET} ${mc}${mem_used}M / ${mem_total}M (${mem_pct}%)${RESET}"
|
|
echo -e " ${BOLD}Pending Updates:${RESET} $updates"
|
|
echo -e " ${BOLD}Logged-in Users:${RESET} $users_count"
|
|
echo -e " ${BOLD}Failed Services:${RESET} $failed_count"
|
|
}
|
|
|
|
generate_motd_box() {
|
|
local host ip up disk_pct load mem_info mem_pct mem_used mem_total
|
|
local updates users_count failed_count
|
|
|
|
host=$(get_hostname)
|
|
ip=$(get_primary_ip)
|
|
up=$(get_uptime)
|
|
disk_pct=$(get_disk_usage)
|
|
load=$(get_load_average)
|
|
mem_info=$(get_memory_usage)
|
|
mem_pct=$(echo "$mem_info" | cut -d'|' -f1)
|
|
mem_used=$(echo "$mem_info" | cut -d'|' -f2)
|
|
mem_total=$(echo "$mem_info" | cut -d'|' -f3)
|
|
updates=$(get_pending_updates)
|
|
users_count=$(get_logged_in_users)
|
|
failed_count=$(get_failed_services)
|
|
|
|
local dc mc lc
|
|
dc=$(threshold_color "$disk_pct" 75 90)
|
|
mc=$(threshold_color "$mem_pct" 75 90)
|
|
local load_1m
|
|
load_1m=$(echo "$load" | awk '{print $1}')
|
|
lc=$(load_color "$load_1m")
|
|
|
|
local fc_color="$GREEN"
|
|
if [[ "$failed_count" != "N/A" && "$failed_count" -gt 0 ]]; then
|
|
fc_color="$RED"
|
|
fi
|
|
|
|
# Build rows as "label|value" pairs to measure widest content
|
|
local label_w=18
|
|
local rows=(
|
|
"Hostname:|$host"
|
|
"IP Address:|$ip"
|
|
"Uptime:|$up"
|
|
"Disk (root):|${disk_pct}%"
|
|
"Load Average:|$load"
|
|
"Memory:|${mem_used}M / ${mem_total}M (${mem_pct}%)"
|
|
"Pending Updates:|$updates"
|
|
"Logged-in Users:|$users_count"
|
|
"Failed Services:|$failed_count"
|
|
)
|
|
|
|
# Calculate box width from widest content
|
|
local header="System Status: ${host}"
|
|
local dateline
|
|
dateline=$(date '+%Y-%m-%d %H:%M:%S %Z')
|
|
local w=${#header}
|
|
[[ ${#dateline} -gt $w ]] && w=${#dateline}
|
|
|
|
local row label value row_len
|
|
for row in "${rows[@]}"; do
|
|
label="${row%%|*}"
|
|
value="${row#*|}"
|
|
row_len=$(( label_w + 1 + ${#value} ))
|
|
[[ $row_len -gt $w ]] && w=$row_len
|
|
done
|
|
|
|
# Add 2 for inner padding (space on each side)
|
|
w=$(( w + 2 ))
|
|
# Minimum width
|
|
[[ $w -lt 56 ]] && w=56
|
|
|
|
# Build box-drawing borders at calculated width
|
|
local bar
|
|
bar=$(printf '═%.0s' $(seq 1 $((w + 2))))
|
|
local border="╔${bar}╗"
|
|
local bottom="╚${bar}╝"
|
|
local sep="╠${bar}╣"
|
|
|
|
BOX_W=$w # export to _box_row
|
|
|
|
echo ""
|
|
echo -e " ${CYAN}${border}${RESET}"
|
|
printf " ${CYAN}║${RESET} ${BOLD}%-${w}s${RESET} ${CYAN}║${RESET}\n" "$header"
|
|
printf " ${CYAN}║${RESET} ${DIM}%-${w}s${RESET} ${CYAN}║${RESET}\n" "$dateline"
|
|
echo -e " ${CYAN}${sep}${RESET}"
|
|
_box_row "Hostname: " "$host"
|
|
_box_row "IP Address: " "$ip"
|
|
_box_row "Uptime: " "$up"
|
|
_box_row "Disk (root): " "${disk_pct}%" "$dc"
|
|
_box_row "Load Average: " "$load" "$lc"
|
|
_box_row "Memory: " "${mem_used}M / ${mem_total}M (${mem_pct}%)" "$mc"
|
|
_box_row "Pending Updates: " "$updates"
|
|
_box_row "Logged-in Users: " "$users_count"
|
|
_box_row "Failed Services: " "$failed_count" "$fc_color"
|
|
echo -e " ${CYAN}${bottom}${RESET}"
|
|
echo ""
|
|
}
|
|
|
|
_box_row() {
|
|
# Print a row inside the box with correct padding regardless of content length
|
|
# Usage: _box_row "Label:" "value" [color_prefix]
|
|
local label="$1"
|
|
local value="$2"
|
|
local color="${3:-}"
|
|
local inner_w="${BOX_W:-56}"
|
|
|
|
# Build the visible text (no ANSI)
|
|
local vis_text=" ${label} ${value}"
|
|
local vis_len=${#vis_text}
|
|
|
|
# Pad to fill the box
|
|
local pad_len=$(( inner_w - vis_len ))
|
|
[[ $pad_len -lt 0 ]] && pad_len=0
|
|
local padding
|
|
padding=$(printf '%*s' "$pad_len" '')
|
|
|
|
# Build output with optional color on value
|
|
if [[ -n "$color" ]]; then
|
|
printf " ${CYAN}║${RESET} %s %b%s${RESET}%s ${CYAN}║${RESET}\n" "$label" "$color" "$value" "$padding"
|
|
else
|
|
printf " ${CYAN}║${RESET} %s %s%s ${CYAN}║${RESET}\n" "$label" "$value" "$padding"
|
|
fi
|
|
}
|
|
|
|
# ── Install ──────────────────────────────────────────────────────────
|
|
|
|
install_motd() {
|
|
if [[ "$DRY_RUN" == "true" ]]; then
|
|
log "[DRY-RUN] Would install MOTD script to ${MOTD_TARGET}"
|
|
log "[DRY-RUN] Run with --force to actually install"
|
|
echo ""
|
|
log "Generated script preview:"
|
|
echo " #!/bin/bash"
|
|
echo " # Generated by ${SCRIPT_NAME} on $(date '+%Y-%m-%d %H:%M:%S')"
|
|
echo " $(readlink -f "$0") --plain --no-color"
|
|
return
|
|
fi
|
|
|
|
if [[ $EUID -ne 0 ]]; then
|
|
err "Installation requires root privileges"
|
|
exit 1
|
|
fi
|
|
|
|
local script_path
|
|
script_path=$(readlink -f "$0")
|
|
|
|
local motd_dir
|
|
motd_dir=$(dirname "$MOTD_TARGET")
|
|
if [[ ! -d "$motd_dir" ]]; then
|
|
mkdir -p "$motd_dir"
|
|
fi
|
|
|
|
cat > "$MOTD_TARGET" <<MOTD_EOF
|
|
#!/bin/bash
|
|
# Generated by ${SCRIPT_NAME} on $(date '+%Y-%m-%d %H:%M:%S')
|
|
# Source: ${script_path}
|
|
${script_path} --plain
|
|
MOTD_EOF
|
|
|
|
chmod 755 "$MOTD_TARGET"
|
|
log "Installed MOTD script to ${MOTD_TARGET}"
|
|
log "The MOTD will update on each login"
|
|
}
|
|
|
|
# ══════════════════════════════════════════════════════════════════════
|
|
# USAGE
|
|
# ══════════════════════════════════════════════════════════════════════
|
|
|
|
usage() {
|
|
cat <<EOF
|
|
${SCRIPT_NAME} — Generate a dynamic MOTD with system stats and health info
|
|
|
|
USAGE:
|
|
${SCRIPT_NAME} [OPTIONS]
|
|
|
|
OPTIONS:
|
|
--plain Plain text output (no box art)
|
|
--install Install as ${MOTD_TARGET} (dry-run by default)
|
|
--force Actually install (disables dry-run)
|
|
--verbose Enable debug output
|
|
--no-color Disable colored output
|
|
--help Show this help
|
|
|
|
ENVIRONMENT VARIABLES:
|
|
COLOR Color mode: auto, always, never (default: auto)
|
|
MOTD_TARGET Install target (default: ${MOTD_TARGET})
|
|
|
|
EXAMPLES:
|
|
# Display MOTD with box art
|
|
./motd-generator.sh
|
|
|
|
# Plain format for piping
|
|
./motd-generator.sh --plain --no-color
|
|
|
|
# Preview install (dry-run)
|
|
./motd-generator.sh --install
|
|
|
|
# Actually install
|
|
sudo ./motd-generator.sh --install --force
|
|
EOF
|
|
}
|
|
|
|
# ══════════════════════════════════════════════════════════════════════
|
|
# ARGUMENT PARSING
|
|
# ══════════════════════════════════════════════════════════════════════
|
|
|
|
parse_args() {
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--plain)
|
|
PLAIN="true"; shift ;;
|
|
--install)
|
|
MODE="install"; shift ;;
|
|
--force)
|
|
DRY_RUN="false"; shift ;;
|
|
--verbose)
|
|
VERBOSE="true"; shift ;;
|
|
--no-color)
|
|
COLOR="never"; shift ;;
|
|
--help|-h)
|
|
setup_colors
|
|
usage
|
|
exit 0 ;;
|
|
*)
|
|
err "Unknown option: $1"
|
|
echo "Run ${SCRIPT_NAME} --help for usage" >&2
|
|
exit 1 ;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
# ══════════════════════════════════════════════════════════════════════
|
|
# MAIN
|
|
# ══════════════════════════════════════════════════════════════════════
|
|
|
|
main() {
|
|
parse_args "$@"
|
|
setup_colors
|
|
|
|
case "$MODE" in
|
|
install)
|
|
install_motd
|
|
;;
|
|
display)
|
|
if [[ "$PLAIN" == "true" ]]; then
|
|
generate_motd_plain
|
|
else
|
|
generate_motd_box
|
|
fi
|
|
;;
|
|
esac
|
|
}
|
|
|
|
main "$@"
|