Handle hostnames with upper-case letters
[webmin.git] / smart-status / smart-status-lib.pl
1 =head1 smart-status-lib.pl
2
3 Functions for getting SMART status
4
5 =cut
6
7 BEGIN { push(@INC, ".."); };
8 use WebminCore;
9 &init_config();
10 =head2 get_smart_version()
11
12 Returns the version number of the SMART tools on this system
13
14 =cut
15 sub get_smart_version
16 {
17 if (!defined($smartctl_version_cache)) {
18         local $out = &backquote_command(
19                         "$config{'smartctl'} --version 2>&1 </dev/null");
20         if ($out =~ /smartmontools release\s+(\S+)/i) {
21                 $smartctl_version_cache = $1;
22                 }
23         }
24 return $smartctl_version_cache;
25 }
26
27 =head2 list_smart_disks_partitions
28
29 Returns a sorted list of disks that can support SMART.
30
31 =cut
32 sub list_smart_disks_partitions
33 {
34 if (&foreign_check("fdisk")) {
35         return &list_smart_disks_partitions_fdisk();
36         }
37 elsif (&foreign_check("mount")) {
38         return &list_smart_disks_partitions_fstab();
39         }
40 return ( );
41 }
42
43 =head2 list_smart_disks_partitions_fdisk
44
45 Returns a sorted list of disks that can support SMART, using the Linux fdisk
46 module. May include faked-up 3ware devices.
47
48 =cut
49 sub list_smart_disks_partitions_fdisk
50 {
51 &foreign_require("fdisk");
52 local @rv;
53 my $twcount = 0;
54 foreach my $d (sort { $a->{'device'} cmp $b->{'device'} }
55                     &fdisk::list_disks_partitions()) {
56         if (($d->{'type'} eq 'scsi' || $d->{'type'} eq 'raid') &&
57             $d->{'model'} =~ /3ware|amcc/i) {
58                 # A 3ware hardware RAID device.
59
60                 # First find the controllers.
61                 local @ctrls = &list_3ware_controllers();
62
63                 # For each controller, find all the units (u0, u1, etc..)
64                 local @units;
65                 foreach my $c (@ctrls) {
66                         push(@units, &list_3ware_subdisks($c));
67                         }
68
69                 # Assume that /dev/sdX maps to units in order
70                 my $i = 0;
71                 foreach my $sd (@{$units[$twcount]->[2]}) {
72                         my $c = $units[$twcount]->[1];
73                         my $cidx = &indexof($c, @ctrls);
74                         my $dev = "/dev/twa".$cidx;
75                         if (!-r $dev) {
76                                 $dev = "/dev/twe".$cidx;
77                                 }
78                         push(@rv, { 'device' => $dev,
79                                     'prefix' => $dev,
80                                     'desc' => '3ware physical disk unit '.
81                                       $units[$twcount]->[0].' number '.$sd,
82                                     'type' => 'scsi',
83                                     'subtype' => '3ware',
84                                     'subdisk' => substr($sd, 1),
85                                     'id' => $d->{'id'},
86                                   });
87                         $i++;
88                         }
89                 $twcount++;
90                 }
91         elsif ($d->{'device'} =~ /^\/dev\/cciss\/(.*)$/) {
92                 # HP Smart Array .. add underlying disks
93                 my $count = &count_subdisks($d, "cciss");
94                 for(my $i=0; $i<$count; $i++) {
95                         push(@rv, { 'device' => $d->{'device'},
96                                     'prefix' => $d->{'device'},
97                                     'desc' => 'HP Smart Array physical disk '.$i,
98                                     'type' => 'scsi',
99                                     'subtype' => 'cciss',
100                                     'subdisk' => $i,
101                                     'id' => $d->{'id'},
102                                   });
103                         }
104                 }
105         elsif ($d->{'type'} eq 'scsi' || $d->{'type'} eq 'ide') {
106                 # Some other disk
107                 push(@rv, $d);
108                 }
109         }
110 return sort { $a->{'device'} cmp $b->{'device'} ||
111               $a->{'subdisk'} <=> $b->{'subdisk'} } @rv;
112 }
113
114 =head2 list_3ware_subdisks(controller)
115
116 Returns a list, each element of which is a unit, controller and list of subdisks
117
118 =cut
119 sub list_3ware_subdisks
120 {
121 local ($ctrl) = @_;
122 local $out = &backquote_command("tw_cli info $ctrl");
123 return () if ($?);
124 my @rv;
125 foreach my $l (split(/\r?\n/, $out)) {
126         if ($l =~ /^(u\d+)\s/) {
127                 push(@rv, [ $1, $ctrl, [ ] ]);
128                 }
129         elsif ($l =~ /^(p\d+)\s+(\S+)\s+(\S+)/ &&
130                $2 ne 'NOT-PRESENT') {
131                 my ($u) = grep { $_->[0] eq $3 } @rv;
132                 if ($u) {
133                         push(@{$u->[2]}, $1);
134                         }
135                 }
136         }
137 return @rv;
138 }
139
140 =head2 list_3ware_controllers()
141
142 Returns a list of 3ware controllers, each of which is just a string like c0
143
144 =cut
145 sub list_3ware_controllers
146 {
147 local $out = &backquote_command("tw_cli show");
148 return () if ($?);
149 my @rv;
150 foreach my $l (split(/\r?\n/, $out)) {
151         if ($l =~ /^(c\d+)\s/) {
152                 push(@rv, $1);
153                 }
154         }
155 return @rv;
156 }
157
158 =head2 count_subdisks(&drive, type, [device])
159
160 Returns the number of sub-disks for a hardware RAID device, by calling
161 smartctl on them until failure.
162
163 =cut
164 sub count_subdisks
165 {
166 local ($d, $type, $device) = @_;
167 $device ||= $d->{'device'};
168 local $count = 0;
169 while(1) {
170         local $cmd = "$config{'smartctl'} -d $type,$count ".quotemeta($device);
171         &execute_command($cmd);
172         last if ($?);
173         $count++;
174         }
175 return $count;
176 }
177
178 =head2 list_smart_disks_partitions_fstab
179
180 Returns a list of disks on which we can use SMART, taken from /etc/fstab.
181
182 =cut
183 sub list_smart_disks_partitions_fstab
184 {
185 &foreign_require("mount");
186 my @rv;
187 foreach my $m (&mount::list_mounted(1)) {
188         if ($m->[1] =~ /^(\/dev\/(da|ad)([0-9]+))/ &&
189             $m->[2] ne 'cd9660') {
190                 # FreeBSD-style disk name
191                 push(@rv, { 'device' => $1,
192                             'desc' => ($2 eq 'ad' ? 'IDE' : 'SCSI').
193                                       ' disk '.$3 });
194                 }
195         elsif ($m->[1] =~ /^(\/dev\/disk\d+)/ &&
196                ($m->[2] eq 'ufs' || $m->[2] eq 'hfs')) {
197                 # MacOS disk name
198                 push(@rv, { 'device' => $1,
199                             'desc' => $1 });
200                 }
201         elsif ($m->[1] =~ /^(\/dev\/([hs])d([a-z]))/ &&
202                $m->[2] ne 'iso9660') {
203                 # Linux disk name
204                 push(@rv, { 'device' => $1,
205                             'desc' => ($2 eq 'h' ? 'IDE' : 'SCSI').
206                                       ' disk '.uc($3) });
207                 }
208         }
209 my %done;
210 @rv = grep { !$done{$_->{'device'}}++ } @rv;
211 return @rv;
212 }
213
214 =head2 get_drive_status(device-name, [&drive])
215
216 Returns a hash reference containing the status of some drive
217
218 =cut
219 sub get_drive_status
220 {
221 local ($device, $drive) = @_;
222 local %rv;
223 local $qd = quotemeta($device);
224 local $extra_args = &get_extra_args($device, $drive);
225 if (&get_smart_version() > 5.0) {
226         # Use new command format
227
228         # Check support
229         local $out = &backquote_command(
230                         "$config{'smartctl'} $extra_args  -i $qd 2>&1");
231         if ($out =~ /SMART\s+support\s+is:\s+Available/i) {
232                 $rv{'support'} = 1;
233                 }
234         elsif ($out =~ /Device\s+supports\s+SMART/i) {
235                 $rv{'support'} = 1;
236                 }
237         else {
238                 $rv{'support'} = 0;
239                 }
240         if ($out =~ /SMART\s+support\s+is:\s+Enabled/i) {
241                 $rv{'enabled'} = 1;
242                 }
243         elsif ($out =~ /Device.*is\+Enabled/i) {
244                 $rv{'enabled'} = 1;
245                 }
246         elsif ($out =~ /Device\s+supports\s+SMART\s+and\s+is\s+Enabled/i) {
247                 # Added to match output from RHEL5
248                 $rv{'enabled'} = 1;
249                 }
250         else {
251                 # Not enabled!
252                 $rv{'enabled'} = 0;
253                 }
254         if (!$rv{'support'} || !$rv{'enabled'}) {
255                 # No point checking further!
256                 return \%rv;
257                 }
258
259         # Check status
260         $out = &backquote_command(
261                 "$config{'smartctl'} $extra_args -H $qd 2>&1");
262         if ($out =~ /test result: FAILED/i) {
263                 $rv{'check'} = 0;
264                 }
265         else {
266                 $rv{'check'} = 1;
267                 }
268         }
269 else {
270         # Use old command format
271
272         # Check status
273         local $out = &backquote_command(
274                         "$config{'smartctl'} $extra_args -c $qd 2>&1");
275         if ($out =~ /supports S.M.A.R.T./i) {
276                 $rv{'support'} = 1;
277                 }
278         else {
279                 $rv{'support'} = 0;
280                 }
281         if ($out =~ /is enabled/i) {
282                 $rv{'enabled'} = 1;
283                 }
284         else {
285                 # Not enabled!
286                 $rv{'enabled'} = 0;
287                 }
288         if (!$rv{'support'} || !$rv{'enabled'}) {
289                 # No point checking further!
290                 return \%rv;
291                 }
292         if ($out =~ /Check S.M.A.R.T. Passed/i) {
293                 $rv{'check'} = 1;
294                 }
295         else {
296                 $rv{'check'} = 0;
297                 }
298         }
299
300 if ($config{'attribs'}) {
301         # Fetch other attributes
302         local ($lastline, @attribs);
303         local $doneknown = 0;
304         $rv{'raw'} = "";
305         open(OUT, "$config{'smartctl'} $extra_args -a $qd |");
306         while(<OUT>) {
307                 s/\r|\n//g;
308                 if (/^\((\s*\d+)\)(.*)\s(0x\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)/) {
309                         # An old-style vendor attribute
310                         $doneknown = 1;
311                         push(@attribs, [ $2, $7 ]);
312                         }
313                 elsif (/^\s*(\d+)\s+(\S+)\s+(0x\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)/) {
314                         # A new-style vendor attribute
315                         $doneknown = 1;
316                         push(@attribs, [ $2, $10 ]);
317                         $attribs[$#attribs]->[0] =~ s/_/ /g;
318                         }
319                 elsif (/^(\S.*\S):\s+\(\s*(\S+)\)\s*(.*)/ && !$doneknown) {
320                         # A known attribute
321                         local $attrib = [ $1, $2, $3 ];
322                         if ($lastline =~ /^\S/ && $lastline !~ /:/) {
323                                 $attrib->[0] = $lastline." ".$attrib->[0];
324                                 }
325                         push(@attribs, $attrib);
326                         }
327                 elsif (/^\s+(\S.*)/ && @attribs && !$doneknown) {
328                         # Continuation of a known attribute description
329                         local $cont = $1;
330                         local $ls = $attribs[$#attribs];
331                         if ($ls->[2] =~ /\.\s*$/) {
332                                 $ls->[2] .= "<br>".$cont;
333                                 }
334                         else {
335                                 $ls->[2] .= " ".$cont;
336                                 }
337                         }
338                 elsif (/ATA\s+Error\s+Count:\s+(\d+)/i) {
339                         # An error line!
340                         $rv{'errors'} = $1;
341                         }
342                 $lastline = $_;
343                 $rv{'raw'} .= $_."\n";
344                 }
345         close(OUT);
346         $rv{'attribs'} = \@attribs;
347         }
348 return \%rv;
349 }
350
351 # short_test(device, [&drive])
352 # Starts a short drive test, and returns 1 for success or 0 for failure, plus
353 # any output.
354 sub short_test
355 {
356 local ($device, $drive) = @_;
357 local $qm = quotemeta($device);
358 local $extra_args = &get_extra_args($device, $drive);
359 if (&get_smart_version() > 5.0) {
360         local $out = &backquote_logged("$config{'smartctl'} $extra_args -t short $qm 2>&1");
361         if ($? || $out !~ /testing has begun/i) {
362                 return (0, $out);
363                 }
364         else {
365                 return (1, $out);
366                 }
367         }
368 else {
369         local $out = &backquote_logged("$config{'smartctl'} $extra_args -S $qm 2>&1");
370         if ($? || $out !~ /test has begun/i) {
371                 return (0, $out);
372                 }
373         else {
374                 return (1, $out);
375                 }
376         }
377 }
378
379 # ext_test(device, [&drive])
380 # Starts an extended drive test, and returns 1 for success or 0 for failure,
381 # plus any output.
382 sub ext_test
383 {
384 local ($device, $drive) = @_;
385 local $qm = quotemeta($device);
386 local $extra_args = &get_extra_args($device, $drive);
387 if (&get_smart_version() > 5.0) {
388         local $out = &backquote_logged("$config{'smartctl'} $extra_args -t long $qm 2>&1");
389         if ($? || $out !~ /testing has begun/i) {
390                 return (0, $out);
391                 }
392         else {
393                 return (1, $out);
394                 }
395         }
396 else {
397         local $out = &backquote_logged("$config{'smartctl'} $extra_args -X $qm 2>&1");
398         if ($? || $out !~ /test has begun/i) {
399                 return (0, $out);
400                 }
401         else {
402                 return (1, $out);
403                 }
404         }
405 }
406
407 # data_test(device, [&drive])
408 # Starts offline data collection, and returns 1 for success or 0 for failure,
409 # plus any output.
410 sub data_test
411 {
412 local ($device, $drive) = @_;
413 local $qm = quotemeta($device);
414 local $extra_args = &get_extra_args($device, $drive);
415 if (&get_smart_version() > 5.0) {
416         local $out = &backquote_logged("$config{'smartctl'} $extra_args -t offline $qm 2>&1");
417         if ($? || $out !~ /testing has begun/i) {
418                 return (0, $out);
419                 }
420         else {
421                 return (1, $out);
422                 }
423         }
424 else {
425         local $out = &backquote_logged("$config{'smartctl'} $extra_args -O $qm 2>&1");
426         if ($? || $out !~ /test has begun/i) {
427                 return (0, $out);
428                 }
429         else {
430                 return (1, $out);
431                 }
432         }
433 }
434
435 =head2 get_extra_args(device, [&drive])
436
437 Returns extra command-line args to smartctl, needed for some drive type.
438
439 =cut
440 sub get_extra_args
441 {
442 local ($device, $drive) = @_;
443 if (!$drive) {
444         ($drive) = grep { $_->{'device'} eq $device }
445                         &list_smart_disks_partitions();
446         }
447 local $extra_args = $config{'extra'};
448 if ($drive && defined($drive->{'subdisk'})) {
449         $extra_args .= " -d $drive->{'subtype'},$drive->{'subdisk'}";
450         }
451 elsif ($config{'ata'}) {
452         $extra_args .= " -d ata";
453         }
454 return $extra_args;
455 }
456
457 1;
458