File Coverage

blib/lib/Disk/SMART.pm
Criterion Covered Total %
statement 113 146 77.4
branch 14 36 38.8
condition 2 11 18.1
subroutine 20 23 86.9
pod 9 9 100.0
total 158 225 70.2


line stmt bran cond sub pod time code
1             package Disk::SMART;
2              
3 3     3   1691726 use warnings;
  3         9  
  3         97  
4 3     3   16 use strict;
  3         5  
  3         70  
5 3     3   71 use 5.010;
  3         17  
6 3     3   15 use Carp;
  3         6  
  3         217  
7 3     3   2282 use Math::Round;
  3         28771  
  3         169  
8 3     3   2208 use File::Which;
  3         2724  
  3         6449  
9              
10             {
11             $Disk::SMART::VERSION = '0.18'
12             }
13              
14             our $smartctl = which('smartctl');
15              
16             =head1 NAME
17              
18             Disk::SMART - Provides an interface to smartctl to return disk stats and to run tests.
19              
20             =head1 SYNOPSIS
21              
22             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
23              
24             use Disk::SMART;
25              
26             my $smart = Disk::SMART->new('/dev/sda', '/dev/sdb');
27              
28             my $disk_health = $smart->get_disk_health('/dev/sda');
29              
30             =cut
31              
32              
33             =head1 CONSTRUCTOR
34              
35             =head2 B
36              
37             Instantiates the Disk::SMART object
38              
39             C - 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.
40              
41             my $smart = Disk::SMART->new();
42             my $smart = Disk::SMART->new( '/dev/sda', '/dev/sdb' );
43             my @disks = $smart->get_disk_list();
44              
45             Returns C object if smartctl is available and can poll the given device(s).
46              
47             =cut
48              
49             sub new {
50 0     0 1 0 my ( $class, @devices ) = @_;
51 0         0 my $self = bless {}, $class;
52 0 0       0 die "$class must be called as root, please run $0 as root or with sudo\n" if $>;
53 0 0       0 @devices = @devices ? @devices : $self->get_disk_list();
54 0 0       0 confess "Valid device identifier not supplied to constructor, or no disks detected.\n"
55             if !@devices;
56              
57 0         0 $self->update_data(@devices);
58              
59 0         0 return $self;
60             }
61              
62              
63             =head1 USER METHODS
64              
65             =head2 B
66              
67             Returns hash of the SMART disk attributes and values
68              
69             C - Device identifier of a single SSD / Hard Drive
70              
71             my %disk_attributes = $smart->get_disk_attributes('/dev/sda');
72              
73             =cut
74              
75             sub get_disk_attributes {
76 2     2 1 706 my ( $self, $device ) = @_;
77 2         6 $self->_validate_param($device);
78              
79 1         2 return %{ $self->{'devices'}->{$device}->{'attributes'} };
  1         13  
80             }
81              
82              
83             =head2 B
84              
85             Returns scalar of any listed errors
86              
87             C - Device identifier of a single SSD/ Hard Drive
88              
89             my $disk_errors = $smart->get_disk_errors('/dev/sda');
90              
91             =cut
92              
93             sub get_disk_errors {
94 2     2 1 1523 my ( $self, $device ) = @_;
95 2         5 $self->_validate_param($device);
96              
97 1         6 return $self->{'devices'}->{$device}->{'errors'};
98             }
99              
100              
101             =head2 B
102              
103             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.
104              
105             Eg. "FAILED - Reported_Uncorrectable_Errors = 1"
106              
107             The attributes are:
108              
109             5 - Reallocated_Sector_Count
110              
111             187 - Reported_Uncorrectable_Errors
112              
113             188 - Command_Timeout
114              
115             197 - Current_Pending_Sector_Count
116              
117             198 - Offline_Uncorrectable
118              
119             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/
120              
121              
122             C - Device identifier of a single SSD / Hard Drive
123              
124             my $disk_health = $smart->get_disk_health('/dev/sda');
125              
126             =cut
127              
128             sub get_disk_health {
129 3     3 1 612 my ( $self, $device ) = @_;
130 3         9 $self->_validate_param($device);
131              
132 2         5 my $status = $self->{'devices'}->{$device}->{'health'};
133              
134 2         3 my %failure_attribute_hash;
135 2         3 while ( my ($key, $value) = each %{ $self->{'devices'}->{$device}->{'attributes'} } ) {
  38         119  
136 36 100       150 if ( $key =~ /\A5\Z|\A187\Z|\A188\Z|\A197\Z|\A198\Z/ ) {
137 8         14 $failure_attribute_hash{$key} = $value;
138 8 100       28 $status .= ": $key - $value->[0] = $value->[1]" if ( $value->[1] > 0 );
139             }
140             }
141              
142 2         10 return $status;
143             }
144              
145              
146             =head2 B
147              
148             Returns list of detected hda and sda devices. This method can be called manually if unsure what devices are present.
149              
150             $smart->get_disk_list;
151              
152             =cut
153            
154             sub get_disk_list {
155 0 0   0 1 0 open my $fh, '-|', 'parted -l' or confess "Can't run parted binary\n";
156 0         0 local $/ = undef;
157 0         0 my @disks = map { /Disk (\/.*\/[h|s]d[a-z]):/ } split /\n/, <$fh>;
  0         0  
158 0 0       0 close $fh or confess "Can't close file handle reading parted output\n";
159 0         0 return @disks;
160             }
161              
162             =head2 B
163              
164             Returns the model of the device. eg. "ST3250410AS".
165              
166             C - Device identifier of a single SSD / Hard Drive
167              
168             my $disk_model = $smart->get_disk_model('/dev/sda');
169              
170             =cut
171              
172             sub get_disk_model {
173 3     3 1 627 my ( $self, $device ) = @_;
174 3         7 $self->_validate_param($device);
175              
176 2         10 return $self->{'devices'}->{$device}->{'model'};
177             }
178              
179             =head2 B
180              
181             Returns an array with the temperature of the device in Celsius and Farenheit, or N/A.
182              
183             C - Device identifier of a single SSD / Hard Drive
184              
185             my ($temp_c, $temp_f) = $smart->get_disk_temp('/dev/sda');
186              
187             =cut
188              
189             sub get_disk_temp {
190 3     3 1 634 my ( $self, $device ) = @_;
191 3         9 $self->_validate_param($device);
192              
193 2         2 return @{ $self->{'devices'}->{$device}->{'temp'} };
  2         10  
194             }
195              
196             =head2 B
197              
198             Runs the SMART short self test and returns the result.
199              
200             C - Device identifier of SSD/ Hard Drive
201              
202             $smart->run_short_test('/dev/sda');
203              
204             =cut
205              
206             sub run_short_test {
207 1     1 1 628 my ( $self, $device ) = @_;
208 1         4 $self->_validate_param($device);
209              
210 0         0 my $test_out = get_smart_output( $device, '-t short' );
211 0         0 my ($short_test_time) = $test_out =~ /Please wait (.*) minutes/s;
212 0         0 sleep( $short_test_time * 60 );
213              
214 0         0 my $smart_output = _get_smart_output( $device, '-a' );
215 0         0 ($smart_output) = $smart_output =~ /(SMART Self-test log.*)\nSMART Selective self-test/s;
216 0         0 my @device_tests = split /\n/, $smart_output;
217 0         0 my $short_test_number = $device_tests[2];
218 0         0 my $short_test_status = substr $short_test_number, 25, +30;
219 0         0 $short_test_status = _trim($short_test_status);
220              
221 0         0 return $short_test_status;
222             }
223              
224             =head2 B
225              
226             Updates the SMART output and attributes for each device. Returns undef.
227              
228             C - 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.
229              
230             $smart->update_data('/dev/sda');
231              
232             =cut
233              
234             sub update_data {
235 2     2 1 747 my ( $self, @p_devices ) = @_;
236 2 50       7 my @devices = @p_devices ? @p_devices : $self->get_disk_list();
237              
238 2         5 foreach my $device (@devices) {
239 2         3 my $out;
240 2         6 $out = _get_smart_output( $device, '-a' );
241 2 50 33     17 confess "Smartctl couldn't poll device $device\nSmartctl Output:\n$out\n"
242             if ( !$out || $out !~ /START OF INFORMATION SECTION/ );
243              
244 2         5 chomp($out);
245 2         10 $self->{'devices'}->{$device}->{'SMART_OUTPUT'} = $out;
246              
247 2         7 $self->_process_disk_attributes($device);
248 2         6 $self->_process_disk_errors($device);
249 2         6 $self->_process_disk_health($device);
250 2         8 $self->_process_disk_model($device);
251 2         5 $self->_process_disk_temp($device);
252             }
253              
254 2         7 return;
255             }
256              
257             sub _get_smart_output {
258 0     0   0 my ( $device, $options ) = @_;
259 0   0     0 $options = $options // '';
260              
261 0 0 0     0 die "smartctl binary was not found on your system, are you running as root?\n"
262             if ( !defined $smartctl || !-f $smartctl );
263              
264 0 0       0 open my $fh, '-|', "$smartctl $device $options" or confess "Can't run smartctl binary\n";
265 0         0 local $/ = undef;
266 0         0 my $smart_output = <$fh>;
267              
268 0 0       0 if ( $smart_output =~ /Unknown USB bridge/ ) {
269 0 0       0 open $fh, '-|', "$smartctl $device $options -d sat" or confess "Can't run smartctl binary\n";
270 0         0 $smart_output = <$fh>;
271             }
272 0         0 return $smart_output;
273             }
274              
275             sub _process_disk_attributes {
276 2     2   4 my ( $self, $device ) = @_;
277 2         7 $self->_validate_param($device);
278              
279 2         5 my $smart_output = $self->{'devices'}->{$device}->{'SMART_OUTPUT'};
280 2         33 my ($smart_attributes) = $smart_output =~ /(ID# ATTRIBUTE_NAME.*)\nSMART Error/s;
281 2         26 my @attributes = split /\n/, $smart_attributes;
282 2         3 shift @attributes; #remove table header
283              
284 2         5 foreach my $attribute (@attributes) {
285 34         48 my $id = substr $attribute, 0, +3;
286 34         48 my $name = substr $attribute, 4, +24;
287 34         50 my $value = substr $attribute, 83, +50;
288 34         53 $id = _trim($id);
289 34         62 $name = _trim($name);
290 34         60 $value = _trim($value);
291 34         120 $self->{'devices'}->{$device}->{'attributes'}->{$id} = [ $name, $value ];
292             }
293              
294 2         7 return;
295             }
296              
297             sub _process_disk_errors {
298 2     2   5 my ( $self, $device ) = @_;
299 2         5 $self->_validate_param($device);
300              
301 2         5 my $smart_output = $self->{'devices'}->{$device}->{'SMART_OUTPUT'};
302 2         34 my ($errors) = $smart_output =~ /SMART Error Log Version: [1-9](.*)SMART Self-test log/s;
303 2         5 $errors = _trim($errors);
304 2 50       23 $errors = 'N/A' if !$errors;
305              
306 2         6 return $self->{'devices'}->{$device}->{'errors'} = $errors;
307             }
308              
309             sub _process_disk_health {
310 2     2   4 my ( $self, $device ) = @_;
311 2         5 $self->_validate_param($device);
312              
313 2         5 my $smart_output = $self->{'devices'}->{$device}->{'SMART_OUTPUT'};
314 2         14 my ($health) = $smart_output =~ /SMART overall-health self-assessment test result:(.*)\n/;
315 2         5 $health = _trim($health);
316 2 50 33     18 $health = 'N/A' if !$health || $health !~ /PASSED|FAILED/x;
317              
318 2         5 return $self->{'devices'}->{$device}->{'health'} = $health;
319             }
320              
321             sub _process_disk_model {
322 2     2   4 my ( $self, $device ) = @_;
323 2         10 $self->_validate_param($device);
324            
325 2         4 my $smart_output = $self->{'devices'}->{$device}->{'SMART_OUTPUT'};
326 2         9 my ($model) = $smart_output =~ /Device\ Model:(.*)\n/;
327 2         4 $model = _trim($model);
328 2 100       5 $model = 'N/A' if !$model;
329              
330 2         6 return $self->{'devices'}->{$device}->{'model'} = $model;
331             }
332              
333             sub _process_disk_temp {
334 2     2   5 my ( $self, $device ) = @_;
335 2         3 $self->_validate_param($device);
336 2         3 my ( $temp_c, $temp_f );
337              
338 2         5 my $smart_output = $self->{'devices'}->{$device}->{'SMART_OUTPUT'};
339 2         45 ($temp_c) = $smart_output =~ /(Temperature_Celsius.*\n|Airflow_Temperature_Cel.*\n)/;
340              
341 2 100       5 if ($temp_c) {
342 1         2 $temp_c = substr $temp_c, 83, +3;
343 1         4 $temp_c = _trim($temp_c);
344 1         9 $temp_f = round( ( $temp_c * 9 ) / 5 + 32 );
345 1         24 $temp_c = int $temp_c;
346 1         2 $temp_f = int $temp_f;
347             }
348             else {
349 1         2 $temp_c = 'N/A';
350 1         3 $temp_f = 'N/A';
351             }
352              
353 2         9 return $self->{'devices'}->{$device}->{'temp'} = [ ( $temp_c, $temp_f ) ];
354             }
355              
356             sub _trim {
357 109     109   137 my $string = shift;
358 109         394 $string =~ s/^\s+|\s+$//g; #trim beginning and ending whitepace
359 109         178 return $string;
360             }
361              
362             sub _validate_param {
363 24     24   37 my ( $self, $device ) = @_;
364             croak "$device not found in object. Verify you specified the right device identifier.\n"
365 24 100       707 if ( !exists $self->{'devices'}->{$device} );
366              
367 18         26 return;
368             }
369              
370             1;
371              
372              
373             __END__