albert revised this gist 2 months ago. Go to revision
No changes
albert revised this gist 2 months ago. Go to revision
No changes
albert revised this gist 2 months ago. Go to revision
No changes
albert revised this gist 2 months ago. Go to revision
1 file changed, 316 insertions
diskreport.sh(file created)
| @@ -0,0 +1,316 @@ | |||
| 1 | + | #!/bin/bash | |
| 2 | + | ||
| 3 | + | if [ "$EUID" -ne 0 ]; then | |
| 4 | + | echo "Error: This script must be run as root (e.g., sudo $0)" >&2 | |
| 5 | + | exit 1 | |
| 6 | + | fi | |
| 7 | + | ||
| 8 | + | if ! command -v smartctl >/dev/null 2>&1; then | |
| 9 | + | echo "Error: smartctl is not installed." >&2 | |
| 10 | + | exit 1 | |
| 11 | + | fi | |
| 12 | + | ||
| 13 | + | OUTPUT="${1:-diskreport.html}" | |
| 14 | + | ||
| 15 | + | escape_html() { | |
| 16 | + | sed 's/&/\&/g; s/</\</g; s/>/\>/g' | |
| 17 | + | } | |
| 18 | + | ||
| 19 | + | cat << EOF > "$OUTPUT" | |
| 20 | + | <!DOCTYPE html> | |
| 21 | + | <html lang="en"> | |
| 22 | + | <head> | |
| 23 | + | <meta charset="UTF-8"> | |
| 24 | + | <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| 25 | + | <title>DiskReport</title> | |
| 26 | + | <style> | |
| 27 | + | :root { | |
| 28 | + | --bg-color: #0f0f0f; | |
| 29 | + | --box-bg: #1a1a1a; | |
| 30 | + | --raw-bg: #0a0a0a; | |
| 31 | + | --text-color: #d3d7cf; | |
| 32 | + | --pass-color: #8ae234; | |
| 33 | + | --fail-color: #ef2929; | |
| 34 | + | --warn-color: #fce94f; | |
| 35 | + | --link-color: #729fcf; | |
| 36 | + | --border-color: #333333; | |
| 37 | + | --muted-color: #888a85; | |
| 38 | + | } | |
| 39 | + | ||
| 40 | + | * { | |
| 41 | + | scrollbar-width: thin; | |
| 42 | + | scrollbar-color: var(--border-color) var(--bg-color); | |
| 43 | + | } | |
| 44 | + | ||
| 45 | + | ::-webkit-scrollbar { | |
| 46 | + | width: 10px; | |
| 47 | + | height: 10px; | |
| 48 | + | } | |
| 49 | + | ::-webkit-scrollbar-track { | |
| 50 | + | background: var(--bg-color); | |
| 51 | + | } | |
| 52 | + | ::-webkit-scrollbar-thumb { | |
| 53 | + | background: var(--border-color); | |
| 54 | + | border-radius: 5px; | |
| 55 | + | } | |
| 56 | + | ::-webkit-scrollbar-thumb:hover { | |
| 57 | + | background: var(--muted-color); | |
| 58 | + | } | |
| 59 | + | ||
| 60 | + | html, body { | |
| 61 | + | min-height: 100vh; | |
| 62 | + | margin: 0; | |
| 63 | + | padding: 0; | |
| 64 | + | background-color: var(--bg-color); | |
| 65 | + | color: var(--text-color); | |
| 66 | + | font-family: ui-monospace, "Cascadia Code", "Source Code Pro", Menlo, Consolas, "Courier New", monospace; | |
| 67 | + | font-size: 16px; | |
| 68 | + | line-height: 1.5; | |
| 69 | + | } | |
| 70 | + | body { | |
| 71 | + | display: flex; | |
| 72 | + | justify-content: center; | |
| 73 | + | align-items: flex-start; | |
| 74 | + | padding: 2rem; | |
| 75 | + | box-sizing: border-box; | |
| 76 | + | } | |
| 77 | + | .container { | |
| 78 | + | margin: auto; | |
| 79 | + | width: 100%; | |
| 80 | + | max-width: 800px; | |
| 81 | + | } | |
| 82 | + | .header-box { | |
| 83 | + | border: 2px solid var(--pass-color); | |
| 84 | + | text-align: center; | |
| 85 | + | padding: 1.5rem; | |
| 86 | + | border-radius: 8px; | |
| 87 | + | margin-bottom: 2rem; | |
| 88 | + | background: var(--box-bg); | |
| 89 | + | box-shadow: 0 4px 6px rgba(0,0,0,0.3); | |
| 90 | + | } | |
| 91 | + | .header-title { | |
| 92 | + | color: var(--pass-color); | |
| 93 | + | margin: 0 0 0.5rem 0; | |
| 94 | + | font-size: 2rem; | |
| 95 | + | letter-spacing: 2px; | |
| 96 | + | text-transform: uppercase; | |
| 97 | + | } | |
| 98 | + | .header-date { | |
| 99 | + | color: var(--muted-color); | |
| 100 | + | font-size: 0.9rem; | |
| 101 | + | } | |
| 102 | + | .drive-box { | |
| 103 | + | margin-bottom: 2rem; | |
| 104 | + | background: var(--box-bg); | |
| 105 | + | padding: 1.5rem; | |
| 106 | + | border-radius: 8px; | |
| 107 | + | border: 1px solid var(--border-color); | |
| 108 | + | box-shadow: 0 4px 6px rgba(0,0,0,0.3); | |
| 109 | + | } | |
| 110 | + | .pass { color: var(--pass-color); font-weight: bold; } | |
| 111 | + | .fail { color: var(--fail-color); font-weight: bold; } | |
| 112 | + | .warn { color: var(--warn-color); font-weight: bold; } | |
| 113 | + | pre { | |
| 114 | + | margin: 0; | |
| 115 | + | white-space: pre-wrap; | |
| 116 | + | word-wrap: break-word; | |
| 117 | + | } | |
| 118 | + | summary { | |
| 119 | + | cursor: pointer; | |
| 120 | + | color: var(--link-color); | |
| 121 | + | margin-top: 1.5rem; | |
| 122 | + | outline: none; | |
| 123 | + | font-weight: bold; | |
| 124 | + | user-select: none; | |
| 125 | + | transition: color 0.2s ease; | |
| 126 | + | } | |
| 127 | + | summary:hover { color: #ffffff; } | |
| 128 | + | .raw-data { | |
| 129 | + | color: var(--muted-color); | |
| 130 | + | font-size: 14px; | |
| 131 | + | margin-top: 1rem; | |
| 132 | + | padding: 1rem; | |
| 133 | + | background: var(--raw-bg); | |
| 134 | + | border-left: 3px solid var(--border-color); | |
| 135 | + | overflow-x: auto; | |
| 136 | + | white-space: pre; | |
| 137 | + | } | |
| 138 | + | ||
| 139 | + | .raw-data::-webkit-scrollbar-track { | |
| 140 | + | background: var(--raw-bg); | |
| 141 | + | } | |
| 142 | + | </style> | |
| 143 | + | </head> | |
| 144 | + | <body> | |
| 145 | + | <div class="container"> | |
| 146 | + | ||
| 147 | + | <div class="header-box"> | |
| 148 | + | <h1 class="header-title">DiskReport</h1> | |
| 149 | + | <div class="header-date">GENERATED: $(date)</div> | |
| 150 | + | </div> | |
| 151 | + | EOF | |
| 152 | + | ||
| 153 | + | for disk in $(lsblk -nd -o NAME,TYPE | awk '$2=="disk" {print $1}'); do | |
| 154 | + | dev="/dev/$disk" | |
| 155 | + | ||
| 156 | + | if smartctl -i "$dev" >/dev/null 2>&1; then | |
| 157 | + | raw_output=$(smartctl -a "$dev") | |
| 158 | + | ||
| 159 | + | model=$(echo "$raw_output" | grep -iE "^Device Model:|^Product:|^Model Number:|^Vendor:" | head -n 1 | awk -F: '{print $2}' | xargs) | |
| 160 | + | if [ -z "$model" ]; then | |
| 161 | + | model=$(lsblk -n -d -o MODEL "$dev" | xargs) | |
| 162 | + | fi | |
| 163 | + | ||
| 164 | + | interface=$(lsblk -n -d -o TRAN "$dev" | tr 'a-z' 'A-Z' | xargs) | |
| 165 | + | if [ -z "$interface" ] || [ "$interface" = "UNKNOWN" ]; then | |
| 166 | + | if echo "$raw_output" | grep -qi "Transport protocol:"; then | |
| 167 | + | interface=$(echo "$raw_output" | grep -i "Transport protocol:" | awk -F: '{print $2}' | xargs | cut -d' ' -f1) | |
| 168 | + | elif echo "$raw_output" | grep -qi "Protocol:.*NVM Express"; then | |
| 169 | + | interface="NVMe" | |
| 170 | + | elif echo "$raw_output" | grep -qi "SATA Version is:"; then | |
| 171 | + | interface=$(echo "$raw_output" | grep -i "SATA Version is:" | awk -F: '{print $2}' | xargs | cut -d, -f1) | |
| 172 | + | elif echo "$raw_output" | grep -qi "SCSI"; then | |
| 173 | + | interface="SCSI" | |
| 174 | + | else | |
| 175 | + | interface="Unknown" | |
| 176 | + | fi | |
| 177 | + | fi | |
| 178 | + | ||
| 179 | + | health=$(echo "$raw_output" | grep -iE "test result:|Health Status:" | awk -F: '{print $2}' | xargs) | |
| 180 | + | if [[ "$health" == *"PASSED"* || "$health" == *"OK"* ]]; then | |
| 181 | + | health_fmt="<span class='pass'>OK</span>" | |
| 182 | + | else | |
| 183 | + | health_fmt="<span class='fail'>${health:-FAIL/UNKNOWN}</span>" | |
| 184 | + | fi | |
| 185 | + | ||
| 186 | + | health_pct="" | |
| 187 | + | pct_used=$(echo "$raw_output" | grep -i "^Percentage Used:" | awk '{print $3}' | tr -d '%') | |
| 188 | + | if [ -z "$pct_used" ]; then | |
| 189 | + | pct_used=$(echo "$raw_output" | grep -i "Percentage used endurance indicator:" | awk '{print $5}' | tr -d '%') | |
| 190 | + | fi | |
| 191 | + | ||
| 192 | + | if [ -n "$pct_used" ] && [[ "$pct_used" =~ ^[0-9]+$ ]]; then | |
| 193 | + | health_pct=$((100 - pct_used)) | |
| 194 | + | else | |
| 195 | + | wearout=$(echo "$raw_output" | grep -iE "^[ ]*(231|233|202|177|169)[ ]" | head -n 1 | awk '{print $4}') | |
| 196 | + | if [ -n "$wearout" ] && [[ "$wearout" =~ ^[0-9]+$ ]]; then | |
| 197 | + | health_pct=$((10#$wearout)) | |
| 198 | + | fi | |
| 199 | + | fi | |
| 200 | + | ||
| 201 | + | if [ -n "$health_pct" ]; then | |
| 202 | + | if [ "$health_pct" -ge 80 ]; then health_str="<span class='pass'>${health_pct}% (Excellent)</span>" | |
| 203 | + | elif [ "$health_pct" -ge 50 ]; then health_str="<span class='warn'>${health_pct}% (Good)</span>" | |
| 204 | + | elif [ "$health_pct" -ge 20 ]; then health_str="<span class='warn'>${health_pct}% (Fair)</span>" | |
| 205 | + | else health_str="<span class='fail'>${health_pct}% (Poor)</span>" | |
| 206 | + | fi | |
| 207 | + | else | |
| 208 | + | if [[ "$health" == *"PASSED"* || "$health" == *"OK"* ]]; then | |
| 209 | + | health_str="<span class='pass'>100% (Excellent)</span>" | |
| 210 | + | else | |
| 211 | + | health_str="<span class='fail'>0% (Failing)</span>" | |
| 212 | + | fi | |
| 213 | + | fi | |
| 214 | + | ||
| 215 | + | perf_pct=100 | |
| 216 | + | perf_reason="" | |
| 217 | + | ||
| 218 | + | crc_err=$(echo "$raw_output" | grep -iE "^[ ]*199[ ]+UDMA_CRC_Error_Count" | awk '{print $10}') | |
| 219 | + | if [[ -n "$crc_err" && "$crc_err" -gt 0 ]]; then | |
| 220 | + | perf_pct=80 | |
| 221 | + | perf_reason=" (Degraded - Cable/Interface Errors)" | |
| 222 | + | fi | |
| 223 | + | ||
| 224 | + | nvme_err=$(echo "$raw_output" | grep -i "^Media and Data Integrity Errors:" | awk '{print $6}') | |
| 225 | + | if [[ -n "$nvme_err" && "$nvme_err" -gt 0 ]]; then | |
| 226 | + | perf_pct=70 | |
| 227 | + | perf_reason=" (Degraded - Integrity Errors)" | |
| 228 | + | fi | |
| 229 | + | ||
| 230 | + | scsi_defects=$(echo "$raw_output" | grep -i "Elements in grown defect list:" | awk -F: '{print $2}' | xargs) | |
| 231 | + | if [[ -n "$scsi_defects" && "$scsi_defects" -gt 0 ]]; then | |
| 232 | + | perf_pct=70 | |
| 233 | + | perf_reason=" (Degraded - Grown Defects)" | |
| 234 | + | fi | |
| 235 | + | ||
| 236 | + | realloc=$(echo "$raw_output" | grep -iE "^[ ]*5[ ]+Reallocated_Sector_Ct" | awk '{print $10}') | |
| 237 | + | if [[ -n "$realloc" && "$realloc" -gt 0 ]]; then | |
| 238 | + | if [ "$perf_pct" -gt 80 ]; then perf_pct=80; fi | |
| 239 | + | perf_reason=" (Degraded - Reallocated Sectors)" | |
| 240 | + | fi | |
| 241 | + | ||
| 242 | + | if [ "$perf_pct" -eq 100 ]; then | |
| 243 | + | perf_str="<span class='pass'>100% (Excellent)</span>" | |
| 244 | + | elif [ "$perf_pct" -ge 80 ]; then | |
| 245 | + | perf_str="<span class='warn'>${perf_pct}%${perf_reason}</span>" | |
| 246 | + | else | |
| 247 | + | perf_str="<span class='fail'>${perf_pct}%${perf_reason}</span>" | |
| 248 | + | fi | |
| 249 | + | ||
| 250 | + | temp=$(echo "$raw_output" | grep -iE "^Temperature:|^Current Drive Temperature:" | head -n 1 | awk -F: '{print $2}' | xargs) | |
| 251 | + | ||
| 252 | + | pwr_raw=$(echo "$raw_output" | grep -iE "Power On Hours|Accumulated power on time|number of hours powered up") | |
| 253 | + | if echo "$pwr_raw" | grep -q "minutes"; then | |
| 254 | + | pwr=$(echo "$pwr_raw" | awk -F'hours:minutes ' '{print $2}' | cut -d: -f1) | |
| 255 | + | pwr="${pwr} Hours" | |
| 256 | + | elif echo "$pwr_raw" | grep -qi "number of hours powered up"; then | |
| 257 | + | pwr=$(echo "$pwr_raw" | awk -F= '{print $2}' | awk '{print $1}') | |
| 258 | + | pwr="${pwr} Hours" | |
| 259 | + | elif [ -n "$pwr_raw" ]; then | |
| 260 | + | pwr=$(echo "$pwr_raw" | awk -F: '{print $2}' | xargs | sed 's/,//g') | |
| 261 | + | pwr="${pwr} Hours" | |
| 262 | + | else | |
| 263 | + | pwr="N/A" | |
| 264 | + | fi | |
| 265 | + | ||
| 266 | + | written="N/A" | |
| 267 | + | if echo "$raw_output" | grep -q "Data Units Written:"; then | |
| 268 | + | written=$(echo "$raw_output" | grep "Data Units Written:" | awk -F'[][]' '{print $2}') | |
| 269 | + | elif echo "$raw_output" | grep -qi "gigabytes processed \[consume\]:"; then | |
| 270 | + | gb_written=$(echo "$raw_output" | grep -i "gigabytes processed \[consume\]:" | awk '{print $4}') | |
| 271 | + | written=$(awk -v gb="$gb_written" 'BEGIN { printf "%.2f TB", gb/1000 }') | |
| 272 | + | elif echo "$raw_output" | grep -qiE "^[ ]*241[ ]+Total_LBAs_Written"; then | |
| 273 | + | lbas=$(echo "$raw_output" | grep -iE "^[ ]*241[ ]+Total_LBAs_Written" | awk '{print $10}') | |
| 274 | + | written=$(awk -v lbas="$lbas" 'BEGIN { printf "%.2f TB", (lbas * 512) / 1000000000000 }') | |
| 275 | + | fi | |
| 276 | + | ||
| 277 | + | safe_dev=$(echo "$dev" | escape_html) | |
| 278 | + | safe_model=$(echo "${model:-Unknown}" | escape_html) | |
| 279 | + | safe_interface=$(echo "${interface:-Unknown}" | escape_html) | |
| 280 | + | safe_temp=$(echo "${temp:-N/A}" | escape_html) | |
| 281 | + | safe_pwr=$(echo "${pwr:-N/A}" | escape_html) | |
| 282 | + | safe_written=$(echo "${written:-N/A}" | escape_html) | |
| 283 | + | safe_raw=$(echo "$raw_output" | grep -v "Copyright" | grep -v "smartmontools.org" | escape_html) | |
| 284 | + | ||
| 285 | + | cat << EOF >> "$OUTPUT" | |
| 286 | + | <div class='drive-box'> | |
| 287 | + | <pre> | |
| 288 | + | ========================================================== | |
| 289 | + | DRIVE : $safe_dev | |
| 290 | + | MODEL : $safe_model | |
| 291 | + | INTERFACE : $safe_interface | |
| 292 | + | ---------------------------------------------------------- | |
| 293 | + | HEALTH : $health_str | |
| 294 | + | PERFORMANCE : $perf_str | |
| 295 | + | STATUS : $health_fmt | |
| 296 | + | TEMP : $safe_temp | |
| 297 | + | POWER ON : $safe_pwr | |
| 298 | + | WRITTEN : $safe_written | |
| 299 | + | ========================================================== | |
| 300 | + | </pre> | |
| 301 | + | <details> | |
| 302 | + | <summary>raw</summary> | |
| 303 | + | <pre class='raw-data'>$safe_raw</pre> | |
| 304 | + | </details> | |
| 305 | + | </div> | |
| 306 | + | EOF | |
| 307 | + | fi | |
| 308 | + | done | |
| 309 | + | ||
| 310 | + | cat << 'EOF' >> "$OUTPUT" | |
| 311 | + | </div> | |
| 312 | + | </body> | |
| 313 | + | </html> | |
| 314 | + | EOF | |
| 315 | + | ||
| 316 | + | echo "DiskReport generated: $(pwd)/$OUTPUT" | |
Newer
Older