From 1327b8c6ef55cb97ac39cfed4a40733363693643 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Tichavsk=C3=BD?= Date: Wed, 19 Nov 2025 16:58:54 +0100 Subject: [PATCH] Upload files to "/" --- disk.py | 179 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 179 insertions(+) create mode 100644 disk.py diff --git a/disk.py b/disk.py new file mode 100644 index 0000000..1bcc5fd --- /dev/null +++ b/disk.py @@ -0,0 +1,179 @@ +import subprocess +import json +import sys +import math + +def run_command(cmd): + try: + result = subprocess.run(cmd, shell=True, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + return result.stdout.decode('utf-8') + except subprocess.CalledProcessError: + return None + +def get_size(bytes_size): + """Converts raw bytes to human readable string.""" + if not bytes_size: return "N/A" + size_name = ("B", "KB", "MB", "GB", "TB", "PB") + if bytes_size == 0: return "0 B" + i = int(math.floor(math.log(bytes_size, 1024))) + p = math.pow(1024, i) + s = round(bytes_size / p, 1) + return f"{s} {size_name[i]}" + +def get_smart_data(device_path): + # -a: All info, -j: JSON output + cmd = f"smartctl -a -j {device_path}" + result = subprocess.run(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + try: + return json.loads(result.stdout.decode('utf-8')) + except json.JSONDecodeError: + return None + +def parse_smart_data(data, device_node, rotation): + if not data: return None + + # --- 1. BASIC INFO --- + model = data.get('model_name', 'Unknown') + if model == 'Unknown': model = data.get('model_number', 'Unknown') + serial = data.get('serial_number', 'Unknown') + size_bytes = data.get('user_capacity', {}).get('bytes', 0) + + # Get Logical Sector Size for LBA calculations (default to 512) + sector_size = data.get('logical_block_size', 512) + + # --- 2. TYPE DETECTION --- + protocol = data.get('device', {}).get('protocol', '').lower() + disk_type = "SSD" + if rotation == '1': disk_type = "HDD" + elif 'nvme' in protocol: disk_type = "NVMe" + + # --- 3. HEALTH --- + passed = data.get('smart_status', {}).get('passed') + health = "OK" if passed else "FAIL" + if passed is None: health = "Unknown" + + # --- 4. METRICS --- + hours = "N/A" + spare = "N/A" + temp = "N/A" + errors = 0 + total_bytes_written = 0 + + # Temperature + temp_data = data.get('temperature', {}).get('current') + if temp_data: temp = f"{temp_data}C" + + if disk_type == "NVMe": + # --- NVMe LOGIC --- + nvme_log = data.get('nvme_smart_health_information_log', {}) + hours = nvme_log.get('power_on_hours', 'N/A') + + spare_val = nvme_log.get('available_spare', None) + if spare_val is not None: spare = f"{spare_val}%" + + errors = nvme_log.get('media_errors', 0) + + duw = nvme_log.get('data_units_written', 0) + if duw: total_bytes_written = duw * 1000 * 512 + + else: + # --- SATA/HDD LOGIC --- + attributes = data.get('ata_smart_attributes', {}).get('table', []) + + writes_32mib_candidates = [] + lba_written_val = 0 + + for attr in attributes: + id_ = attr.get('id') + # 'value' is the Normalized value (0-100 usually) + norm_val = attr.get('value', 0) + # 'raw.value' is the Raw count + raw_val = attr.get('raw', {}).get('value', 0) + + # Standard Metrics + if id_ == 9: hours = raw_val + if id_ == 194 and temp == "N/A": temp = f"{raw_val}C" + if id_ in [5, 197]: errors += raw_val + + # --- SATA SSD SPARE / LIFE LOGIC --- + # We prioritize explicit percentage attributes. + # 202: Percent_Lifetime_Remain + # 233: Media_Wearout_Indicator (Intel/Others) + # 232: Available_Reservd_Space + # 231: SSD_Life_Left + # 169: Remaining_Lifetime_Perc + if disk_type == "SSD": + if id_ in [169, 202, 231, 232, 233]: + # For these IDs, the Normalized Value is the percentage remaining + if norm_val is not None: + spare = f"{norm_val}%" + + # --- SATA WRITE CALCULATION --- + if id_ in [225, 241, 243]: + writes_32mib_candidates.append(raw_val) + if id_ == 246: + lba_written_val = raw_val + + # Finalize Writes + if writes_32mib_candidates: + total_bytes_written = max(writes_32mib_candidates) * 32 * 1024 * 1024 + elif lba_written_val > 0: + total_bytes_written = lba_written_val * sector_size + + written_str = get_size(total_bytes_written) if total_bytes_written > 0 else "N/A" + + return { + "Device": device_node, + "Type": disk_type, + "Size": get_size(size_bytes), + "Model": model, + "Serial": serial, + "Temp": temp, + "Hours": hours, + "Spare": spare, + "Err": errors, + "Written": written_str, + "Health": health + } + +def main(): + # Root check + if subprocess.run("id -u", shell=True, stdout=subprocess.PIPE).stdout.decode().strip() != '0': + print("Error: Must be run as root (sudo).") + sys.exit(1) + + # Get Devices + lsblk_cmd = "lsblk -d -n -o NAME,ROTA,TYPE --json" + lsblk_output = run_command(lsblk_cmd) + + if not lsblk_output: sys.exit(1) + try: + disks = json.loads(lsblk_output)['blockdevices'] + except: sys.exit(1) + + # Print Header + # Fmt: Dev(9) Type(6) Size(10) Temp(6) Health(8) Hours(9) Err(6) Spare(8) Written(10) Serial(20) Model + # header = f"{'DEVICE':<12} {'TYPE':<4} {'SIZE':<7} {'TEMP':<4} {'HEALTH':<6} {'HOURS':<5} {'ERR':<3} {'SPARE':<5} {'WRITTEN':<7} {'SERIAL':<20} {'MODEL'}" + header = f"{'DEVICE':<12} {'SIZE':<7} {'TEMP':<4} {'HEALTH':<6} {'HOURS':<5} {'ERR':<3} {'SPARE':<5} {'WRITTEN':<7} {'MODEL'}" + print(header) + print("-" * len(header)) + + # Process Disks + for disk in disks: + name = disk['name'] + if disk['type'] != 'disk': continue + + device_node = f"/dev/{name}" + smart_json = get_smart_data(device_node) + p = parse_smart_data(smart_json, device_node, disk['rota']) + + if p: + #/dev/nvme0n1 419.19 GB 29C OK 58695 0 98% 321.59 TB INTEL SSDPE2MX450G7 + #row = f"{p['Device']:<12} {p['Type']:<4} {p['Size']:<8} {p['Temp']:<3} {p['Health']:<5} {str(p['Hours']):<6} {str(p['Err']):<4} {str(p['Spare']):<3} {p['Written']:<8} {p['Serial']:<20} {p['Model']}" + row = f"{p['Device']:<12} {p['Size']:<8} {p['Temp']:<3} {p['Health']:<5} {str(p['Hours']):<6} {str(p['Err']):<4} {str(p['Spare']):<3} {p['Written']:<8} {p['Model']}" + print(row) + else: + print(f"{device_node:<9} [SMART Data Unavailable]") + +if __name__ == "__main__": + main()