Subversion Repositories sysadmin_scripts

Rev

Rev 151 | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
151 rodolico 1
#! /usr/bin/env perl
2
 
3
# getDriveInfo
4
# 
5
# Script to get information on drives for all Unix systems. It will retrieve 
6
# manufacturing information, SMART information and, with software RAID or 
7
# ZFS file systems, membership.
8
#
9
# Also looks for a file, /etc/drive_bays to determine which physical bay a
10
# drive is installed in (tab separated, first line header's 'bay' and 'serial'
11
# followed by the data).
12
#
13
# generates a tab separated table of all values retrieved
14
#
15
# Copyright (c) 2024, Daily Data, Inc.
16
# 
17
# Redistribution and use in source and binary forms, with or without
18
# modification, are permitted provided that the following conditions
19
# are met:
20
# 
21
#   Redistributions of source code must retain the above copyright notice,
22
#   this list of conditions and the following disclaimer.
23
#   Redistributions in binary form must reproduce the above copyright notice,
24
#   this list of conditions and the following disclaimer in the documentation
25
#   and/or other materials provided with the distribution.
26
# 
27
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
28
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
29
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
30
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
31
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 
32
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
33
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
34
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
35
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
36
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
37
# THE POSSIBILITY OF SUCH DAMAGE.
38
#
39
# v0.1 RWR 20240708
40
# Added ability to read tab separated list of bays and serial numbers 
41
# in /etc/drive_bays to populate the physical bay's a drive is in. Format
42
# (\t means "tab"):
43
# bay\tserial
44
# 0\tserial number of drive 0
45
# 1\tserial number of drive in bay 1
46
# bay can be any string, so in one case we use 'internal' as the bay number
47
 
48
use strict;
49
use warnings;
50
 
51
use Data::Dumper;
52
 
53
my $TESTING = 0;
54
 
55
my $driveDefinitions; # hashref is global that everything will put info into
56
 
57
my $ignoreDriveTypes = { # a list of fake drive "types" that we just ignore.
58
         'VBOX HARDDISK' => 1, # virtualbox
59
         'CTLDISK' => 1,   # iSCSI drive
60
         };
61
 
62
# check for commands we want to run, so some of the must be on the
63
# system
64
my $commands = {
65
                  'smartctl' => '',
66
                  'lsblk' => '',
67
                  'geom' => '',
68
                  'zpool' => '',
69
                  'mdadm' => '',
70
                  'glabel' => '',
71
 
72
               };
73
 
74
 
75
# remove leading and trailing spaces from string
76
sub trim {
77
 my $value = shift;
78
 $value =~ s/^\s+|\s+$//g if $value;
79
 return $value;
80
}
81
 
82
# cleanUp - passed a delimiter and a string, does the following (delimiter can be '')
83
#           chomps the string (removes trailing newlines)
84
#           removes all text BEFORE the delimiter, the delimiter, and any whitespace
85
#           thus, the string 'xxI Am x  a weird string' with a newline will become
86
#           'a weird string' with no newline
87
# will also look for single and double quotes surrounding entire string and remove them
88
# if they exist
89
 
90
sub cleanUp {
91
   my ($delimiter, $text) = @_;
92
   chomp $text;
93
   if ( $delimiter && $text =~ m/[^$delimiter]*$delimiter\s*(.*)/ ) {
94
      $text = $1;
95
   }
96
   if ( $text =~ m/^'(.*)'$/ ) {
97
      $text = $1;
98
   }
99
   if ( $text =~ m/^"(.*)"$/ ) {
100
      $text = $1;
101
   }
102
   return $text;
103
}
104
 
105
 
106
# checks if a command is valid on this system. If so, returns the full path to it
107
# else, returns empty string.
108
sub validCommandOnSystem {
109
   my $command = shift;
110
   $command = `which $command 2> /dev/null`;
111
   chomp $command;
112
   return -x $command ? $command : '';
113
}
114
 
115
# returns the operation system, all lower case
116
sub getOperatingSystem {
117
   return lc &cleanUp('', qx(uname -s));
118
}   
119
 
120
 
121
# acquires information using lsblk (Linux), if it is available
122
# uses global %driveDefinitions to store the results
123
sub lsblk {
124
   eval ( 'use JSON qw( decode_json );' );
125
   if ( $@ ) {
126
      warn "Could not load JSON library\n";    
127
      return;
128
   }
129
   my $output;
130
   if ( $TESTING ) {
131
      $output = `cat lsblk`; 
132
   } else {
133
      $output = qx'lsblk -bdJO 2>/dev/null';
134
      # older versions do not have the O option, so we'll run it without
135
      $output = qx'lsblk -bdJ 2>/dev/null' if $?;
136
   }
137
   my $drives = decode_json( join( '', $output ) );
138
   $drives = $drives->{'blockdevices'};
139
   while ( my $thisDrive = shift @{$drives} ) {
140
      if ( $thisDrive->{'type'} eq 'disk' ) {
141
         my $key = '/dev/' . $thisDrive->{'name'};
142
         $driveDefinitions->{$key}->{'capacity'} = $thisDrive->{'size'};
143
         $driveDefinitions->{$key}->{'model'} = defined $thisDrive->{'model'} ? $thisDrive->{'model'} : '';
144
         $driveDefinitions->{$key}->{'serial'} = defined( $thisDrive->{'serial'} ) ? $thisDrive->{'serial'} : '' ;
145
         $driveDefinitions->{$key}->{'sector'} = '';
146
      }
147
   }
148
}
149
 
150
# Acquires disk information if it available (FreeBSD)
151
# uses global %driveDefinitions to store the results
152
sub geom {
153
   my %disks;
154
   my @data = `geom disk list`;
155
 
156
   my %conversion = (
157
      'ident' => 'serial',
158
      'descr' => 'model',
159
      'Mediasize' => 'capacity',
160
      'Sectorsize' => 'sector'
161
      );
162
 
163
   my $currentDisk = '';
164
   for ( my $i = 0; $i < @data; $i++ ) {
165
      if ( $data[$i] =~ m/^Geom name:\s+(.*)/ ) {
166
         $currentDisk = &fixDisk($1);
167
      } elsif ( $data[$i] =~ m/^\s+([^:]+):\s+(.*)/ ) {
168
        $driveDefinitions->{$currentDisk}->{$conversion{$1}} = $2 if $conversion{$1};
169
      }
170
   }
171
   return 1;
172
}
173
 
174
# determines what to call to get drive definitions
175
sub getDriveDefinitions {
176
   if ( $commands->{'lsblk'} ) { # must be a Linux system
177
      &lsblk();
178
   } elsif ( $commands->{'geom'} ) { # FreeBSD
179
      &geom();
180
   } else {
181
      die "Could not locate lsblk or geom to get list of disks, aborting\n";
182
   }
183
   return 1;
184
}
185
 
186
# Actual call to smartctl
187
sub getSmartInformationReport {
188
   my ($drive, $type) = @_;
189
   $type = '' if ( $type =~ m/scsi/ );
190
   my @report = `smartctl -j -a $drive $type`;
191
   chomp @report;
192
   my %reportHash;
193
   for ( my $i = 0; $i < @report; $i++ ) {
194
      if ( $report[$i] =~ m/^(.*):(.*)$/ ) {
195
         $reportHash{$1} = trim($2);
196
      }
197
   }
198
   return \%reportHash;
199
} # getSmartInformationReport
200
 
201
 
202
 
203
sub getSmartInformation {
204
   my ( $drive,$type ) = @_;
205
   # my $report = &getSmartInformationReport( $drive, $type );
206
   $type = $type eq 'scsi' ? '' : "-t $type";
207
   my $report = &getJson( "smartctl -j $type -a $drive" );
208
   #print Dumper( $report ); die;
209
   # this is what we look for in the smart report
210
   $driveDefinitions->{$drive}->{'make'} = defined( $report->{'model_name'} ) ? $report->{'model_name'} : '';
211
   $driveDefinitions->{$drive}->{'model'} = defined( $report->{'model_family'} ) ? $report->{'model_family'} : '';
212
   $driveDefinitions->{$drive}->{'serial'} = defined( $report->{'serial_number'} ) ? $report->{'serial_number'} : '';
213
   $driveDefinitions->{$drive}->{'capacity'} = defined(    $report->{'user_capacity'}->{'bytes'} ) ?    $report->{'user_capacity'}->{'bytes'} : '';
214
   $driveDefinitions->{$drive}->{'sector'} = defined( $report->{'logical_block_size'} ) ? $report->{'logical_block_size'} : '';
215
   $driveDefinitions->{$drive}->{'rotation'} = defined( $report->{'rotation_rate'} ) ? $report->{'rotation_rate'} : '';
216
 
217
   $driveDefinitions->{$drive}->{'reboots'} = defined( $report->{'power_cycle_count'} ) ? $report->{'power_cycle_count'} : '';
218
   $driveDefinitions->{$drive}->{'temperature'} = defined( $report->{'temperature'}->{'current'} ) ? $report->{'temperature'}->{'current'} : '';
219
   $driveDefinitions->{$drive}->{'form_factor'} = defined( $report->{'form_factor'}->{'name'} ) ? $report->{'form_factor'}->{'name'} : '';
220
   $driveDefinitions->{$drive}->{'power_on_hours'} = defined( $report->{'power_on_time'}->{'hours'} ) ? $report->{'power_on_time'}->{'hours'} : '';
221
 
222
   my $toCheck = {
223
      '241' => 'lba_write',
224
      '233' => 'wearout',
225
      '9' => 'hours_on',
226
      '12' => 'power_cycle'
227
   };
228
 
229
   # make sure there are (blank) entries for everything
230
   foreach my $this ( keys %$toCheck ) {
231
      $driveDefinitions->{$drive}->{$toCheck->{$this}} = '';
232
   }
233
   # Now, fill in when we can find it
234
   foreach my $entry ( @{$report->{'ata_smart_attributes'}->{'table'}} ) {
235
      if ( defined( $toCheck->{$entry->{'id'}} ) ) {
236
         $driveDefinitions->{$drive}->{$toCheck->{$entry->{'id'}} . '_raw' } = $entry->{'raw'}->{'value'};
237
         $driveDefinitions->{$drive}->{$toCheck->{$entry->{'id'}} } = $entry->{'value'};
238
      }
239
   }
240
 
241
 
242
   return 1;
243
   my %keys = ( 
244
                  'Rotation Rate' => { 
245
                                          'tag' => 'rotation',
246
                                          'regex' => '(.*)'
247
                                      },
248
               );
249
   foreach my $key ( keys %keys ) {
250
      if ( defined( $report->{$key} ) && $report->{$key} =~ m/$keys{$key}->{'regex'}/ ) {
251
         $driveDefinitions->{$drive}->{$keys{$key}->{'tag'}} = $1;
252
      }
253
   }
254
   return 1;
255
}
256
 
257
# reads a JSON command and decodes it
258
sub getJson {
259
   my $command = shift;
260
   eval ( 'use JSON qw( decode_json );' );
261
   if ( $@ ) {
262
      warn "Could not load JSON library when processing [$command]\n";    
263
      return;
264
   }
265
   my $output = qx/$command/;
266
   return decode_json( join( '', $output ) );
267
}
268
 
269
 
270
sub getSmart {
271
   # by running scan, we get the type of drive which we are looking at
272
   #my %allDrives = map { $_ =~ '(^[a-z0-9/]+)\s+(.*)\#'; ($1,$2) } `smartctl --scan`;
273
   my $drives = getJson( 'smartctl -j --scan' );
274
   $drives = $drives->{'devices'};
275
   my %allDrives;
276
   foreach my $thisDrive ( @$drives ) {
277
      $allDrives{$thisDrive->{'name'} } =  $thisDrive->{'type'} if $thisDrive->{'type'} ne 'atacam';
278
   }
279
 
280
   # Add smart information to the definitions,
281
   foreach my $thisDrive ( sort keys %allDrives ) {
282
      $driveDefinitions->{$thisDrive}->{'type'} = $allDrives{$thisDrive};
283
      &getSmartInformation( $thisDrive, $driveDefinitions->{$thisDrive}->{'type'} );
284
      $driveDefinitions->{$thisDrive}->{'capacity'} =~ s/,//g;
285
   }
286
   return 1;
287
}
288
 
289
sub mdadm {
290
   my @mdstat;
291
   if ( $TESTING ) {
292
      @mdstat = `cat mdstat`;
293
   } else {
294
      @mdstat = `cat /proc/mdstat`;
295
   }
296
   for ( my $line = 0; $line < @mdstat; $line++ ) {
297
      if ( $mdstat[$line] =~ m/^(md\d+)[\s:]+active (raid\d+)\s(.*)$/ ) { # this is the definition of an MD
298
         my $md = $1;
299
         my $raidLevel = $2;
300
         my @drives = split( /\s/, $3 );
301
         foreach my $currentDisk ( @drives ) {
302
            $currentDisk =~ m/^([a-z]+)(\d?)\[(\d+)\]/;
303
            my $disk = &fixDisk( $1 );
304
            my $component = $3;
305
            $driveDefinitions->{$disk}->{'vdev_type'} = $raidLevel;
306
            $driveDefinitions->{$disk}->{'pool'} = $md;
307
            $driveDefinitions->{$disk}->{'component'} = $3;
308
         }
309
      } # if it is a RAID member
310
   } # for $line
311
   return 1;
312
}
313
 
314
# some BSD systems will have partitions labeled before they
315
# are put into a zfs file system. This will build a translation
316
# for them
317
# returns 'label' => 'device'
318
# 'gptid/c73b7eae-f242-11ed-b0df-ecf4bbdad680' => 'da0p1'
319
sub glabel {
320
   my $labels;
321
   my @drives = `glabel status -s`;
322
   for ( my $i = 0; $i < @drives; $i++ ) {
323
      my @fields = split( /\s+/, $drives[$i] );
324
      $labels->{$fields[0]} = '/dev/' . $fields[2];
325
   }
326
   return $labels;
327
}
328
 
329
# ensure /dev/ in front of disk name   
330
sub fixDisk {
331
   my $disk = shift;
332
   my $partition = '';
333
   if ( $disk =~ m|^(/dev/)?([a-z]{2}\d+)p(\d+)$| ) { # BSD style
334
      $disk = "/dev/$2";
335
      $partition = $3;
336
   } elsif ( $disk =~ m|^(/dev/)?([a-z]{3})(\d+)$| ) { # linux style
337
      $disk = "/dev/$2";
338
      $partition = $3;
339
   }
340
   $disk = '/dev/' . $disk unless $disk =~ m/\/dev\//;
341
   return $disk
342
}
343
 
344
sub zfs {
345
   # grab a list of all pools and associated drives
346
   my @zpool = `zpool status`;
347
   # grab label to drive information
348
   my $labels = &glabel();
349
   my $i=0;
350
   while ( $i < @zpool ) {
351
      # bypass everything up to the pool: flag
352
      while ( $i < @zpool && $zpool[$i] !~ m/^\s*pool:\s*(\S+)/ ) {
353
         $i++;
354
      }
355
      last unless $i < @zpool;
356
      $zpool[$i] =~ m/^\s*pool:\s*(\S+)/;
357
      my $currentPool = $1;
358
      # bypass everything up to the headers
359
      while ( $i < @zpool && $zpool[$i] !~ m/^(\s+)NAME\s+STATE/ ) {
360
         $i++;
361
      }
362
      $zpool[$i] =~ m/^(\s+)NAME\s+STATE/;
363
      # the number of spaces preceding NAME is the number of spaces preceding the pool name
364
      my $poolNameIndent = $1;
365
      $i++;
366
      # The final loop will go through all the items which are in the pool configurations
367
      # and parse them out.
368
      my $currentComponent = '';
369
      my $poolType = '';
370
      my $poolTypeIndent = '';
371
      my $poolDeviceIndent = '';
372
      while ( $zpool[$i] =~ m/^$poolNameIndent(.*)/ ) {
373
         my $line = $1;
374
         if ( $line =~ m/^([a-zA-Z0-9_-]+)/ ) { # this is a pool name
375
            $currentComponent = $1;
376
         } elsif ( ! $poolTypeIndent && $line =~ m/^(\s+)([a-zA-Z0-9_-]+)/ ) {
377
            $poolTypeIndent = $1;
378
            $poolType = $2;
379
         } elsif ( $line =~ m/^$poolTypeIndent([a-zA-Z0-9_-]+)/ ) {
380
            $poolType = $1;
381
         } elsif ( ! $poolDeviceIndent && $line =~ m/^(\s+)([a-zA-Z0-9_\/-]+)/ ) {
382
            my $disk = &fixDisk( $labels->{$2} ? $labels->{$2} : $2 );
383
            $poolDeviceIndent = $1;
384
            $driveDefinitions->{$disk}->{'vdev_type'} = $poolType;
385
            $driveDefinitions->{$disk}->{'pool'} = $currentPool;
386
            $driveDefinitions->{$disk}->{'component'} = $currentComponent;
387
         } elsif ( $line =~ m/^$poolDeviceIndent([a-zA-Z0-9_\/-]+)/ ) {
388
            my $disk = &fixDisk($labels->{$1} ? $labels->{$1} : $1);
389
            $driveDefinitions->{$disk}->{'vdev_type'} = $poolType;
390
            $driveDefinitions->{$disk}->{'pool'} = $currentPool;
391
            $driveDefinitions->{$disk}->{'component'} = $currentComponent;
392
         }
393
         $i++;
394
      } # while
395
   } # while
396
   return 1;
397
}
398
 
399
sub getUsage {
400
   foreach my $drive ( keys %$driveDefinitions ) {
401
      $driveDefinitions->{$drive}->{'vdev_type'} = '';
402
      $driveDefinitions->{$drive}->{'pool'} = '';
403
      $driveDefinitions->{$drive}->{'component'} = '';
404
   }
405
   if ( $TESTING || $commands->{'mdadm'} ) {
406
      mdadm();
407
   } elsif ( $commands->{'zpool'} ) {
408
      zfs();
409
   }
410
 
411
   return 1;
412
}
413
 
414
# if /etc/drive_bays exists and is in proper format
415
# include the location of each drive
416
sub getDriveBays {
417
   my $driveBays = ();
418
   if ( -f '/etc/drive_bays' ) {
419
      open DATA, "</etc/drive_bays";
420
      my $line = <DATA>;
421
      while ( my $line = <DATA> ) {
422
         chomp $line;
423
         next unless $line;
424
         my ($bay, $serial) = split( "\t", $line );
425
 
426
         $driveBays->{$serial} = $bay;
427
      }
428
   }
429
   foreach my $drive ( keys %{$driveDefinitions} ) {
430
      $driveDefinitions->{$drive}->{'bay'} = defined( $driveBays->{$driveDefinitions->{$drive}->{'serial'}} ) ? $driveBays->{$driveDefinitions->{$drive}->{'serial'}} : '';
431
   }
432
   return 1;
433
}
434
 
435
 
436
sub report {
437
   my @headers;
438
   my @report;
439
 
440
   for my $drive ( sort keys %$driveDefinitions ) {
441
      my @line;
442
      # don't print iSCSI definitions
443
      next if defined( $driveDefinitions->{$drive}->{'Transport protocol'} ) && $driveDefinitions->{$drive}->{'Transport protocol'} eq 'ISCSI';
444
      #also, blow off our ignored types
445
      next if ( defined( $driveDefinitions->{$drive}->{'Model'} ) && defined( $ignoreDriveTypes->{ $driveDefinitions->{$drive}->{'Model'} }  ) );
446
 
447
      # remove comma's from capacity
448
      $driveDefinitions->{$drive}->{'Capacity'} =~ s/,//g if $driveDefinitions->{$drive}->{'Capacity'};
449
      push @line, $drive;
450
      unless ( @headers ) {
451
         # for some reason, doing this the fast way (with keys) did not work. some of the keys were joined together
452
         # so, doing it this way.
453
         push @headers, 'drive';
454
         foreach my $header ( sort keys %{$driveDefinitions->{$drive}} ) {
455
            $header =~ s/[^a-z0-9_]//gi;
456
            push @headers, $header;
457
         }
458
         #@headers = sort keys %{$driveDefinitions->{$drive}} unless @headers;
459
         #unshift @headers,'drive';
460
      }
461
      foreach my $key ( sort keys %{$driveDefinitions->{$drive}} ) {
462
         push @line,$driveDefinitions->{$drive}->{$key};
463
      }
464
      push @report, join( "\t", @line );;
465
   }
466
   unshift @report,join ("\t",@headers);
467
   return join( "\n", @report ) . "\n";
468
   #return Dumper( $driveDefinitions );;
469
}
470
 
471
# check the commands for validity
472
foreach my $command ( keys %$commands ) {
473
   $commands->{$command} = &validCommandOnSystem( $command );
474
}
475
 
476
my $os = &getOperatingSystem();
477
 
478
# get the drives in the machine with minimal information
479
&getDriveDefinitions( $driveDefinitions );
480
 
481
# figure out what the drives are used for, ie mdadm or zfs or something
482
&getUsage( );
483
 
484
&getSmart();
485
 
486
&getDriveBays();
487
 
488
print &report( $driveDefinitions );
489
 
490
1;