diff --git a/machine_report_macos.sh b/machine_report_macos.sh new file mode 100755 index 0000000..b20920a --- /dev/null +++ b/machine_report_macos.sh @@ -0,0 +1,381 @@ +#!/bin/bash +# TR-100 Machine Report (macOS Compatible) +# Copyright © 2024, U.S. Graphics, LLC. BSD-3-Clause License. + +# Global variables +MIN_NAME_LEN=5 +MAX_NAME_LEN=13 + +MIN_DATA_LEN=20 +MAX_DATA_LEN=32 + +BORDERS_AND_PADDING=7 + +# Basic configuration, change as needed +report_title="UNITED STATES GRAPHICS COMPANY" +last_login_ip_present=0 +zfs_present=0 +zfs_filesystem="zroot/ROOT/os" + +# Utilities +max_length() { + local max_len=0 + local len + + for str in "$@"; do + len=${#str} + if (( len > max_len )); then + max_len=$len + fi + done + + if [ $max_len -lt $MAX_DATA_LEN ]; then + printf '%s' "$max_len" + else + printf '%s' "$MAX_DATA_LEN" + fi +} + +# All data strings must go here +set_current_len() { + CURRENT_LEN=$(max_length \ + "$report_title" \ + "$os_name" \ + "$os_kernel" \ + "$net_hostname" \ + "$net_machine_ip" \ + "$net_client_ip" \ + "$net_current_user" \ + "$cpu_model" \ + "$cpu_cores_per_socket vCPU(s) / $cpu_sockets Socket(s)" \ + "$cpu_hypervisor" \ + "$cpu_freq GHz" \ + "$cpu_1min_bar_graph" \ + "$cpu_5min_bar_graph" \ + "$cpu_15min_bar_graph" \ + "$zfs_used_gb/$zfs_available_gb GB [$disk_percent%]" \ + "$disk_bar_graph" \ + "$zfs_health" \ + "$root_used_gb/$root_total_gb GB [$disk_percent%]" \ + "${mem_used_gb}/${mem_total_gb} GiB [${mem_percent}%]" \ + "${mem_bar_graph}" \ + "$last_login_time" \ + "$last_login_ip" \ + "$last_login_ip" \ + "$sys_uptime" \ + ) +} + +PRINT_HEADER() { + local length=$((CURRENT_LEN+MAX_NAME_LEN+BORDERS_AND_PADDING)) + + local top="┌" + local bottom="├" + for (( i = 0; i < length - 2; i++ )); do + top+="┬" + bottom+="┴" + done + top+="┐" + bottom+="┤" + + printf '%s\n' "$top" + printf '%s\n' "$bottom" +} + +PRINT_CENTERED_DATA() { + local max_len=$((CURRENT_LEN+MAX_NAME_LEN-BORDERS_AND_PADDING)) + local text="$1" + local total_width=$((max_len + 12)) + + local text_len=${#text} + local padding_left=$(( (total_width - text_len) / 2 )) + local padding_right=$(( total_width - text_len - padding_left )) + + printf "│%${padding_left}s%s%${padding_right}s│\n" "" "$text" "" +} + +PRINT_DIVIDER() { + # either "top" or "bottom", no argument means middle divider + local side="$1" + case "$side" in + "top") + local left_symbol="├" + local middle_symbol="┬" + local right_symbol="┤" + ;; + "bottom") + local left_symbol="└" + local middle_symbol="┴" + local right_symbol="┘" + ;; + *) + local left_symbol="├" + local middle_symbol="┼" + local right_symbol="┤" + esac + + local length=$((CURRENT_LEN+MAX_NAME_LEN+BORDERS_AND_PADDING)) + local divider="$left_symbol" + for (( i = 0; i < length - 3; i++ )); do + divider+="─" + if [ "$i" -eq 14 ]; then + divider+="$middle_symbol" + fi + done + divider+="$right_symbol" + printf '%s\n' "$divider" +} + +PRINT_DATA() { + local name="$1" + local data="$2" + local max_data_len=$CURRENT_LEN + + # Pad name + local name_len=${#name} + if (( name_len < MIN_NAME_LEN )); then + name=$(printf "%-${MIN_NAME_LEN}s" "$name") + elif (( name_len > MAX_NAME_LEN )); then + name=$(echo "$name" | cut -c 1-$((MAX_NAME_LEN-3)))... + else + name=$(printf "%-${MAX_NAME_LEN}s" "$name") + fi + + # Truncate or pad data + local data_len=${#data} + if (( data_len >= MAX_DATA_LEN || data_len == MAX_DATA_LEN-1 )); then + data=$(echo "$data" | cut -c 1-$((MAX_DATA_LEN-3-2)))... + else + data=$(printf "%-${max_data_len}s" "$data") + fi + + printf "│ %-${MAX_NAME_LEN}s │ %s │\n" "$name" "$data" +} + +PRINT_FOOTER() { + local length=$((CURRENT_LEN+MAX_NAME_LEN+BORDERS_AND_PADDING)) + local footer="└" + for (( i = 0; i < length - 3; i++ )); do + footer+="─" + if [ "$i" -eq 14 ]; then + footer+="┴" + fi + done + footer+="┘" + printf '%s\n' "$footer" +} + +bar_graph() { + local percent + local num_blocks + local width=$CURRENT_LEN + local graph="" + local used=$1 + local total=$2 + + if (( total == 0 )); then + percent=0 + else + percent=$(awk -v used="$used" -v total="$total" 'BEGIN { printf "%.2f", (used / total) * 100 }') + fi + + num_blocks=$(awk -v percent="$percent" -v width="$width" 'BEGIN { printf "%d", (percent / 100) * width }') + + for (( i = 0; i < num_blocks; i++ )); do + graph+="█" + done + for (( i = num_blocks; i < width; i++ )); do + graph+="░" + done + printf "%s" "${graph}" +} + +get_ip_addr() { + # Initialize variables + ipv4_address="" + ipv6_address="" + + # Check if ifconfig command exists + if command -v ifconfig &> /dev/null; then + # Try to get IPv4 address using ifconfig + ipv4_address=$(ifconfig | awk ' + /^[a-z]/ {iface=$1} + iface != "lo0:" && iface !~ /^docker/ && /inet / && !/127\.0\.0\.1/ && !found_ipv4 {found_ipv4=1; print $2}') + + # If IPv4 address not available, try IPv6 using ifconfig + if [ -z "$ipv4_address" ]; then + ipv6_address=$(ifconfig | awk ' + /^[a-z]/ {iface=$1} + iface != "lo0:" && iface !~ /^docker/ && /inet6 / && !/::1/ && !found_ipv6 {found_ipv6=1; print $2}') + fi + fi + + # If neither IPv4 nor IPv6 address is available, assign "No IP found" + if [ -z "$ipv4_address" ] && [ -z "$ipv6_address" ]; then + ip_address="No IP found" + else + # Prioritize IPv4 if available, otherwise use IPv6 + ip_address="${ipv4_address:-$ipv6_address}" + fi + + printf '%s' "$ip_address" +} + +# Operating System Information (macOS) +os_name="macOS $(sw_vers -productVersion)" +os_kernel="$(uname -s) $(uname -r)" + +# Network Information +net_current_user=$(whoami) +net_hostname=$(hostname) +if [ -z "$net_hostname" ]; then net_hostname="Not Defined"; fi + +net_machine_ip=$(get_ip_addr) +net_client_ip=$(who am i | awk '{print $5}' | tr -d '()') +if [ -z "$net_client_ip" ]; then + net_client_ip="Not connected" +fi + +# Get DNS from scutil +net_dns_ip=($(scutil --dns | grep 'nameserver\[[0-9]*\]' | awk '{print $3}' | sort -u)) + +# CPU Information (macOS) +cpu_model="$(sysctl -n machdep.cpu.brand_string | awk '{print $1 " " $2 " " $3 " " $4}')" +cpu_hypervisor="Bare Metal" + +# Check if running in a VM +if sysctl -n machdep.cpu.features | grep -q "VMX\|SVM"; then + vm_check=$(system_profiler SPHardwareDataType | grep "Model Name" | grep -i "virtual\|vmware\|parallels") + if [ -n "$vm_check" ]; then + cpu_hypervisor="Virtual Machine" + fi +fi + +cpu_cores="$(sysctl -n hw.ncpu)" +cpu_cores_per_socket="$cpu_cores" +cpu_sockets="1" +cpu_freq="$(sysctl -n hw.cpufrequency 2>/dev/null | awk '{ printf "%.2f", $1 / 1000000000 }')" +if [ -z "$cpu_freq" ]; then + cpu_freq="$(sysctl -n hw.cpufrequency_max 2>/dev/null | awk '{ printf "%.2f", $1 / 1000000000 }')" +fi +if [ -z "$cpu_freq" ]; then + cpu_freq="N/A" +fi + +# Load averages +load_avg_1min=$(sysctl -n vm.loadavg | awk '{print $2}') +load_avg_5min=$(sysctl -n vm.loadavg | awk '{print $3}') +load_avg_15min=$(sysctl -n vm.loadavg | awk '{print $4}') + +# Memory Information (macOS) +mem_total=$(sysctl -n hw.memsize) +mem_total_kb=$((mem_total / 1024)) + +# Get memory pressure and calculate used memory +vm_stat_output=$(vm_stat) +page_size=$(vm_stat | grep "page size" | awk '{print $8}') + +pages_wired=$(echo "$vm_stat_output" | grep "Pages wired down" | awk '{print $4}' | tr -d '.') +pages_active=$(echo "$vm_stat_output" | grep "Pages active" | awk '{print $3}' | tr -d '.') +pages_compressed=$(echo "$vm_stat_output" | grep "Pages occupied by compressor" | awk '{print $5}' | tr -d '.') + +mem_used_bytes=$(((pages_wired + pages_active + pages_compressed) * page_size)) +mem_used_kb=$((mem_used_bytes / 1024)) + +mem_percent=$(awk -v used="$mem_used_kb" -v total="$mem_total_kb" 'BEGIN { printf "%.2f", (used / total) * 100 }') +mem_total_gb=$(echo "$mem_total_kb" | awk '{ printf "%.2f", $1 / (1024 * 1024) }') +mem_used_gb=$(echo "$mem_used_kb" | awk '{ printf "%.2f", $1 / (1024 * 1024) }') + +# Disk Information (macOS) +root_partition="/" +disk_info=$(df -g "$root_partition" | awk 'NR==2 {print $2, $3}') +root_total_gb=$(echo "$disk_info" | awk '{printf "%.2f", $1}') +root_used_gb=$(echo "$disk_info" | awk '{printf "%.2f", $2}') +disk_percent=$(awk -v used="$root_used_gb" -v total="$root_total_gb" 'BEGIN { printf "%.2f", (used / total) * 100 }') + +# Last login and Uptime +last_login=$(last -1 "$USER" | head -n 1) +last_login_ip=$(echo "$last_login" | awk '{print $3}') + +# Check if last_login_ip is an IP address +if [[ "$last_login_ip" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + last_login_ip_present=1 + last_login_time=$(echo "$last_login" | awk '{print $4, $5, $6, $7}') +else + last_login_time=$(echo "$last_login" | awk '{print $4, $5, $6, $7}') +fi + +# macOS uptime formatting +boot_time=$(sysctl -n kern.boottime | awk '{print $4}' | tr -d ',') +current_time=$(date +%s) +uptime_seconds=$((current_time - boot_time)) +uptime_days=$((uptime_seconds / 86400)) +uptime_hours=$(( (uptime_seconds % 86400) / 3600 )) +uptime_minutes=$(( (uptime_seconds % 3600) / 60 )) + +sys_uptime="" +if [ $uptime_days -gt 0 ]; then + sys_uptime="${uptime_days}d " +fi +if [ $uptime_hours -gt 0 ]; then + sys_uptime="${sys_uptime}${uptime_hours}h " +fi +if [ $uptime_minutes -gt 0 ]; then + sys_uptime="${sys_uptime}${uptime_minutes}m" +fi +sys_uptime=$(echo "$sys_uptime" | sed 's/ $//') + +# Set current length before graphs get calculated +set_current_len + +# Create graphs +cpu_1min_bar_graph=$(bar_graph "$load_avg_1min" "$cpu_cores") +cpu_5min_bar_graph=$(bar_graph "$load_avg_5min" "$cpu_cores") +cpu_15min_bar_graph=$(bar_graph "$load_avg_15min" "$cpu_cores") + +mem_bar_graph=$(bar_graph "$mem_used_kb" "$mem_total_kb") + +disk_bar_graph=$(bar_graph "$root_used_gb" "$root_total_gb") + +# Machine Report +PRINT_HEADER +PRINT_CENTERED_DATA "$report_title" +PRINT_CENTERED_DATA "TR-100 MACHINE REPORT" +PRINT_DIVIDER "top" +PRINT_DATA "OS" "$os_name" +PRINT_DATA "KERNEL" "$os_kernel" +PRINT_DIVIDER +PRINT_DATA "HOSTNAME" "$net_hostname" +PRINT_DATA "MACHINE IP" "$net_machine_ip" +PRINT_DATA "CLIENT IP" "$net_client_ip" + +for dns_num in "${!net_dns_ip[@]}"; do + PRINT_DATA "DNS IP $(($dns_num + 1))" "${net_dns_ip[dns_num]}" +done + +PRINT_DATA "USER" "$net_current_user" +PRINT_DIVIDER +PRINT_DATA "PROCESSOR" "$cpu_model" +PRINT_DATA "CORES" "$cpu_cores_per_socket vCPU(s) / $cpu_sockets Socket(s)" +PRINT_DATA "HYPERVISOR" "$cpu_hypervisor" +PRINT_DATA "CPU FREQ" "$cpu_freq GHz" +PRINT_DATA "LOAD 1m" "$cpu_1min_bar_graph" +PRINT_DATA "LOAD 5m" "$cpu_5min_bar_graph" +PRINT_DATA "LOAD 15m" "$cpu_15min_bar_graph" + +PRINT_DIVIDER +PRINT_DATA "VOLUME" "$root_used_gb/$root_total_gb GB [$disk_percent%]" +PRINT_DATA "DISK USAGE" "$disk_bar_graph" + +PRINT_DIVIDER +PRINT_DATA "MEMORY" "${mem_used_gb}/${mem_total_gb} GiB [${mem_percent}%]" +PRINT_DATA "USAGE" "${mem_bar_graph}" +PRINT_DIVIDER +PRINT_DATA "LAST LOGIN" "$last_login_time" + +if [ $last_login_ip_present -eq 1 ]; then + PRINT_DATA "" "$last_login_ip" +fi + +PRINT_DATA "UPTIME" "$sys_uptime" +PRINT_DIVIDER "bottom"