Last active 11 minutes ago

diskreport.sh Raw
1#!/bin/bash
2
3if [ "$EUID" -ne 0 ]; then
4 echo "Error: This script must be run as root (e.g., sudo $0)" >&2
5 exit 1
6fi
7
8if ! command -v smartctl >/dev/null 2>&1; then
9 echo "Error: smartctl is not installed." >&2
10 exit 1
11fi
12
13OUTPUT="${1:-diskreport.html}"
14
15escape_html() {
16 sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g'
17}
18
19cat << 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>
151EOF
152
153for 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>
306EOF
307 fi
308done
309
310cat << 'EOF' >> "$OUTPUT"
311</div>
312</body>
313</html>
314EOF
315
316echo "DiskReport generated: $(pwd)/$OUTPUT"
317