cf-node-bootstrap/lib/skel/.bashrc.d/11-quickinfo.bashrc
pyr0ball e33d6bb1ad feat: record install-time state, show CircuitForge node info in quickinfo
- lib/cf-apps.functions: cf-provision-node/install-cf-app/
  provision-orchard-node now write to ~/.config/cf-node-bootstrap/state.conf
  (record-cf-node-state) - provisioning profile, each installed app
  (name:install_type:clone_dir), and orchard role/coordinator URL. The
  orchard role/coordinator is read back from circuitforge-orch's own
  generated ~/.config/circuitforge/cf-orch.env rather than tracked
  separately, since that's the actual source of truth for what its
  interactive installer decided.
- 11-quickinfo.bashrc: new "CircuitForge Node" section (show_cf_node_info,
  default true) displaying the provisioning profile, orchard role/
  coordinator if joined, and each installed app with a best-effort
  running/stopped status. Docker-compose apps are checked by compose
  project from their clone dir rather than by container name, since
  compose's generated container names don't match the app name directly.
  Section is entirely absent if the state file doesn't exist yet (i.e.
  the CF Apps menu has never been run on this node).
2026-07-17 21:16:07 -07:00

1255 lines
No EOL
40 KiB
Bash
Executable file

#!/bin/bash
###############################################################
# MOTD/Login Quick Information script #
# This script pulls information from a variety of sources #
# in a Linux system, and aggregates it into a small table #
# that is displayed upon terminal login to the system. #
# #
# Written by Alan "pyr0ball" Weinstock #
# Enhanced for multi-distro compatibility #
###############################################################
quickinfo_version=4.1.2
prbl_functons_req_ver=2.1.0
# Source PRbL Functions locally or retrieve from online
if [ -f $prbl_functions ] ; then
source $prbl_functions
else
if [ -f ${rundir}/functions ] ; then
source ${rundir}/functions
else
# Iterate through get commands and fall back on next if unavailable
if command -v curl >/dev/null 2>&1; then
source <(curl -ks 'https://raw.githubusercontent.com/pyr0ball/PRbL/main/functions')
elif command -v wget >/dev/null 2>&1; then
source <(wget -qO- 'https://raw.githubusercontent.com/pyr0ball/PRbL/main/functions')
elif command -v fetch >/dev/null 2>&1; then
source <(fetch -qo- 'https://raw.githubusercontent.com/pyr0ball/PRbL/main/functions')
else
echo "Error: curl, wget, and fetch commands are not available. Please install one to retrieve PRbL functions."
exit 1
fi
fi
fi
# Locate script information
scriptname="${BASH_SOURCE[0]##*/}"
rundir="${BASH_SOURCE[0]%/*}"
# Degree symbol used in temperature readouts. PRbL/functions sets
# unicode_supported when sourced; fall back to a bare "C" (no degree sign)
# on terminals/locales without Unicode support.
deg_symbol='°'
[[ "$unicode_supported" == true ]] || deg_symbol=''
########################################
# Default quickinfo bashrc Preferences #
########################################
# Disable run on non-interactive sessions (set true to disable)
interactive_only=false
# Network Adapter Preferences
# set false to hide network adapters without valid IP's
show_disconnected=true
# Ignored network adapter names (regex match)
# separate adapter names with '\|' ex. "lo\|tun0"
filtered_adapters="lo"
# Interfaces matching these patterns are collapsed into a single summary line
# (count + up/down) instead of one line each. Keys are extended-regex
# patterns (matched with =~), values are the label shown in the summary.
# Docker especially can spawn dozens of br-*/veth* interfaces that would
# otherwise flood this section.
declare -A grouped_adapter_patterns=(
["^br-"]="Docker bridges"
["^veth"]="Docker veth pairs"
["^docker[0-9]"]="Docker interfaces"
)
# Disks
allowed_disk_prefixes=(
"sd"
"md"
"mapper"
"nvme"
"mmcblk"
"root"
"vd"
"xvd"
)
disallowed_disk_prefixes=(
"boot"
"snap"
"loop"
)
# Settings for additional information display
show_gpu_info=true
show_container_info=true
show_service_status=true
show_security_info=true
show_smart_status=false # Requires root privileges or sudo config
show_top_processes=true
show_power_info=true # Enable power monitoring (requires appropriate hardware/sensors)
show_power_details=false
show_cf_node_info=true # Show installed CircuitForge apps / orchard role (requires cf-node-bootstrap's install-time state file)
# Services to check (space separated list)
critical_services="sshd nginx apache2 mysqld mariadb docker containerd kubelet cron"
#######################################################
# Do not change these unless you know what you're doing
allowed_disk_prefixes_string=""
disallowed_disk_prefixes_string=""
# Build the allowed disk prefixes pattern
for prefix in "${allowed_disk_prefixes[@]}"; do
if [ -z "$allowed_disk_prefixes_string" ]; then
allowed_disk_prefixes_string="$prefix"
else
allowed_disk_prefixes_string+="\\|$prefix"
fi
done
# Build the disallowed disk prefixes pattern
for prefix in "${disallowed_disk_prefixes[@]}"; do
if [ -z "$disallowed_disk_prefixes_string" ]; then
disallowed_disk_prefixes_string="$prefix"
else
disallowed_disk_prefixes_string+="\\|$prefix"
fi
done
# check PRbL functions version
if command -v vercomp >/dev/null 2>&1; then
if [[ $(vercomp $functionsrev $prbl_functons_req_ver) == 2 ]] ; then
warn "PRbL functions installed are lower than recommended ($prbl_functons_req_ver)"
warn "Some features may not work as expected"
fi
fi
################################
# Enhanced OS Detection Function
################################
detect_os() {
# Initialize OS info variables
OS="Unknown"
VER="Unknown"
ID="Unknown"
# Method 1: lsb_release
if command -v lsb_release >/dev/null 2>&1; then
OS=$(lsb_release -si)
VER=$(lsb_release -sr)
ID=$(lsb_release -si | tr '[:upper:]' '[:lower:]')
return
fi
# Method 2: /etc/os-release
if [ -f /etc/os-release ]; then
. /etc/os-release
OS=$NAME
VER=$VERSION_ID
ID=${ID:-$NAME}
ID=$(echo $ID | tr '[:upper:]' '[:lower:]')
return
fi
# Method 3: /etc/lsb-release
if [ -f /etc/lsb-release ]; then
. /etc/lsb-release
OS=$DISTRIB_ID
VER=$DISTRIB_RELEASE
ID=$(echo $DISTRIB_ID | tr '[:upper:]' '[:lower:]')
return
fi
# Method 4: /etc/{system}-release files
for release_file in /etc/*-release; do
if [ -f "$release_file" ] && [ "$release_file" != "/etc/os-release" ] && [ "$release_file" != "/etc/lsb-release" ]; then
OS=$(head -n1 "$release_file" | cut -d' ' -f1)
VER=$(head -n1 "$release_file" | grep -o -E '[0-9]+\.[0-9]+' | head -n1)
ID=$(echo $OS | tr '[:upper:]' '[:lower:]')
return
fi
done
# Method 5: Fallback to uname
OS=$(uname -s)
VER=$(uname -r)
ID=$(echo $OS | tr '[:upper:]' '[:lower:]')
}
################################
# Package Manager Detection
################################
detect_package_manager() {
package_manager="unknown"
update_cmd=""
update_check_cmd=""
if command -v apt >/dev/null 2>&1 || command -v apt-get >/dev/null 2>&1; then
package_manager="apt"
update_cmd="apt update && apt upgrade"
if [ -f /usr/lib/update-notifier/apt-check ]; then
update_check_cmd="/usr/lib/update-notifier/apt-check --human-readable"
else
update_check_cmd="apt list --upgradable 2>/dev/null | grep -c ^"
fi
elif command -v dnf >/dev/null 2>&1; then
package_manager="dnf"
update_cmd="dnf upgrade"
update_check_cmd="dnf check-update --quiet | grep -v \"^$\" | wc -l"
elif command -v yum >/dev/null 2>&1; then
package_manager="yum"
update_cmd="yum update"
update_check_cmd="yum check-update --quiet | grep -v \"^$\" | wc -l"
elif command -v pacman >/dev/null 2>&1; then
package_manager="pacman"
update_cmd="pacman -Syu"
update_check_cmd="pacman -Qu | wc -l"
elif command -v zypper >/dev/null 2>&1; then
package_manager="zypper"
update_cmd="zypper update"
update_check_cmd="zypper list-updates | grep -c \"^v \""
elif command -v apk >/dev/null 2>&1; then
package_manager="apk"
update_cmd="apk update && apk upgrade"
update_check_cmd="apk version -v | grep -c upgradable"
fi
}
################################
# Check for Updates
################################
check_for_updates() {
packages_available=0
security_updates=0
need_updates=false
case $package_manager in
apt)
if [ -f /usr/lib/update-notifier/apt-check ]; then
# Capture both stdout and stderr, and handle malformed output
apt_check_output=$(/usr/lib/update-notifier/apt-check 2>&1)
# Validate that we have a proper semicolon-separated output
if [[ "$apt_check_output" == *";"* ]]; then
packages_available=$(echo "$apt_check_output" | cut -d';' -f1)
security_updates=$(echo "$apt_check_output" | cut -d';' -f2)
# Validate that packages_available is a valid number
if [[ "$packages_available" =~ ^[0-9]+$ ]]; then
if [ $packages_available -gt 0 ]; then
packages="${packages_available} updates can be installed"
need_updates=true
else
packages="0 updates available"
fi
else
packages="Error parsing update count"
fi
# Validate that security_updates is a valid number
if [[ "$security_updates" =~ ^[0-9]+$ ]]; then
if [ $security_updates -gt 0 ]; then
supdates="${security_updates} security updates"
need_updates=true
else
supdates="0 security updates"
fi
else
supdates="Error parsing security updates"
fi
else
packages="Invalid update-notifier output"
fi
else
# If apt-check isn't available, try an alternative method
if command -v apt >/dev/null 2>&1; then
# This method requires root/sudo, will be empty if not available
# FIX: Properly capture and process the apt list output
apt_output=$(apt list --upgradable 2>/dev/null)
if [ $? -eq 0 ]; then
# Only count lines that actually contain "upgradable" to avoid parsing errors
packages_count=$(echo "$apt_output" | grep -c "upgradable" || echo "0")
# Ensure we have a clean numeric value
if [[ "$packages_count" =~ ^[0-9]+$ ]]; then
packages_available=$packages_count
if [ $packages_available -gt 0 ]; then
packages="${packages_available} updates can be installed"
need_updates=true
else
packages="0 updates available"
fi
else
packages="Error checking updates"
fi
else
packages="Need privileges to check updates"
fi
else
packages="apt not available"
fi
fi
;;
dnf|yum)
# Check if we can run as non-root
if timeout 5 $package_manager check-update -q &>/dev/null; then
# Capture the output in a variable and ensure it's a valid number
check_output=$($update_check_cmd)
if [[ "$check_output" =~ ^[0-9]+$ ]]; then
packages_available=$check_output
if [ $packages_available -gt 0 ]; then
packages="${packages_available} updates can be installed"
need_updates=true
else
packages="0 updates available"
fi
else
packages="Error checking updates"
fi
else
packages="Need root to check updates"
fi
;;
pacman)
if pacman -Qu &>/dev/null; then
# Ensure we get a clean numeric value
check_output=$(pacman -Qu | wc -l)
if [[ "$check_output" =~ ^[0-9]+$ ]]; then
packages_available=$check_output
if [ $packages_available -gt 0 ]; then
packages="${packages_available} updates can be installed"
need_updates=true
else
packages="0 updates available"
fi
else
packages="Error checking updates"
fi
else
packages="Need to run 'pacman -Sy' to check updates"
fi
;;
zypper)
if timeout 5 zypper list-updates &>/dev/null; then
# Ensure we get a clean numeric value
check_output=$(zypper list-updates | grep '^v ' | wc -l)
if [[ "$check_output" =~ ^[0-9]+$ ]]; then
packages_available=$check_output
if [ $packages_available -gt 0 ]; then
packages="${packages_available} updates can be installed"
need_updates=true
else
packages="0 updates available"
fi
else
packages="Error checking updates"
fi
else
packages="Need root to check updates"
fi
;;
apk)
if timeout 5 apk version -v &>/dev/null; then
# Ensure we get a clean numeric value
check_output=$(apk version -v | grep -c upgradable)
if [[ "$check_output" =~ ^[0-9]+$ ]]; then
packages_available=$check_output
if [ $packages_available -gt 0 ]; then
packages="${packages_available} updates can be installed"
need_updates=true
else
packages="0 updates available"
fi
else
packages="Error checking updates"
fi
else
packages="Need root to check updates"
fi
;;
*)
packages="Unknown package manager"
;;
esac
# Check for release upgrade
release_upgrade=""
if [ -x /usr/lib/ubuntu-release-upgrader/release-upgrade-motd ]; then
if [ -f /var/lib/ubuntu-release-upgrader/release-upgrade-available ]; then
release_upgrade="$(cat /var/lib/ubuntu-release-upgrader/release-upgrade-available)"
need_updates=true
fi
fi
}
################################
# CPU Temperature
################################
get_cpu_temp() {
cputemp="N/A"
# Method 1: lm-sensors
if command -v sensors >/dev/null 2>&1; then
temp=$(LC_ALL=C sensors | grep -m 1 -E 'Core 0|CPU Temp|Tdie' | grep -o -E '[0-9]+\.[0-9]+' | head -n1)
if [ ! -z "$temp" ]; then
cputemp=$(printf "%.0f" $temp)
fi
fi
# Method 2: /sys/class/thermal
if [ "$cputemp" = "N/A" ] && [ -d /sys/class/thermal ]; then
for zone in /sys/class/thermal/thermal_zone*/temp; do
if [ -f "$zone" ]; then
temp=$(cat "$zone")
if [ $temp -gt 0 ]; then
# Convert millidegrees to degrees
cputemp=$(( temp / 1000 ))
break
fi
fi
done
fi
# Method 3: Raspberry Pi specific
if [ "$cputemp" = "N/A" ] && [ -f /sys/class/thermal/thermal_zone0/temp ]; then
temp=$(cat /sys/class/thermal/thermal_zone0/temp)
cputemp=$(( temp / 1000 ))
fi
# Add color warning if temperature is high
if [[ "$cputemp" != "N/A" ]]; then
if [[ "$cputemp" -gt "65" ]]; then
cputemp="${ong}${blk}${cputemp}${deg_symbol}C${dfl}"
else
cputemp="${cputemp}${deg_symbol}C"
fi
fi
}
################################
# GPU Information
################################
get_gpu_info() {
gpu_info="N/A"
gpu_temp="N/A"
gpu_util="N/A"
# Check for NVIDIA GPU with nvidia-smi
if command -v nvidia-smi >/dev/null 2>&1; then
gpu_info=$(nvidia-smi --query-gpu=name --format=csv,noheader | head -n1 | cut -c -30)
gpu_temp=$(nvidia-smi --query-gpu=temperature.gpu --format=csv,noheader | head -n1)
gpu_util=$(nvidia-smi --query-gpu=utilization.gpu --format=csv,noheader | head -n1 | cut -d' ' -f1)
# Add color to GPU temperature
if [[ "$gpu_temp" != "N/A" ]]; then
if [[ "$gpu_temp" -gt "70" ]]; then
gpu_temp="${ong}${blk}${gpu_temp}${deg_symbol}C${dfl}"
else
gpu_temp="${gpu_temp}${deg_symbol}C"
fi
fi
have_gpu=true
return
fi
# Check for AMD GPU with rocm-smi
if command -v rocm-smi >/dev/null 2>&1; then
gpu_info=$(rocm-smi --showproductname | grep -v "===" | head -n1 | awk '{print $2}' | cut -c -30)
gpu_temp=$(rocm-smi --showtemp | grep -v "===" | head -n1 | awk '{print $2}')
# Add color to GPU temperature
if [[ "$gpu_temp" != "N/A" ]]; then
if [[ "$gpu_temp" -gt "70" ]]; then
gpu_temp="${ong}${blk}${gpu_temp}${deg_symbol}C${dfl}"
else
gpu_temp="${gpu_temp}${deg_symbol}C"
fi
fi
have_gpu=true
return
fi
# Basic GPU detection through lspci
if command -v lspci >/dev/null 2>&1; then
gpu_info=$(lspci | grep -i 'vga\|3d\|display' | head -n1 | cut -d: -f3 | cut -c -30)
if [ ! -z "$gpu_info" ]; then
have_gpu=true
return
fi
fi
have_gpu=false
}
################################
# Get Container/VM Information
################################
get_container_info() {
container_type="N/A"
container_stats="N/A"
in_container=false
# Check if running in Docker
if [ -f /.dockerenv ] || grep -q docker /proc/1/cgroup 2>/dev/null; then
container_type="Docker"
in_container=true
fi
# Check if running in LXC/LXD
if grep -q lxc /proc/1/cgroup 2>/dev/null; then
container_type="LXC"
in_container=true
fi
# Check if running in Kubernetes
if [ -f /var/run/secrets/kubernetes.io/serviceaccount/namespace ]; then
container_type="Kubernetes Pod"
in_container=true
fi
# Check for systemd-nspawn
if [ -d /run/systemd/nspawn ]; then
container_type="systemd-nspawn"
in_container=true
fi
# Check for VM
if [ "$in_container" = false ]; then
if command -v systemd-detect-virt >/dev/null 2>&1; then
virt=$(systemd-detect-virt)
if [ "$virt" != "none" ]; then
container_type="VM ($virt)"
in_container=true
fi
elif [ -e /proc/cpuinfo ]; then
if grep -q "hypervisor" /proc/cpuinfo; then
container_type="VM (unknown)"
in_container=true
fi
fi
fi
# Get Docker container stats if Docker is installed
if command -v docker >/dev/null 2>&1 && [ "$in_container" = false ]; then
container_count=$(docker ps -q 2>/dev/null | wc -l || echo "0")
if [ "$container_count" -gt 0 ]; then
container_stats="$container_count running Docker containers"
have_containers=true
else
container_stats="No running containers"
have_containers=false
fi
fi
# Get LXC container stats if LXC is installed
# if command -v lxc >/dev/null 2>&1 && [ "$in_container" = false ]; then
# container_count=$(lxc list --format csv -c s | grep -c RUNNING 2>/dev/null || echo "0")
# if [ "$container_count" -gt 0 ]; then
# container_stats="$container_stats, $container_count running LXC containers"
# have_containers=true
# fi
# fi
}
################################
# Get CircuitForge Node Info
################################
# Reads the state file cf-node-bootstrap's installer writes (see
# lib/cf-apps.functions:record-cf-node-state) - not present unless that
# installer's CF Apps menu has been run on this node at least once.
# Best-effort running/stopped check for a single installed app. Docker
# compose-based apps are checked by compose project (from the clone dir),
# not by container name, since compose's generated container names don't
# match the app name directly. Single-container apps (no compose file
# present) fall back to a name match against `docker ps`.
_cf_node_app_status() {
local dir="$1" install_type="$2" app_name="$3"
if [[ "$install_type" != "docker" ]]; then
echo "n/a"
return
fi
command -v docker >/dev/null 2>&1 || { echo "n/a"; return; }
local compose_file=""
for f in compose.yml docker-compose.yml; do
if [ -f "${dir}/${f}" ]; then
compose_file="$f"
break
fi
done
if [ -n "$compose_file" ]; then
if (cd "$dir" && docker compose -f "$compose_file" ps -q 2>/dev/null | grep -q .); then
echo "running"
else
echo "stopped"
fi
elif docker ps --format '{{.Names}}' 2>/dev/null | grep -qx "$app_name"; then
echo "running"
else
echo "stopped"
fi
}
get_cf_node_info() {
cf_node_info_available=false
cf_node_profile_display="N/A"
cf_node_apps_display=()
cf_node_orchard_display=""
local state_file="${HOME}/.config/cf-node-bootstrap/state.conf"
[ -f "$state_file" ] || return
cf_node_info_available=true
# State file only defines cf_node_*/cf_app_*/cf_orchard_* keys (written by
# record-cf-node-state), so sourcing it here won't clobber other quickinfo
# variables.
source "$state_file"
cf_node_profile_display="${cf_node_profile:-unknown}"
local var app_value app_name install_type clone_dir status status_disp
for var in $(compgen -v cf_app_ 2>/dev/null); do
app_value="${!var}"
IFS=':' read -r app_name install_type clone_dir <<< "$app_value"
status=$(_cf_node_app_status "$clone_dir" "$install_type" "$app_name")
case "$status" in
running) status_disp="${grn}running${dfl}" ;;
stopped) status_disp="${lrd}stopped${dfl}" ;;
*) status_disp="${gry}${install_type}${dfl}" ;;
esac
cf_node_apps_display+=("${app_name} ${gry}(${install_type})${dfl}: ${status_disp}")
done
if [ -n "$cf_orchard_role" ]; then
cf_node_orchard_display="${bld}Role:${dfl} ${ylw}${cf_orchard_role}${dfl}"
if [ -n "$cf_orchard_coordinator_url" ]; then
cf_node_orchard_display="${cf_node_orchard_display} | ${bld}Coordinator:${dfl} ${cyn}${cf_orchard_coordinator_url}${dfl}"
fi
fi
}
# Get public IP address using multiple fallbacks
get_wan_ip() {
wan_ip="Unavailable"
# Try multiple services with a timeout to avoid hanging
if command -v curl >/dev/null 2>&1; then
wan_ip=$(timeout 2 curl -s ifconfig.me 2>/dev/null || \
timeout 2 curl -s icanhazip.com 2>/dev/null || \
timeout 2 curl -s ipecho.net/plain 2>/dev/null || \
timeout 2 curl -s api.ipify.org 2>/dev/null || \
echo "Unavailable")
elif command -v wget >/dev/null 2>&1; then
wan_ip=$(timeout 2 wget -qO- ifconfig.me 2>/dev/null || \
timeout 2 wget -qO- icanhazip.com 2>/dev/null || \
timeout 2 wget -qO- ipecho.net/plain 2>/dev/null || \
timeout 2 wget -qO- api.ipify.org 2>/dev/null || \
echo "Unavailable")
elif command -v fetch >/dev/null 2>&1; then
wan_ip=$(timeout 2 fetch -qo- ifconfig.me 2>/dev/null || \
timeout 2 fetch -qo- icanhazip.com 2>/dev/null || \
timeout 2 fetch -qo- ipecho.net/plain 2>/dev/null || \
timeout 2 fetch -qo- api.ipify.org 2>/dev/null || \
echo "Unavailable")
fi
echo "$wan_ip"
}
################################
# Get Service Status
################################
get_service_status() {
services_down=()
if command -v systemctl >/dev/null 2>&1; then
for service in $critical_services; do
if systemctl is-active --quiet $service 2>/dev/null; then
# Service is running, do nothing
:
elif systemctl is-enabled --quiet $service 2>/dev/null; then
# Service is enabled but not running
services_down+=("$service")
fi
done
elif command -v service >/dev/null 2>&1; then
for service in $critical_services; do
if service $service status &>/dev/null; then
# Service is running, do nothing
:
else
# Check if service exists but is not running
if [ -f "/etc/init.d/$service" ]; then
services_down+=("$service")
fi
fi
done
fi
if [ ${#services_down[@]} -gt 0 ]; then
service_status="${lrd}Services down: ${services_down[*]}${dfl}"
have_service_issues=true
else
service_status="${grn}All monitored services running${dfl}"
have_service_issues=false
fi
}
################################
# Get Security Information
################################
get_security_info() {
failed_logins=0
last_login="N/A"
ssh_sessions=0
# Get failed login attempts
# NOTE: filter by _COMM=sshd (the actual binary name) rather than -u sshd/ssh
# (the systemd unit name), since that varies by distro (ssh.service on
# Debian/Ubuntu, sshd.service on RHEL/Arch) and silently returned zero
# matches on the wrong one.
if command -v journalctl >/dev/null 2>&1; then
failed_logins=$(journalctl _COMM=sshd --since yesterday 2>/dev/null | grep -c "Failed password")
elif [ -f /var/log/auth.log ]; then
failed_logins=$(grep -c "Failed password" /var/log/auth.log)
elif [ -f /var/log/secure ]; then
failed_logins=$(grep -c "Failed password" /var/log/secure)
fi
# Get last successful login
if command -v last >/dev/null 2>&1; then
last_user=$(who am i | awk '{print $1}')
if [ ! -z "$last_user" ]; then
last_login=$(last -n 2 $last_user | head -n 1 | awk '{print $4" "$5" "$6" "$7}')
fi
fi
# Get current SSH sessions
if command -v who >/dev/null 2>&1; then
ssh_sessions=$(who | grep -c "pts")
fi
# Get firewall status without requiring root privileges
if command -v ufw >/dev/null 2>&1; then
# Check if ufw is loaded in kernel modules - doesn't require root
if lsmod | grep -q "^ufw"; then
# Check if there are any ufw rules by looking at iptables (if readable)
if [ -r /etc/ufw/user.rules ]; then
ufw_rules=$(grep -c "^-A" /etc/ufw/user.rules)
if [ $ufw_rules -gt 0 ]; then
firewall_status="UFW: likely active with $ufw_rules rules"
else
firewall_status="UFW: likely inactive (no rules found)"
fi
else
# Check status of ufw service without sudo
if systemctl is-active --quiet ufw 2>/dev/null; then
firewall_status="UFW: service running"
else
firewall_status="UFW: service not running"
fi
fi
else
firewall_status="UFW: module not loaded (inactive)"
fi
elif command -v firewall-cmd >/dev/null 2>&1; then
# For firewalld, check if the service is running
if systemctl is-active --quiet firewalld 2>/dev/null; then
firewall_status="firewalld: service running"
else
firewall_status="firewalld: service not running"
fi
elif command -v iptables >/dev/null 2>&1; then
# Check for iptables rules (non-root might not see all)
if iptables -L -n 2>/dev/null | grep -q "REJECT\|DROP"; then
firewall_status="iptables: active"
elif [ -r /etc/iptables/rules.v4 ] || [ -r /etc/sysconfig/iptables ]; then
# Check for saved rules files
firewall_status="iptables: likely active (rules file exists)"
elif systemctl is-active --quiet iptables 2>/dev/null; then
firewall_status="iptables: service running"
else
firewall_status="iptables: status unknown"
fi
else
firewall_status="No firewall detected"
fi
}
################################
# Get SMART Disk Status
################################
get_smart_status() {
smart_alerts=()
if command -v smartctl >/dev/null 2>&1; then
for disk in $(lsblk -d -o name | grep -E "^($allowed_disk_prefixes_string)" | grep -v -E "^($disallowed_disk_prefixes_string)"); do
# Try to run smartctl without sudo first
smart_status=$(smartctl -H /dev/$disk 2>/dev/null)
if [ $? -ne 0 ]; then
# If failed, try with sudo if available (won't work without password prompt in most cases)
if command -v sudo >/dev/null 2>&1; then
smart_status=$(sudo -n smartctl -H /dev/$disk 2>/dev/null || echo "Permission denied")
else
smart_status="Permission denied"
fi
fi
if echo "$smart_status" | grep -q "FAILED"; then
smart_alerts+=("${lrd}/dev/$disk FAILED${dfl}")
elif echo "$smart_status" | grep -q "Permission denied"; then
continue
fi
done
fi
if [ ${#smart_alerts[@]} -gt 0 ]; then
smart_status="${lrd}SMART Alerts: ${smart_alerts[*]}${dfl}"
have_smart_issues=true
else
smart_status="${grn}No SMART alerts detected${dfl}"
have_smart_issues=false
fi
}
################################
# Get Top Processes
################################
get_top_processes() {
if command -v ps >/dev/null 2>&1; then
# 'comm' (bare process name, no path/args) is used instead of 'args' so the
# display shows the actual binary name rather than a path truncated before
# reaching it. The 'ps' invocation gathering this data is itself excluded
# (NR>1 skips the header; $4!="ps" drops the sampling process, which can
# otherwise show an inflated %cpu artifact from its own brief runtime).
# Get top 3 CPU processes
top_cpu=$(ps -eo pcpu,pid,user,comm --sort=-pcpu | awk 'NR>1 && $4!="ps"' | head -n 3 | awk '{printf "%-5s %-8s %-15.15s\n", $1"%", $3, $4}')
# Get top 3 memory processes
top_mem=$(ps -eo pmem,pid,user,comm --sort=-pmem | awk 'NR>1 && $4!="ps"' | head -n 3 | awk '{printf "%-5s %-8s %-15.15s\n", $1"%", $3, $4}')
fi
}
################################
# Main Logic
################################
# Run detection functions
detect_os
detect_package_manager
check_for_updates
get_cpu_temp
# Get network information
location=$(uname -a | awk '{print $2}')
image_version=$(uname -r)
software_version="$OS $VER"
mem_usage=$(LC_ALL=C free -m | grep Mem | awk '{print $3"M/"$2"M"}')
# Get swap usage
swap_usage=$(LC_ALL=C free -m | grep Swap | awk '{if ($2 > 0) print $3"M/"$2"M"; else print "None"}')
# Get number of CPUs and load averages
num_cpus=$(nproc 2>/dev/null || grep -c ^processor /proc/cpuinfo 2>/dev/null || echo 1)
load_check=$(LC_ALL=C uptime | sed -r 's|.*load average: ([\.0-9]+), ([\.0-9]+), ([\.0-9]+)|\1 \2 \3|g')
load_averages=$(echo "$load_check $num_cpus" | awk '{printf "5min: %.0f%% ", $1/$4*100} {printf "10min: %.0f%% ", $2/$4*100} {printf "15min: %.0f%%", $3/$4*100}' ORS=' ')
# Calculate CPU utilization
# Read /proc/stat file (for first datapoint)
read cpu user nice system idle iowait irq softirq steal guest < /proc/stat
# compute active and total utilizations
cpu_active_prev=$((user+system+nice+softirq+steal))
cpu_total_prev=$((user+system+nice+softirq+steal+idle+iowait))
sleep 0.3
# Read /proc/stat file (for second datapoint)
read cpu user nice system idle iowait irq softirq steal guest < /proc/stat
# compute active and total utilizations
cpu_active_cur=$((user+system+nice+softirq+steal))
cpu_total_cur=$((user+system+nice+softirq+steal+idle+iowait))
# compute CPU utilization (%)
cpu_util=$((100*( cpu_active_cur-cpu_active_prev ) / (cpu_total_cur-cpu_total_prev) ))
# Get uptime in human-readable format
# NOTE: this token-position parsing is locale/format-fragile (procps wording
# and column count shift across distros); LC_ALL=C at least stabilizes the
# wording. A /proc/uptime-based rewrite would be more robust; tracked as follow-up.
uptime_raw="$(LC_ALL=C uptime)"
if [[ "$(grep -iq day <<< "$uptime_raw" ; echo $?)" == "0" ]] ; then
# first logic gate, if the system has been online for at least a day
if [[ "$(grep -iq min <<< "$uptime_raw" ; echo $?)" == "0" ]] ; then
# nested logic gate. If the system has been online for more than a day
# but less than a day and an hour, the tokens shift and requires alternate parsing
s_uptime=$(awk '{print $3 " " $4 " " $5 " Minutes"}' <<< "$uptime_raw")
else # gate for if the system has been online longer than one day and one hour
s_uptime=$(awk '{print $3 " " $4 " " $5 " Hours"}' <<< "$uptime_raw")
fi
else # logic gate for if the system has been online less than a day but more than an hour
if [[ "$(grep -iq min <<< "$uptime_raw" ; echo $?)" == "1" ]] ; then
s_uptime=$(awk '{print $3 " Hours "}' <<< "$uptime_raw")
else
s_uptime=$(awk '{print $3 " Minutes "}' <<< "$uptime_raw")
fi
fi
# Get additional information if enabled
if [ "$show_gpu_info" = true ]; then
get_gpu_info
fi
if [ "$show_container_info" = true ]; then
get_container_info
fi
if [ "$show_cf_node_info" = true ]; then
get_cf_node_info
fi
if [ "$show_service_status" = true ]; then
get_service_status
fi
if [ "$show_security_info" = true ]; then
get_security_info
fi
if [ "$show_smart_status" = true ]; then
get_smart_status
fi
if [ "$show_top_processes" = true ]; then
get_top_processes
fi
# Check for fsck message
if [ -x /usr/lib/update-notifier/update-motd-fsck-at-reboot ]; then
fsck_needed="$(exec /usr/lib/update-notifier/update-motd-fsck-at-reboot)"
fi
# Check if reboot required by updates
reboot_required=""
if [ -f /var/run/reboot-required ]; then
reboot_required=$(cat /var/run/reboot-required)
fi
# Network display and filtering
declare -a adapters=()
declare -a ips=()
declare -a macs=()
declare -a ifups=()
for device in $(ls /sys/class/net/ 2>/dev/null | grep -v "$filtered_adapters"); do
adapters+=($device)
_ip=$(ip -o -f inet addr show $device 2>/dev/null | awk '{print $4}' | cut -d/ -f 1 | head -n 1)
if valid-ip $_ip; then
ips+=("$_ip")
ifups+=("up")
else
ips+=("${red}disconnected${dfl}")
ifups+=("down")
fi
if [ -f /sys/class/net/${device}/address ]; then
macs+=("$(cat /sys/class/net/${device}/address)")
else
macs+=("unknown")
fi
done
# Run as a background process to avoid hanging the login
wan_ip=$(get_wan_ip &)
# Disk array setup
declare -a logicals=()
declare -a mounts=()
declare -a usages=()
declare -a freespaces=()
diskinfo=$(/bin/df -h | grep "$allowed_disk_prefixes_string" | grep -v "$disallowed_disk_prefixes_string")
logicals=($(echo "$diskinfo" | awk '{print $1}'))
mounts=($(echo "$diskinfo" | awk '{print $6}'))
usages=($(echo "$diskinfo" | awk '{print $5}'))
freespaces=($(echo "$diskinfo" | awk '{print $4}'))
# Add color to disk usage if more than 90%
for i in "${!usages[@]}"; do
usage="${usages[$i]}"
percentage="${usage%\%}"
if [[ "$percentage" -gt "90" ]]; then
usages[$i]="${lrd}${usage}${dfl}"
elif [[ "$percentage" -gt "75" ]]; then
usages[$i]="${ong}${usage}${dfl}"
fi
done
###############################################
# Display the formatted information
###############################################
boxtop
boxline ""
boxline "${bld}${unl}System Information:${dfl} ${grn}${unl}$location${dfl}"
boxline " ${bld}OS:${dfl} ${ong}${software_version}${dfl} | ${bld}Kernel:${dfl} ${pur}${image_version}${dfl}"
boxline " ${bld}Uptime:${dfl} ${s_uptime}"
# Container information
if [ "$show_container_info" = true ] && [[ "$in_container" = true || "$have_containers" = true ]]; then
boxline ""
boxline "${bld}${unl}Container/VM Information:${dfl}"
if [ "$in_container" = true ]; then
boxline " ${bld}Running in:${dfl} ${ylw}${container_type}${dfl}"
fi
if [ "$have_containers" = true ]; then
boxline " ${bld}Container Status:${dfl} ${container_stats}"
fi
fi
# CircuitForge node information
if [ "$show_cf_node_info" = true ] && [ "$cf_node_info_available" = true ]; then
boxline ""
boxline "${bld}${unl}CircuitForge Node:${dfl}"
boxline " ${bld}Profile:${dfl} ${mag}${cf_node_profile_display}${dfl}"
if [ -n "$cf_node_orchard_display" ]; then
boxline " ${bld}Orchard:${dfl} ${cf_node_orchard_display}"
fi
if [ "${#cf_node_apps_display[@]}" -gt 0 ]; then
boxline " ${bld}Installed apps:${dfl}"
for app_line in "${cf_node_apps_display[@]}"; do
boxline " ${app_line}"
done
fi
fi
# Network information
boxline ""
boxline "${bld}${unl}Network Information:${dfl}"
boxline " |${unl}Interface IP Address MAC Address${dfl}"
boxline " |-------------------------------------------------------"
# Echo out network arrays with consistent spacing
declare -A group_total=()
declare -A group_up=()
for ((i=0; i<"${#adapters[@]}"; i++ )); do
# Skip displaying disconnected interfaces if flag is false
if [[ $show_disconnected != true ]] && [[ ${ifups[$i]} != "up" ]]; then
continue
fi
# Collapse interfaces matching a grouped pattern into a summary tally
# instead of printing them individually (see grouped_adapter_patterns).
group_label=""
for pattern in "${!grouped_adapter_patterns[@]}"; do
if [[ "${adapters[$i]}" =~ $pattern ]]; then
group_label="${grouped_adapter_patterns[$pattern]}"
break
fi
done
if [[ -n "$group_label" ]]; then
group_total[$group_label]=$(( ${group_total[$group_label]:-0} + 1 ))
if [[ ${ifups[$i]} == "up" ]]; then
group_up[$group_label]=$(( ${group_up[$group_label]:-0} + 1 ))
fi
continue
fi
# Extract plain text without color codes for formatting
ip_plain=$(echo "${ips[$i]}" | sed 's/\x1b\[[0-9;]*m//g')
# Format columns with fixed widths, adding colors as requested
interface_col=$(printf "${cyn}%-15s${dfl}" "${adapters[$i]}")
# Handle IP color - yellow for connected, keep existing red for disconnected
if [[ ${ifups[$i]} == "up" ]]; then
# For connected IPs, use yellow instead of cyan
ip_col=$(printf "${ylw}%-19s${dfl}" "$ip_plain")
else
# For disconnected IPs, keep the red color but maintain spacing
ip_col="${ips[$i]}$(printf '%*s' $((19 - ${#ip_plain})) '')"
fi
# Add light blue for MAC addresses
mac_col=$(printf "${lbl}%s${dfl}" "${macs[$i]}")
# Combine the formatted columns
formatted_line="|${interface_col}${ip_col}${mac_col}"
boxline " $formatted_line"
done
# Print one summary line per collapsed group, e.g. "Docker bridges (84): 3 up"
for group_label in "${!group_total[@]}"; do
boxline " |${cyn}${group_label}${dfl} (${group_total[$group_label]}): ${grn}${group_up[$group_label]:-0} up${dfl}"
done
# Display WAN IP separately
boxline " |-------------------------------------------------------"
boxline " ${bld}WAN IP:${dfl} ${grn}${wan_ip}${dfl}"
# System status
boxline ""
boxline "${bld}${unl}System Resources:${dfl}"
boxline " ${bld}CPU Load:${dfl} ${load_averages}"
boxline " ${bld}CPU Temp:${dfl} ${lbl}${cputemp}${dfl} | ${bld}Utilization:${dfl} ${lrd}${cpu_util}%${dfl}"
# GPU information if available
if [ "$show_gpu_info" = true ] && [ "$have_gpu" = true ]; then
boxline " ${bld}GPU:${dfl} ${ylw}${gpu_info}${dfl}"
if [ "$gpu_temp" != "N/A" ]; then
boxline " ${bld}GPU Temp:${dfl} ${lbl}${gpu_temp}${dfl} | ${bld}Utilization:${dfl} ${lrd}${gpu_util}%${dfl}"
fi
fi
# Memory usage
boxline " ${bld}Memory:${dfl} ${mem_usage} | ${bld}Swap:${dfl} ${swap_usage}"
# Storage info
boxline " ${bld}${unl}Disk Info:${dfl}"
boxline " |${unl}Usage Free Mount Volumes${dfl}"
boxline " |-----------------------------------------------"
# Loop through each disk and display info with precise column widths
for ((i=0; i<"${#logicals[@]}"; i++)); do
# Extract the usage text without color codes for formatting
usage_plain=$(echo "${usages[$i]}" | sed 's/\x1b\[[0-9;]*m//g')
# Format the columns
usage_col=$(printf "%-8s" "$usage_plain")
free_col=$(printf "%-12s" "${freespaces[$i]}")
mount_col=$(printf "%-13s" "${mounts[$i]}")
volume_col="${logicals[$i]}"
# Replace the plain text with the colored version, maintaining the same length
usage_col="${usages[$i]}$(printf '%*s' $((8 - ${#usage_plain})) '')"
# Combine the formatted columns
formatted_line="|${usage_col}${free_col}${mount_col}${volume_col}"
boxline " $formatted_line"
done
# SMART status if enabled and issues detected
if [ "$show_smart_status" = true ] && [ "$have_smart_issues" = true ]; then
boxline ""
boxline "${bld}${unl}Disk Health:${dfl}"
boxline " ${smart_status}"
fi
# Service status if enabled and issues detected
if [ "$show_service_status" = true ] && [ "$have_service_issues" = true ]; then
boxline ""
boxline "${bld}${unl}Service Status:${dfl}"
boxline " ${service_status}"
fi
# Security information if enabled
if [ "$show_security_info" = true ]; then
boxline ""
boxline "${bld}${unl}Security Information:${dfl}"
boxline " ${bld}Failed logins (24h):${dfl} ${failed_logins}"
boxline " ${bld}Active SSH sessions:${dfl} ${ssh_sessions}"
if [ "$last_login" != "N/A" ]; then
boxline " ${bld}Last login:${dfl} ${last_login}"
fi
boxline " ${bld}Firewall:${dfl} ${firewall_status}"
fi
# Power information if enabled
if [ "$show_power_info" = true ]; then
get_power_info
if [[ "$system_power" != "N/A" || "$cpu_power" != "N/A" || "$gpu_power" != "N/A" || "$battery_power" != "N/A" ]]; then
boxline ""
boxline "${bld}${unl}Power Usage:${dfl}"
if [[ "$system_power" != "N/A" ]]; then
boxline " ${bld}System:${dfl} ${ong}${system_power}${dfl}"
fi
if [[ "$cpu_power" != "N/A" ]]; then
boxline " ${bld}CPU:${dfl} ${lrd}${cpu_power}${dfl}"
fi
if [[ "$gpu_power" != "N/A" ]]; then
boxline " ${bld}GPU:${dfl} ${lrd}${gpu_power}${dfl}"
fi
if [[ "$battery_power" != "N/A" ]]; then
boxline " ${bld}Battery:${dfl} ${ylw}${battery_power}${dfl}"
fi
# Show detailed breakdown if requested
if [[ "$show_power_details" == "true" && ${#power_sources[@]} -gt 0 ]]; then
boxline " ${bld}${unl}Power Details:${dfl}"
for ((i=0; i<${#power_sources[@]}; i++)); do
boxline " $(printf "%-25s %3d%s" "${power_sources[$i]}:" "${power_values[$i]}" "${power_units[$i]}")"
done
fi
fi
fi
# Top processes if enabled
if [ "$show_top_processes" = true ]; then
boxline ""
boxline "${bld}${unl}Top CPU Processes:${dfl}"
echo "$top_cpu" | while IFS= read -r line; do
boxline " $line"
done
boxline ""
boxline "${bld}${unl}Top Memory Processes:${dfl}"
echo "$top_mem" | while IFS= read -r line; do
boxline " $line"
done
fi
# Update information if needed
if [[ "$need_updates" == "true" ]]; then
boxline ""
boxline "${bld}${unl}Updates:${dfl}"
boxline " ${packages}"
if [ ! -z "$supdates" ]; then
boxline " ${supdates}"
fi
if [ ! -z "$release_upgrade" ]; then
boxline " ${release_upgrade}"
fi
fi
# System maintenance notifications
if [ ! -z "${fsck_needed}" ] || [ ! -z "${reboot_required}" ]; then
boxline ""
boxline "${bld}${unl}System Maintenance:${dfl}"
boxline " ${fsck_needed} ${reboot_required}"
fi
boxline ""
boxbottom