SMART.pm 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389
  1. package Disk::SMART;
  2. use warnings;
  3. use strict;
  4. use 5.010;
  5. use Carp;
  6. use Math::Round;
  7. use File::Which;
  8. {
  9. $Disk::SMART::VERSION = '0.18'
  10. }
  11. our $smartctl = which('smartctl');
  12. =head1 NAME
  13. Disk::SMART - Provides an interface to smartctl to return disk stats and to run tests.
  14. =head1 SYNOPSIS
  15. Disk::SMART is an object oriented module that provides an interface to get SMART disk info from a device as well as initiate testing. An exmple script using this module can be found at https://github.com/paultrost/linux-geek/blob/master/sysinfo.pl
  16. use Disk::SMART;
  17. my $smart = Disk::SMART->new('/dev/sda', '/dev/sdb');
  18. my $disk_health = $smart->get_disk_health('/dev/sda');
  19. =cut
  20. =head1 CONSTRUCTOR
  21. =head2 B<new(DEVICE)>
  22. Instantiates the Disk::SMART object
  23. C<DEVICE> - Device identifier of a single SSD / Hard Drive, or a list. If no devices are supplied then it runs get_disk_list() which will return an array of detected sdX and hdX devices.
  24. my $smart = Disk::SMART->new();
  25. my $smart = Disk::SMART->new( '/dev/sda', '/dev/sdb' );
  26. my @disks = $smart->get_disk_list();
  27. Returns C<Disk::SMART> object if smartctl is available and can poll the given device(s).
  28. =cut
  29. sub new {
  30. my ( $class, @devices ) = @_;
  31. my $self = bless {}, $class;
  32. die "$class must be called as root, please run $0 as root or with sudo\n" if $>;
  33. @devices = @devices ? @devices : $self->get_disk_list();
  34. confess "Valid device identifier not supplied to constructor, or no disks detected.\n"
  35. if !@devices;
  36. $self->update_data(@devices);
  37. return $self;
  38. }
  39. =head1 USER METHODS
  40. =head2 B<get_disk_attributes(DEVICE)>
  41. Returns hash of the SMART disk attributes and values
  42. C<DEVICE> - Device identifier of a single SSD / Hard Drive
  43. my %disk_attributes = $smart->get_disk_attributes('/dev/sda');
  44. =cut
  45. sub get_disk_attributes {
  46. my ( $self, $device ) = @_;
  47. $self->_validate_param($device);
  48. return %{ $self->{'devices'}->{$device}->{'attributes'} };
  49. }
  50. =head2 B<get_disk_errors(DEVICE)>
  51. Returns scalar of any listed errors
  52. C<DEVICE> - Device identifier of a single SSD/ Hard Drive
  53. my $disk_errors = $smart->get_disk_errors('/dev/sda');
  54. =cut
  55. sub get_disk_errors {
  56. my ( $self, $device ) = @_;
  57. $self->_validate_param($device);
  58. return $self->{'devices'}->{$device}->{'errors'};
  59. }
  60. =head2 B<get_disk_health(DEVICE)>
  61. Returns the health of the disk. Output is "PASSED", "FAILED", or "N/A". If the device has positive values for the attributes listed below then the status will output that information.
  62. Eg. "FAILED - Reported_Uncorrectable_Errors = 1"
  63. The attributes are:
  64. 5 - Reallocated_Sector_Count
  65. 187 - Reported_Uncorrectable_Errors
  66. 188 - Command_Timeout
  67. 197 - Current_Pending_Sector_Count
  68. 198 - Offline_Uncorrectable
  69. If Reported_Uncorrectable_Errors is greater than 0 then the drive should be replaced immediately. This list is taken from a study shown at https://www.backblaze.com/blog/hard-drive-smart-stats/
  70. C<DEVICE> - Device identifier of a single SSD / Hard Drive
  71. my $disk_health = $smart->get_disk_health('/dev/sda');
  72. =cut
  73. sub get_disk_health {
  74. my ( $self, $device ) = @_;
  75. $self->_validate_param($device);
  76. my $status = $self->{'devices'}->{$device}->{'health'};
  77. my %failure_attribute_hash;
  78. while ( my ($key, $value) = each %{ $self->{'devices'}->{$device}->{'attributes'} } ) {
  79. if ( $key =~ /\A5\Z|\A187\Z|\A188\Z|\A197\Z|\A198\Z/ ) {
  80. $failure_attribute_hash{$key} = $value;
  81. $status .= ": $key - $value->[0] = $value->[1]" if ( $value->[1] > 0 );
  82. }
  83. }
  84. return $status;
  85. }
  86. =head2 B<get_disk_list>
  87. Returns list of detected hda and sda devices. This method can be called manually if unsure what devices are present.
  88. $smart->get_disk_list;
  89. =cut
  90. sub get_disk_list {
  91. open my $fh, '-|', 'parted -l' or confess "Can't run parted binary\n";
  92. local $/ = undef;
  93. my @disks = map { /Disk (\/.*\/[h|s]d[a-z]):/ } split /\n/, <$fh>;
  94. close $fh or confess "Can't close file handle reading parted output\n";
  95. return @disks;
  96. }
  97. =head2 B<get_disk_model(DEVICE)>
  98. Returns the model of the device. eg. "ST3250410AS".
  99. C<DEVICE> - Device identifier of a single SSD / Hard Drive
  100. my $disk_model = $smart->get_disk_model('/dev/sda');
  101. =cut
  102. sub get_disk_model {
  103. my ( $self, $device ) = @_;
  104. $self->_validate_param($device);
  105. return $self->{'devices'}->{$device}->{'model'};
  106. }
  107. =head2 B<get_disk_temp(DEVICE)>
  108. Returns an array with the temperature of the device in Celsius and Farenheit, or N/A.
  109. C<DEVICE> - Device identifier of a single SSD / Hard Drive
  110. my ($temp_c, $temp_f) = $smart->get_disk_temp('/dev/sda');
  111. =cut
  112. sub get_disk_temp {
  113. my ( $self, $device ) = @_;
  114. $self->_validate_param($device);
  115. return @{ $self->{'devices'}->{$device}->{'temp'} };
  116. }
  117. =head2 B<run_short_test(DEVICE)>
  118. Runs the SMART short self test and returns the result.
  119. C<DEVICE> - Device identifier of SSD/ Hard Drive
  120. $smart->run_short_test('/dev/sda');
  121. =cut
  122. sub run_short_test {
  123. my ( $self, $device ) = @_;
  124. $self->_validate_param($device);
  125. my $test_out = _get_smart_output( $device, '-t short' );
  126. my ($short_test_time) = $test_out =~ /Please wait (.*) minutes/s;
  127. sleep( $short_test_time * 60 );
  128. my $smart_output = _get_smart_output( $device, '-a' );
  129. ($smart_output) = $smart_output =~ /(SMART Self-test log.*)\nSMART Selective self-test/s;
  130. my @device_tests = split /\n/, $smart_output;
  131. my $short_test_number = $device_tests[2];
  132. my $short_test_status = substr $short_test_number, 25, +30;
  133. $short_test_status = _trim($short_test_status);
  134. return $short_test_status;
  135. }
  136. =head2 B<update_data(DEVICE)>
  137. Updates the SMART output and attributes for each device. Returns undef.
  138. C<DEVICE> - Device identifier of a single SSD / Hard Drive or a list of devices. If none are specified then get_disk_list() is called to detect devices.
  139. $smart->update_data('/dev/sda');
  140. =cut
  141. sub update_data {
  142. my ( $self, @p_devices ) = @_;
  143. my @devices = @p_devices ? @p_devices : $self->get_disk_list();
  144. foreach my $device (@devices) {
  145. my $out;
  146. $out = _get_smart_output( $device, '-a' );
  147. confess "Smartctl couldn't poll device $device\nSmartctl Output:\n$out\n"
  148. if ( !$out || $out !~ /START OF INFORMATION SECTION/ );
  149. chomp($out);
  150. $self->{'devices'}->{$device}->{'SMART_OUTPUT'} = $out;
  151. $self->_process_disk_attributes($device);
  152. $self->_process_disk_errors($device);
  153. $self->_process_disk_health($device);
  154. $self->_process_disk_model($device);
  155. $self->_process_disk_temp($device);
  156. }
  157. return;
  158. }
  159. sub _get_smart_output {
  160. my ( $device, $options ) = @_;
  161. $options = $options // '';
  162. die "smartctl binary was not found on your system, are you running as root?\n"
  163. if ( !defined $smartctl || !-f $smartctl );
  164. open my $fh, '-|', "$smartctl $device $options" or confess "Can't run smartctl binary\n";
  165. local $/ = undef;
  166. my $smart_output = <$fh>;
  167. if ( $smart_output =~ /Unknown USB bridge/ ) {
  168. open $fh, '-|', "$smartctl $device $options -d sat" or confess "Can't run smartctl binary\n";
  169. $smart_output = <$fh>;
  170. }
  171. return $smart_output;
  172. }
  173. sub _process_disk_attributes {
  174. my ( $self, $device ) = @_;
  175. $self->_validate_param($device);
  176. my $smart_output = $self->{'devices'}->{$device}->{'SMART_OUTPUT'};
  177. my ($smart_attributes) = $smart_output =~ /(ID# ATTRIBUTE_NAME.*)\nSMART Error/s;
  178. my @attributes = split /\n/, $smart_attributes;
  179. shift @attributes; #remove table header
  180. foreach my $attribute (@attributes) {
  181. my $id = substr $attribute, 0, +3;
  182. my $name = substr $attribute, 4, +24;
  183. my $value = substr $attribute, 83, +50;
  184. $id = _trim($id);
  185. $name = _trim($name);
  186. $value = _trim($value);
  187. $self->{'devices'}->{$device}->{'attributes'}->{$id} = [ $name, $value ];
  188. }
  189. return;
  190. }
  191. sub _process_disk_errors {
  192. my ( $self, $device ) = @_;
  193. $self->_validate_param($device);
  194. my $smart_output = $self->{'devices'}->{$device}->{'SMART_OUTPUT'};
  195. my ($errors) = $smart_output =~ /SMART Error Log Version: [1-9](.*)SMART Self-test log/s;
  196. $errors = _trim($errors);
  197. $errors = 'N/A' if !$errors;
  198. return $self->{'devices'}->{$device}->{'errors'} = $errors;
  199. }
  200. sub _process_disk_health {
  201. my ( $self, $device ) = @_;
  202. $self->_validate_param($device);
  203. my $smart_output = $self->{'devices'}->{$device}->{'SMART_OUTPUT'};
  204. my ($health) = $smart_output =~ /SMART overall-health self-assessment test result:(.*)\n/;
  205. $health = _trim($health);
  206. $health = 'N/A' if !$health || $health !~ /PASSED|FAILED/x;
  207. return $self->{'devices'}->{$device}->{'health'} = $health;
  208. }
  209. sub _process_disk_model {
  210. my ( $self, $device ) = @_;
  211. $self->_validate_param($device);
  212. my $smart_output = $self->{'devices'}->{$device}->{'SMART_OUTPUT'};
  213. my ($model) = $smart_output =~ /Device\ Model:(.*)\n/;
  214. $model = _trim($model);
  215. $model = 'N/A' if !$model;
  216. return $self->{'devices'}->{$device}->{'model'} = $model;
  217. }
  218. sub _process_disk_temp {
  219. my ( $self, $device ) = @_;
  220. $self->_validate_param($device);
  221. my ( $temp_c, $temp_f );
  222. my $smart_output = $self->{'devices'}->{$device}->{'SMART_OUTPUT'};
  223. ($temp_c) = $smart_output =~ /(Temperature_Celsius.*\n|Airflow_Temperature_Cel.*\n)/;
  224. if ($temp_c) {
  225. $temp_c = substr $temp_c, 83, +3;
  226. $temp_c = _trim($temp_c);
  227. $temp_f = round( ( $temp_c * 9 ) / 5 + 32 );
  228. $temp_c = int $temp_c;
  229. $temp_f = int $temp_f;
  230. }
  231. else {
  232. $temp_c = 'N/A';
  233. $temp_f = 'N/A';
  234. }
  235. return $self->{'devices'}->{$device}->{'temp'} = [ ( $temp_c, $temp_f ) ];
  236. }
  237. sub _trim {
  238. my $string = shift;
  239. $string =~ s/^\s+|\s+$//g; #trim beginning and ending whitepace
  240. return $string;
  241. }
  242. sub _validate_param {
  243. my ( $self, $device ) = @_;
  244. croak "$device not found in object. Verify you specified the right device identifier.\n"
  245. if ( !exists $self->{'devices'}->{$device} );
  246. return;
  247. }
  248. 1;
  249. __END__
  250. =head1 COMPATIBILITY
  251. This module should run on any UNIX like OS with Perl 5.10+ and the smartctl progam installed from the smartmontools package.
  252. =head1 AUTHOR
  253. Paul Trost <ptrost@cpan.org>
  254. =head1 LICENSE AND COPYRIGHT
  255. Copyright 2015 by Paul Trost
  256. This script is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License v2, or at your option any later version.
  257. <http://gnu.org/licenses/gpl.html>
  258. =cut