├── .gitignore ├── README.md ├── config.sample.sh ├── installpkgs.sh ├── samples └── minotaur-status.png ├── scripts ├── apcstats.sh ├── arcstats.sh ├── cpuinfo.sh ├── disktemp.sh ├── nvmetemp.sh └── smartstats.sh └── status.sh /.gitignore: -------------------------------------------------------------------------------- 1 | config.sh 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Proxmox-Status 2 | A bash script to gather system metrics like temperatures and ARC utilization that are missing from the Proxmox UI. 3 | 4 | ## Sample 5 | ![](samples/minotaur-status.png) 6 | 7 | ## Setup 8 | 1. The install script adds 3 packages and expects you to be on Proxmox, a Debian-based system. It adds `screenfetch` `lm-sensors` and `smartmontools`. **If you are okay with this**, run `./installpkgs.sh` and follow the instructions printed at the end of the script 9 | 2. Copy `config.sample.sh` to `config.sh` 10 | 3. Edit `config.sh` and replace the example variables with your system's disk and CPU names 11 | 3.1 **Configure Disks** - For ZFS users, you should be able to run `sudo zfs status` and see the name for each disk in your zpool that refers to its `/dev/disk/by-id/` path. You can leave the disk arrays empty or omit them entirely if you don't have mechanical drives or Optane drives, for example 12 | 3.2 **Configure CPU** - If you installed `lm-sensors` in step 1, you'll be able to run `sensors` and find the name of your CPU. My Ryzen 3700X is shown in a comment in the sample config as `k10temp-pci-00c3` and the `cpu_temp_awk_print_fmt` is a format string which picks the second word in the example. It runs the format string on the line with the matching `cpu_temp_field_label`, `Tdie` for the temperature of the CPU die in my case. 13 | 3.3 **Configure Screenfetch** - If you did not install `screenfetch` or do not want to see it at the top of the status output, set `show_screenfetch` to false 14 | 4. There are some aspects of the status which are implicitly configured. 15 | 4.1 **APC UPS Users** - If you are on an APC brand UPS and have `apcupsd` installed with a USB serial connection to the UPS, it can read the status of you battery and show current draw in Watts and as a percentage of its VA rating. It will also show time remaining in the event of a power outage. 16 | 4.2 **ZFS Users** - `arcstats.sh` determines if you have an `arcstats` file on your system, and whether you have a SLOG or L2ARC, then shows additional details for them if possible. 17 | 5. Test by running `./status.sh` and checking that the output shows all of your disks, CPU temp, and ARC stats correctly 18 | -------------------------------------------------------------------------------- /config.sample.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | harddisks=( "ata-WDC_WD40EFRX-68N32N0_WD-1" 4 | "ata-WDC_WD40EFRX-68N32N0_WD-S" 5 | "ata-WDC_WD40EFRX-68N32N0_WD-1" 6 | "ata-WDC_WD40EFRX-68N32N0_WD-5" 7 | "ata-WDC_WD40EFZX-68AWUN0_WD-9" 8 | "ata-WDC_WD40EFZX-68AWUN0_WD-J" 9 | "ata-WDC_WD40EFZX-68AWUN0_WD-7" ) 10 | 11 | ssds=( "ata-Samsung_SSD_860_EVO_500GB_T" 12 | "ata-Samsung_SSD_860_EVO_500GB_R" 13 | "ata-Samsung_SSD_860_EVO_500GB_K" 14 | "ata-Samsung_SSD_860_PRO_256GB_X" 15 | "ata-Samsung_SSD_860_PRO_256GB_W" ) 16 | 17 | nvmedrives=( "nvme-SAMSUNG_MZVLB256HBHQ-0") 18 | 19 | optanedrives=( "nvme-INTEL_SSDPEK1A118GA_B" ) 20 | 21 | 22 | 23 | # sample output from sensors 24 | # k10temp-pci-00c3 25 | # Adapter: PCI adapter 26 | # Tctl: +53.5°C 27 | # Tdie: +53.5°C 28 | # Tccd1: +42.8°C 29 | 30 | cpu_temp_device="k10temp-pci-00c3" 31 | cpu_temp_field_label="Tdie" 32 | cpu_temp_awk_print_fmt="2" 33 | 34 | show_screenfetch=true 35 | -------------------------------------------------------------------------------- /installpkgs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | sudo apt update 4 | sudo apt install screenfetch lm-sensors smartmontools 5 | 6 | echo 7 | 8 | echo "If you haven't already, please run 'sudo sensors-detect' to configure temperature sensors" \ 9 | "for your CPU and other devices which detect temperature like NVMe drives." 10 | 11 | -------------------------------------------------------------------------------- /samples/minotaur-status.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greenec/Proxmox-Status/3fb44bffc43041f131249d2d30b1dfe091cd6fb4/samples/minotaur-status.png -------------------------------------------------------------------------------- /scripts/apcstats.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # abort if apcaccess is not installed 4 | if [ ! -f /sbin/apcaccess ]; then 5 | exit 6 | fi 7 | 8 | ups_info=$(/sbin/apcaccess) 9 | 10 | nominal_power=$(awk '{ if ($1 == "NOMPOWER") { print $3 } }' <<< "$ups_info") 11 | power_unit=$(awk '{ if ($1 == "NOMPOWER") { print $4 } }' <<< "$ups_info") 12 | load_percent=$(awk '{ if ($1 == "LOADPCT") { print $3 } }' <<< "$ups_info") 13 | time_left=$(awk '{ if ($1 == "TIMELEFT") { print $3 " " $4 } }' <<< "$ups_info") 14 | 15 | power=$(bc <<< "scale=2; $nominal_power * $load_percent / 100.0") 16 | 17 | column -t -s '|' <<< "$( 18 | echo "APC UPS Draw:|$power $power_unit ($load_percent %)" 19 | echo "Time Remaining:|$time_left" 20 | )" 21 | -------------------------------------------------------------------------------- /scripts/arcstats.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | arcstats_file="/proc/spl/kstat/zfs/arcstats" 4 | zilstats_file="/proc/spl/kstat/zfs/zil" 5 | 6 | if [ ! -f "$arcstats_file" ]; then 7 | exit 1 8 | fi 9 | 10 | print_raw=false 11 | if [[ $1 == "--print-raw" ]]; then 12 | print_raw=true 13 | fi 14 | 15 | numfmt_bytes="numfmt --to=iec-i --suffix=B --format=%0.1f" 16 | if [ "$print_raw" = true ]; then 17 | numfmt_bytes="tee /dev/null" 18 | fi 19 | 20 | # add a space between the number and unit, e.g. 53GiB -> 53 GiB 21 | iec_space_regexp="s/([0-9])([A-Z])/\1 \2/" 22 | 23 | stat_name_suffix=":" 24 | if [ "$print_raw" = true ]; then 25 | stat_name_suffix="" 26 | fi 27 | 28 | 29 | # get current and max ARC size 30 | arc_size=$(awk '{ if ($1 == "size") print $3 }' "$arcstats_file" | $numfmt_bytes | sed -E "$iec_space_regexp") 31 | max_arc_size=$(awk '{ if ($1 == "c_max") print $3 }' "$arcstats_file" | $numfmt_bytes | sed -E "$iec_space_regexp") 32 | 33 | # calculate ARC hit ratio 34 | hits=$(awk '{ if ($1 == "hits") print $3 }' "$arcstats_file") 35 | misses=$(awk '{ if ($1 == "misses") print $3 }' "$arcstats_file") 36 | total_arc_requests=$(( hits + misses )) 37 | 38 | # exit if ARC cache has not been used 39 | if [ "$total_arc_requests" -eq 0 ]; then 40 | exit 1 41 | fi 42 | 43 | hit_ratio=$( bc <<< "scale=2; $hits * 100 / $total_arc_requests" ) 44 | 45 | mfu_size=$(awk '{ if ($1 == "mfu_size") print $3 }' "$arcstats_file" | $numfmt_bytes | sed -E "$iec_space_regexp") 46 | mru_size=$(awk '{ if ($1 == "mru_size") print $3 }' "$arcstats_file" | $numfmt_bytes | sed -E "$iec_space_regexp") 47 | metadata_cache_size=$(awk '{ if ($1 == "arc_meta_used") print $3 }' "$arcstats_file" | $numfmt_bytes | sed -E "$iec_space_regexp") 48 | dnode_cache_size=$(awk '{ if ($1 == "dnode_size") print $3 }' "$arcstats_file" | $numfmt_bytes | sed -E "$iec_space_regexp") 49 | 50 | arc_utilization=$( 51 | printf "|ARC Size%s|%s|%s" "$stat_name_suffix" "$arc_size" "$max_arc_size" 52 | if [ "$print_raw" = false ]; then 53 | printf " (Max)" 54 | fi 55 | printf "\n" 56 | 57 | printf "|Hit Ratio%s|%s" "$stat_name_suffix" "$hit_ratio" 58 | if [ "$print_raw" = false ]; then 59 | printf " %%" 60 | else 61 | printf "\n|ARC Hits|%s" "$hits" 62 | printf "\n|ARC Misses|%s" "$misses" 63 | fi 64 | printf "\n" 65 | 66 | printf "|MFU Size%s|%s\n" "$stat_name_suffix" "$mfu_size" 67 | printf "|MRU Size%s|%s\n" "$stat_name_suffix" "$mru_size" 68 | printf "|Metadata Cache Size%s|%s\n" "$stat_name_suffix" "$metadata_cache_size" 69 | printf "|Dnode Cache Size%s|%s\n" "$stat_name_suffix" "$dnode_cache_size" 70 | ) 71 | 72 | 73 | # get the size and number of transactions written to the SLOG pool 74 | slog_transaction_count=$(awk '{ if ($1 == "zil_itx_metaslab_slog_count") print $3 }' "$zilstats_file") 75 | slog_transaction_bytes=$(awk '{ if ($1 == "zil_itx_metaslab_slog_bytes") print $3 }' "$zilstats_file") 76 | 77 | slog_transaction_size=$($numfmt_bytes <<< "$slog_transaction_bytes" | sed -E "$iec_space_regexp") 78 | 79 | # calculate transactions and bytes per second 80 | uptime=$(awk '{ print $1 }' /proc/uptime) 81 | slog_tps=$( bc <<< "scale=1; $slog_transaction_count / $uptime" ) 82 | slog_bytes_per_sec=$( bc <<< "scale=2; $slog_transaction_bytes / $uptime" | $numfmt_bytes | sed -E "$iec_space_regexp" ) 83 | 84 | zil_utilization=$( 85 | printf "|ZIL SLOG Transactions%s|%s\n" "$stat_name_suffix" "$slog_transaction_size" 86 | 87 | if [ "$print_raw" = true ]; then 88 | printf "|ZIL SLOG Transaction Count|%s\n" "$slog_transaction_count" 89 | fi 90 | 91 | printf "|ZIL SLOG TPS%s|%s" "$stat_name_suffix" "$slog_tps" 92 | if [ "$print_raw" = false ]; then 93 | printf " itx/sec" 94 | fi 95 | printf "\n" 96 | 97 | printf "|ZIL SLOG Writes%s|%s" "$stat_name_suffix" "$slog_bytes_per_sec" 98 | if [ "$print_raw" = false ]; then 99 | printf "/sec" 100 | fi 101 | printf "\n" 102 | ) 103 | 104 | 105 | # get the size and hit ratio of the L2ARC 106 | l2arc_bytes=$(awk '{ if ($1 == "l2_size") print $3 }' "$arcstats_file") 107 | if [ "$l2arc_bytes" -ne 0 ]; then 108 | l2arc_size=$($numfmt_bytes <<< "$l2arc_bytes" | sed -E "$iec_space_regexp") 109 | l2arc_size_compressed=$(awk '{ if ($1 == "l2_asize") print $3 }' "$arcstats_file" | $numfmt_bytes | sed -E "$iec_space_regexp") 110 | 111 | l2_header_size=$(awk '{ if ($1 == "l2_hdr_size") print $3 }' "$arcstats_file" | $numfmt_bytes | sed -E "$iec_space_regexp") 112 | 113 | # calculate L2ARC hit ratio 114 | l2_hits=$(awk '{ if ($1 == "l2_hits") print $3 }' "$arcstats_file") 115 | l2_misses=$(awk '{ if ($1 == "l2_misses") print $3 }' "$arcstats_file") 116 | total_l2_arc_requests=$(( l2_hits + l2_misses )) 117 | l2arc_hit_ratio=$( bc <<< "scale=2; $l2_hits * 100 / $total_l2_arc_requests" ) 118 | 119 | l2_read_bytes=$(awk '{ if ($1 == "l2_read_bytes") print $3 }' "$arcstats_file") 120 | l2_write_bytes=$(awk '{ if ($1 == "l2_write_bytes") print $3 }' "$arcstats_file") 121 | 122 | l2arc_stats=$( 123 | printf "|L2ARC Size%s|%s\n" "$stat_name_suffix" "$l2arc_size" 124 | printf "|L2ARC Size (compressed)%s|%s\n" "$stat_name_suffix" "$l2arc_size_compressed" 125 | 126 | printf "|L2ARC Hit Ratio%s|%s" "$stat_name_suffix" "$l2arc_hit_ratio" 127 | if [ "$print_raw" = false ]; then 128 | printf " %%" 129 | fi 130 | 131 | printf "\n|L2ARC Header Size%s|%s" "$stat_name_suffix" "$l2_header_size" 132 | 133 | if [ "$print_raw" = true ]; then 134 | printf "\n|L2ARC Hits|%s" "$l2_hits" 135 | printf "\n|L2ARC Misses|%s" "$l2_misses" 136 | printf "\n|L2ARC Read Bytes|%s" "$l2_read_bytes" 137 | printf "\n|L2ARC Write Bytes|%s" "$l2_write_bytes" 138 | fi 139 | printf "\n" 140 | ) 141 | fi 142 | 143 | output=$( 144 | printf "ARC Stats:\n%s\n" "$arc_utilization" 145 | 146 | if [ "$slog_transaction_count" != "0" ]; then 147 | printf "ZIL Stats%s\n%s\n" "$stat_name_suffix" "$zil_utilization" 148 | fi 149 | 150 | if [ -n "$l2arc_stats" ]; then 151 | printf "L2ARC Stats%s\n%s\n" "$stat_name_suffix" "$l2arc_stats" 152 | fi 153 | ) 154 | 155 | if [ "$print_raw" = true ]; then 156 | echo "$output" 157 | else 158 | # print final output in table format 159 | column -t -s '|' <<< "$output" 160 | fi 161 | -------------------------------------------------------------------------------- /scripts/cpuinfo.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | dir=$(dirname "$(realpath "$0")") 4 | source "$dir/../config.sh" 5 | 6 | # print cpu temperature 7 | temp=$(sensors "$cpu_temp_device" | awk -v field_label="$cpu_temp_field_label" -v print_fmt="$cpu_temp_awk_print_fmt" 'match($0, field_label) { print $print_fmt }') 8 | 9 | column -t -s '|' <<< "$( 10 | # print load average info 11 | awk '{ print( "Load average:|" $1 " (1m)\t" $2 " (5m)\t" $3 " (15m)" ) }' < /proc/loadavg 12 | 13 | printf "%s:|%s\n" "$cpu_temp_device" "$temp" 14 | )" 15 | 16 | -------------------------------------------------------------------------------- /scripts/disktemp.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | dir=$(dirname "$(realpath "$0")") 4 | source "$dir/../config.sh" 5 | 6 | if [ "$1" == "ssds" ]; then 7 | disks=("${ssds[@]}") 8 | else 9 | disks=("${harddisks[@]}") 10 | fi 11 | 12 | output=$( 13 | for disk in "${disks[@]}"; do 14 | realpath=$(realpath "/dev/disk/by-id/$disk") 15 | smartctl_cmd="/sbin/smartctl -a $realpath" 16 | 17 | if [ "$EUID" -ne 0 ]; then 18 | smartctl_cmd="sudo $smartctl_cmd" 19 | fi 20 | smartctl=$(eval "$smartctl_cmd") 21 | 22 | disktemp=$(awk '$2 ~ "Temperature" { print $10 }' <<< "$smartctl") 23 | diskname=$(sed -n 's/Device Model:\s*\(.*\)/\1/p' <<< "$smartctl") 24 | 25 | printf "%s:|%s:|%s\u00b0C:|[%s]\n" "$realpath" "$diskname" "$disktemp" "$disk" 26 | done 27 | ) 28 | 29 | # sort output by disk model, e.g. WDC WD40EFRX-68N32N0 30 | printf "%s" "$output" | sort -k 2 | column -t -s '|' 31 | 32 | -------------------------------------------------------------------------------- /scripts/nvmetemp.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | dir=$(dirname "$(realpath "$0")") 4 | source "$dir/../config.sh" 5 | 6 | if [ "$1" == "optane" ]; then 7 | disks=("${optanedrives[@]}") 8 | else 9 | disks=("${nvmedrives[@]}") 10 | fi 11 | 12 | 13 | output=$( 14 | for disk in "${disks[@]}"; do 15 | realpath=$(realpath "/dev/disk/by-id/$disk") 16 | smartctl_cmd="/sbin/smartctl -a $realpath" 17 | 18 | if [ "$EUID" -ne 0 ]; then 19 | smartctl_cmd="sudo $smartctl_cmd" 20 | fi 21 | 22 | smartctl=$(eval "$smartctl_cmd") 23 | 24 | disktemp=$(awk '{ if ($1 == "Temperature:") print $2 }' <<< "$smartctl") 25 | diskname=$(sed -n 's/Model Number:\s*\(.*\)/\1/p' <<< "$smartctl") 26 | 27 | printf "%s:|%s:|%s\u00b0C:|[%s]\n" "$realpath" "$diskname" "$disktemp" "$disk" 28 | done 29 | ) 30 | 31 | # sort output by disk model, e.g. Samsung SSD 970 EVO Plus 2TB 32 | printf "%s" "$output" | sort -k 2 | column -t -s '|' 33 | 34 | -------------------------------------------------------------------------------- /scripts/smartstats.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | CYAN='\033[0;36m' 4 | NC='\033[0m' 5 | 6 | dir=$(dirname "$(realpath "$0")") 7 | source "$dir/../config.sh" 8 | 9 | header_printed=false 10 | 11 | for disk in "${harddisks[@]}"; do 12 | smartctl_cmd="/sbin/smartctl -a /dev/disk/by-id/$disk" 13 | if [ "$EUID" -ne 0 ]; then 14 | smartctl_cmd="sudo $smartctl_cmd" 15 | fi 16 | smartctl=$(eval "$smartctl_cmd") 17 | 18 | if [ "$header_printed" = false ]; then 19 | echo "$smartctl" | grep 'ID#' 20 | header_printed=true 21 | fi 22 | 23 | printf "${CYAN}${disk}${NC}\n" 24 | echo "$smartctl" | grep 'Reallocated_Sector_Ct' 25 | echo "$smartctl" | grep 'Current_Pending_Sector' 26 | printf "\n" 27 | done 28 | 29 | -------------------------------------------------------------------------------- /status.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | CYAN='\033[0;36m' 4 | NC='\033[0m' 5 | 6 | # elevate to root before doing anything so the sudo prompt doesn't disrupt the script 7 | sudo echo 0 > /dev/null 8 | 9 | dir=$(dirname "$(realpath "$0")") 10 | source "$dir/config.sh" 11 | 12 | # measure CPU temp before running screenfetch so it's more accurate 13 | cpuinfo=$("$dir/scripts/cpuinfo.sh") 14 | 15 | if [[ "$show_screenfetch" = true ]]; then 16 | screenfetch 17 | printf "\n" 18 | fi 19 | 20 | printf "${CYAN}CPU Load / Temperature:${NC}\n" 21 | sed 's/^/\t/' <<< "$cpuinfo" 22 | 23 | apcstats=$("$dir/scripts/apcstats.sh") 24 | if [ -n "$apcstats" ]; then 25 | printf "\n${CYAN}UPS Stats:${NC}\n" 26 | sed 's/^/\t/' <<< "$apcstats" 27 | fi 28 | 29 | hddtemp=$("$dir/scripts/disktemp.sh" harddisks) 30 | if [ -n "$hddtemp" ]; then 31 | printf "\n${CYAN}Hard Drive Temperatures:${NC}\n" 32 | sed 's/^/\t/' <<< "$hddtemp" 33 | fi 34 | 35 | ssdtemp=$("$dir/scripts/disktemp.sh" ssds) 36 | if [ -n "$ssdtemp" ]; then 37 | printf "\n${CYAN}SSD Temperatures:${NC}\n" 38 | sed 's/^/\t/' <<< "$ssdtemp" 39 | fi 40 | 41 | nvmetemp=$("$dir/scripts/nvmetemp.sh") 42 | if [ -n "$nvmetemp" ]; then 43 | printf "\n${CYAN}NVMe SSD Temperatures:${NC}\n" 44 | sed 's/^/\t/' <<< "$nvmetemp" 45 | fi 46 | 47 | optanetemp=$("$dir/scripts/nvmetemp.sh" optane) 48 | if [ -n "$optanetemp" ]; then 49 | printf "\n${CYAN}Intel Optane SLOG Temperatures:${NC}\n" 50 | sed 's/^/\t/' <<< "$optanetemp" 51 | fi 52 | 53 | arcstats=$("$dir/scripts/arcstats.sh") 54 | if [ $? -eq 0 ] && [ -n "$arcstats" ]; then 55 | printf "\n${CYAN}ZFS Adaptive Read Cache Stats:${NC}\n" 56 | "$dir/scripts/arcstats.sh" | sed 's/^/\t/' 57 | fi 58 | --------------------------------------------------------------------------------