#!/usr/bin/perl -w -CAL ### ### ParseSMART - SmartD attribute log parser ### By Matti 'ccr' Hamalainen (C) Copyright 2020-2022 TNSP ### use strict; use warnings; use utf8; # Settings my $set_logpath = "/var/lib/smartmontools/"; my $set_smartctl = "smartctl"; # Ordered list of attribute names to show my @ord_attrs = ( "Power_On_Hours", "Power_Cycle_Count", "Spin_Up_Time", "Start_Stop_Count", "Temperature_Celsius", "Seek_Error_Rate", "Raw_Read_Error_Rate", "Multi_Zone_Error_Rate", "UDMA_CRC_Error_Count", "Current_Pending_Sector", "Reallocated_Event_Count", "Reallocated_Sector_Ct", "Offline_Uncorrectable", "Total_Bad_Blocks", ); # Warn about change in these attributes my @warn_attrs = ( "Seek_Error_Rate", "Raw_Read_Error_Rate", "Multi_Zone_Error_Rate", "UDMA_CRC_Error_Count", "Current_Pending_Sector", "Reallocated_Event_Count", "Reallocated_Sector_Ct", "Offline_Uncorrectable", "Total_Bad_Blocks", ); # Attributes for which to use NON-RAW values for # 1 = Use normalized value # 2 = Use RAW value & 0xff (low 8 bits) # 3 = Use RAW value & 0xffff (low 16 bits) # my %use_values = ( "Temperature_Celsius" => 2, "Spin_Up_Time" => 1, ); ### ### Main code starts ### my $prg_name = $0; $prg_name =~ s#^.*/([^/]+)$#$1#; my $set_model; my $set_serial; my $lst_attrs = {}; my $tmp; die( "ParseSMART v0.2 (SmartD attribute log parser)\n". "By Matti 'ccr' Hamalainen (C) Copyright 2020-2022 TNSP\n". "This program is distributed under a 3-clause BSD -style license.\n". "\n". "Usage: ${prg_name} [-a]\n". "Example: ${prg_name} /dev/sda | less -S\n". "\n". "By default only certain attributes will be listed.\n". "Use -a option to list all attributes.\n" ) unless scalar(@ARGV) >= 1; my $opt_device = shift; my $opt_all = (defined($tmp = shift) && $tmp eq "-a") || 0; ### ### Parse smartctl output for attributes ### open(my $fh, "-|", $set_smartctl, "-a", $opt_device) or die("ERROR: Could not execute ".$set_smartctl.": ".$!."\n"); while (defined(my $line = <$fh>)) { $line =~ s/^\s+//; $line =~ s/\s+$//; if ($line =~ /^(\d+)\s+([A-Za-z_-]+)\s+(0x[0-9a-fA-F]+)\s+(\d+|---)\s+(\d+|---)\s+(\d+|---)\s+(Pre-fail|Old_age)\s+(Always|Offline)\s+(\S+)\s+(\d+)/) { # 1:id, 2:name, 3:flag, 4:value, 5:worst, 6:threshold, 7:type, 8:updated, 9:failed, 10:rawvalue $$lst_attrs{$1} = { "name" => $2, "type" => $7, "updated" => $8 }; } elsif ($line =~ /^Device Model:\s+(.+)$/) { $set_model = $1; } elsif ($line =~ /^Serial Number:\s+(.+)$/) { $set_serial = $1; } } close($fh); if (!defined($set_model) || !defined($set_serial)) { die("ERROR: Could not parse device model or serial.\n"); } ### Create list of attribute ids my @srt_attrs = (); for my $aname (@ord_attrs) { for my $mid (keys %{$lst_attrs}) { if ($$lst_attrs{$mid}{"name"} eq $aname) { push(@srt_attrs, $mid); } } } if ($opt_all) { my %map_attrs = map { $_ => 1 } @ord_attrs; for my $mid (keys %{$lst_attrs}) { if (!defined($map_attrs{$$lst_attrs{$mid}{"name"}})) { push(@srt_attrs, $mid); } } } ### Form the datafile name $set_model =~ tr/ -./___/; $set_serial =~ tr/ -./___/; my $set_datafile = $set_logpath."attrlog.".$set_model."-".$set_serial.".ata.csv"; print $set_datafile."\n"; ### Parse CSV data open(my $in_file, "<", $set_datafile) or die("ERROR: Could not open '".$set_datafile."': ".$!."\n"); my $lst_data = {}; while (defined(my $line = <$in_file>)) { $line =~ s/^\s+//; $line =~ s/\s+$//; if ($line =~ /^(\d{4}-\d\d-\d\d\s+\d\d:\d\d:\d\d)\s*;\s*(.*)$/) { my ($stamp, $rest) = ($1, $2); my @fields = split(/\s*;\s*/, $rest); for (my $nfield = 0; $nfield < scalar(@fields); $nfield += 3) { my $aname = $$lst_attrs{$fields[$nfield]}{"name"}; my $anorm = $fields[$nfield + 1]; my $araw = $fields[$nfield + 2]; my $aval = $araw; $aname = "UNDEF" unless defined($aname); if (defined($use_values{$aname})) { my $cvv = $use_values{$aname}; if ($cvv == 1) { $aval = $anorm; } elsif ($cvv == 2) { $aval = $araw & 0xff; } elsif ($cvv == 3) { $aval = $araw & 0xffff ; } } $$lst_data{$stamp}{$fields[$nfield]} = $aval; } } } close($in_file); ### ### Output the list ### my $header = sprintf(" %-19s | %s", "", join(" | ", map { sprintf("%24s", $$lst_attrs{$_}{"name"}) } @srt_attrs)); print $header."\n".("-" x length($header))."\n"; my $pdate; my $nwarn = 0; for my $mdate (sort keys %{$lst_data}) { my $kwarn = 0; if (defined($pdate)) { for my $efield (@warn_attrs) { if ($$lst_data{$pdate}{$efield} != $$lst_data{$mdate}{$efield}) { $kwarn = 1; $nwarn++; last; } } } printf("%s%-19s | %s\n", ($kwarn ? "*" : " "), $mdate, join(" | ", map { sprintf("%24s", $$lst_data{$mdate}{$_}) } @srt_attrs)); } printf("%d WARN list attribute changes.\n", $nwarn);