SMART.pm 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  1. package Disk::SMART;
  2. use warnings;
  3. use strict;
  4. use Carp;
  5. use Math::Round;
  6. {
  7. $Disk::SMART::VERSION = '0.11'
  8. }
  9. chomp( our $smartctl = qx(which smartctl) );
  10. =head1 NAME
  11. Disk::SMART - Provides an interface to smartctl to return disk stats and to run tests.
  12. =head1 SYNOPSIS
  13. Disk::SMART is an object oriented module that provides an interface to get SMART disk info from a device as well as initiate testing.
  14. use Disk::SMART;
  15. my $smart = Disk::SMART->new('/dev/sda', '/dev/sdb');
  16. my $disk_health = $smart->get_disk_health('/dev/sda');
  17. =cut
  18. =head1 CONSTRUCTOR
  19. =head2 B<new(DEVICE)>
  20. Instantiates the Disk::SMART object
  21. C<DEVICE> - Device identifier of SSD / Hard Drive. The constructor takes either a single device name, or an array of device names.
  22. my $smart = Disk::SMART->new( 'dev/sda', '/dev/sdb' );
  23. Returns C<Disk::SMART> object if smartctl is available and can poll the given device(s).
  24. =cut
  25. sub new {
  26. my ( $class, @devices ) = @_;
  27. my $self = bless {}, $class;
  28. croak "Valid device identifier not supplied to constructor for $class.\n"
  29. if !@devices && !defined $ENV{'MOCK_TEST_DATA'};
  30. croak "smartctl binary was not found on your system, are you running as root?\n"
  31. if !-f $smartctl && !defined $ENV{'MOCK_TEST_DATA'};
  32. $self->update_data($_) foreach @devices;
  33. return $self;
  34. }
  35. =head1 USER METHODS
  36. =head2 B<get_disk_attributes(DEVICE)>
  37. Returns hash of the SMART disk attributes and values
  38. C<DEVICE> - Device identifier of SSD/ Hard Drive
  39. my %disk_attributes = $smart->get_disk_attributes('/dev/sda');
  40. =cut
  41. sub get_disk_attributes {
  42. my ( $self, $device ) = @_;
  43. $self->_validate_param($device);
  44. return %{ $self->{'devices'}->{$device}->{'attributes'} };
  45. }
  46. =head2 B<get_disk_errors(DEVICE)>
  47. Returns scalar of any listed errors
  48. C<DEVICE> - Device identifier of SSD/ Hard Drive
  49. my $disk_errors = $smart->get_disk_errors('/dev/sda');
  50. =cut
  51. sub get_disk_errors {
  52. my ( $self, $device ) = @_;
  53. $self->_validate_param($device);
  54. return $self->{'devices'}->{$device}->{'errors'};
  55. }
  56. =head2 B<get_disk_health(DEVICE)>
  57. 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.
  58. Eg. "FAILED - Reported_Uncorrectable_Errors = 1"
  59. The attributes are:
  60. 5 - Reallocated_Sector_Count
  61. 187 - Reported_Uncorrectable_Errors
  62. 188 - Command_Timeout
  63. 197 - Current_Pending_Sector_Count
  64. 198 - Offline_Uncorrectable
  65. 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/
  66. C<DEVICE> - Device identifier of SSD / Hard Drive
  67. my $disk_health = $smart->get_disk_health('/dev/sda');
  68. =cut
  69. sub get_disk_health {
  70. my ( $self, $device ) = @_;
  71. $self->_validate_param($device);
  72. my $status = $self->{'devices'}->{$device}->{'health'};
  73. my %failure_attribute_hash;
  74. while ( my ($key, $value) = each %{ $self->{'devices'}->{$device}->{'attributes'} } ) {
  75. if ( $key =~ /\A5\Z|\A187\Z|\A188\Z|\A197\Z|\A198\Z/ ) {
  76. $failure_attribute_hash{$key} = $value;
  77. $status .= ": $key - $value->[0] = $value->[1]" if ( $value->[1] > 0 );
  78. }
  79. }
  80. return $status;
  81. }
  82. =head2 B<get_disk_model(DEVICE)>
  83. Returns the model of the device. eg. "ST3250410AS".
  84. C<DEVICE> - Device identifier of SSD / Hard Drive
  85. my $disk_model = $smart->get_disk_model('/dev/sda');
  86. =cut
  87. sub get_disk_model {
  88. my ( $self, $device ) = @_;
  89. $self->_validate_param($device);
  90. return $self->{'devices'}->{$device}->{'model'};
  91. }
  92. =head2 B<get_disk_temp(DEVICE)>
  93. Returns an array with the temperature of the device in Celsius and Farenheit, or N/A.
  94. C<DEVICE> - Device identifier of SSD / Hard Drive
  95. my ($temp_c, $temp_f) = $smart->get_disk_temp('/dev/sda');
  96. =cut
  97. sub get_disk_temp {
  98. my ( $self, $device ) = @_;
  99. $self->_validate_param($device);
  100. return @{ $self->{'devices'}->{$device}->{'temp'} };
  101. }
  102. =head2 B<update_data(DEVICE)>
  103. Updates the SMART output and attributes of a device. Returns undef.
  104. C<DEVICE> - Device identifier of SSD Hard Drive
  105. $smart->update_data('/dev/sda');
  106. =cut
  107. sub update_data {
  108. my ( $self, $device ) = @_;
  109. my $out;
  110. $out = $ENV{'MOCK_TEST_DATA'} if defined $ENV{'MOCK_TEST_DATA'};
  111. $out = qx($smartctl -a $device) if ( !defined $ENV{'MOCK_TEST_DATA'} && -f $smartctl );
  112. croak "Smartctl couldn't poll device $device\n"
  113. if ( !$out || $out !~ /START OF INFORMATION SECTION/ );
  114. chomp($out);
  115. $self->{'devices'}->{$device}->{'SMART_OUTPUT'} = $out;
  116. $self->_process_disk_attributes($device);
  117. $self->_process_disk_errors($device);
  118. $self->_process_disk_health($device);
  119. $self->_process_disk_model($device);
  120. $self->_process_disk_temp($device);
  121. return;
  122. }
  123. =head2 B<run_short_test(DEVICE)>
  124. Runs the SMART short self test and returns the result.
  125. C<DEVICE> - Device identifier of SSD/ Hard Drive
  126. $smart->run_short_test('/dev/sda');
  127. =cut
  128. sub run_short_test {
  129. my ( $self, $device ) = @_;
  130. $self->_validate_param($device);
  131. if ( !defined $ENV{'MOCK_TEST_DATA'} ) {
  132. my $out = qx( $smartctl -t short $device );
  133. my ($short_test_time) = $out =~ /Please wait (.*) minutes/s;
  134. sleep( $short_test_time * 60 );
  135. }
  136. my $smart_output = $ENV{'MOCK_TEST_DATA'} // qx($smartctl -a $device);
  137. ($smart_output) = $smart_output =~ /(SMART Self-test log.*)\nSMART Selective self-test/s;
  138. my @device_tests = split /\n/, $smart_output;
  139. my $short_test_number = $device_tests[2];
  140. my $short_test_status = substr $short_test_number, 25, +30;
  141. $short_test_status = _trim($short_test_status);
  142. return $short_test_status;
  143. }
  144. ##INTERNAL FUNCTIONS BELOW. THESE SHOULD NEVER BE CALLED BY A SCRIPT##
  145. sub _process_disk_attributes {
  146. my ( $self, $device ) = @_;
  147. $self->_validate_param($device);
  148. my $smart_output = $self->{'devices'}->{$device}->{'SMART_OUTPUT'};
  149. my ($smart_attributes) = $smart_output =~ /(ID# ATTRIBUTE_NAME.*)\nSMART Error/s;
  150. my @attributes = split /\n/, $smart_attributes;
  151. shift @attributes; #remove table header
  152. foreach my $attribute (@attributes) {
  153. my $id = substr $attribute, 0, +3;
  154. my $name = substr $attribute, 4, +24;
  155. my $value = substr $attribute, 83, +50;
  156. $id = _trim($id);
  157. $name = _trim($name);
  158. $value = _trim($value);
  159. $self->{'devices'}->{$device}->{'attributes'}->{$id} = [ $name, $value ];
  160. }
  161. return;
  162. }
  163. sub _process_disk_errors {
  164. my ( $self, $device ) = @_;
  165. $self->_validate_param($device);
  166. my $smart_output = $self->{'devices'}->{$device}->{'SMART_OUTPUT'};
  167. my ($errors) = $smart_output =~ /SMART Error Log Version: [1-9](.*)SMART Self-test log/s;
  168. $errors = _trim($errors);
  169. $errors = 'N/A' if !$errors;
  170. return $self->{'devices'}->{$device}->{'errors'} = $errors;
  171. }
  172. sub _process_disk_health {
  173. my ( $self, $device ) = @_;
  174. $self->_validate_param($device);
  175. my $smart_output = $self->{'devices'}->{$device}->{'SMART_OUTPUT'};
  176. my ($health) = $smart_output =~ /SMART overall-health self-assessment test result:(.*)\n/;
  177. $health = _trim($health);
  178. $health = 'N/A' if !$health || $health !~ /PASSED|FAILED/x;
  179. return $self->{'devices'}->{$device}->{'health'} = $health;
  180. }
  181. sub _process_disk_model {
  182. my ( $self, $device ) = @_;
  183. $self->_validate_param($device);
  184. my $smart_output = $self->{'devices'}->{$device}->{'SMART_OUTPUT'};
  185. my ($model) = $smart_output =~ /Device\ Model:(.*)\n/;
  186. $model = _trim($model);
  187. $model = 'N/A' if !$model;
  188. return $self->{'devices'}->{$device}->{'model'} = $model;
  189. }
  190. sub _process_disk_temp {
  191. my ( $self, $device ) = @_;
  192. $self->_validate_param($device);
  193. my ( $temp_c, $temp_f );
  194. my $smart_output = $self->{'devices'}->{$device}->{'SMART_OUTPUT'};
  195. ($temp_c) = $smart_output =~ /(Temperature_Celsius.*\n|Airflow_Temperature_Cel.*\n)/;
  196. if ($temp_c) {
  197. $temp_c = substr $temp_c, 83, +3;
  198. $temp_c = _trim($temp_c);
  199. $temp_f = round( ( $temp_c * 9 ) / 5 + 32 );
  200. $temp_c = int $temp_c;
  201. $temp_f = int $temp_f;
  202. }
  203. else {
  204. $temp_c = 'N/A';
  205. $temp_f = 'N/A';
  206. }
  207. return $self->{'devices'}->{$device}->{'temp'} = [ ( $temp_c, $temp_f ) ];
  208. }
  209. sub _trim {
  210. my $string = shift;
  211. $string =~ s/^\s+|\s+$//g; #trim beginning and ending whitepace
  212. return $string;
  213. }
  214. sub _validate_param {
  215. my ( $self, $device ) = @_;
  216. croak "$device not found in object. Verify you specified the right device identifier.\n"
  217. if ( !exists $self->{'devices'}->{$device} );
  218. return;
  219. }
  220. 1;
  221. __END__
  222. =head1 COMPATIBILITY
  223. This module should run on any UNIX like OS with Perl 5.10+ and the smartctl progam installed from the smartmontools package.
  224. =head1 AUTHOR
  225. Paul Trost <ptrost@cpan.org>
  226. =head1 LICENSE AND COPYRIGHT
  227. Copyright 2014 by Paul Trost
  228. 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.
  229. <http://gnu.org/licenses/gpl.html>
  230. =cut