6
\$\begingroup\$

Due to lack of central configuration management I am avoiding any Perl modules which would need to be installed on each of a great many servers. Result is more code than would normally be required in order to format HTML and send email as well as make system calls which are less desirable.

#!/usr/bin/perl

use strict;
use warnings;
use Sys::Hostname;
use POSIX qw(uname);

my (%fsSize, %fsFree, %fsPct, %overrides);
my ($thresh, $fh);
my $repFile = "/tmp/chkDiskResults.txt";
my $send = 0;
my $hostname = hostname();
my @uname = uname();

# Determine the OS and set the 'df' command appropriately
my $df;
if ($uname[0] =~ 'AIX') {
  $df = "df -tg";
} elsif ($uname[0] =~ 'Linux') {
  $df = "df -h";
}

# Check for an override file loading it if it exists and running
# simple checks to ensure the values are valid.
my $overrideFile = "/etc/override";
if (-e $overrideFile) {
  open($fh, '<', $overrideFile) or die "Unable to open file: $overrideFile\n $!";
  while (my $line = <$fh>) {
    my @split = split /\s+/, $line;
    unless (!$split[1] || $split[1] !~ /^[0-9]+$/) {
      $overrides{$split[0]} = $split[1];
    }
  } 
  close($fh);
}

# Execute the system 'df' command ignoring anything that isn't a
# real filesystem
# $cols[1] => Total space in GB
# $cols[3] => Free space in GB
# $cols[4] => Percent used column
# $cols[5] => Mounted on column
foreach my $line (qx[$df |grep -E -v "(Filesystem|proc|tmpfs)"])  {
  my @cols = split /\s+/, $line;
  chop($cols[1]);
  chomp($cols[3]);
  chop($cols[4]);

  # set threshold based on disk size; A 1TB disk doesn't need
  # to alert when 100GB are available
  if ($cols[1] >= 800) {
    $thresh = 98;
  } elsif ($cols[1] < 800 && $cols[1] >= 400) {
    $thresh = 96;
  } elsif ($cols[1] < 400 && $cols[1] >= 200) {
    $thresh = 94;
  } elsif ($cols[1] < 200 && $cols[1] >= 100) {
    $thresh = 92;
  } else {
    $thresh = 90;
  }

  $fsSize{$cols[5]} = $cols[1];# . "G";
  $fsFree{$cols[5]} = $cols[3];# . "G";
  $fsPct{$cols[5]} = $cols[4];
}

# Do the needful; override the thresholds if necessary; write
# offending filesystems to /tmp/chkDiskResults.txt as HTML 
# since Outlook mangles text formatting
open($fh, '>', $repFile) or die "Unable to open file: $repFile\n $!";

print $fh <<"EOF";
<html>
  <body>
    <h1>Disk usage report for $hostname</h1>
    <table width="500">
      <tr>
        <th align="left">Filesystem</th>
        <th>Size</th>
        <th>Free</th>
        <th>Percent Used</th>
      </tr>
EOF

foreach my $key (keys %fsPct) {
  my $origThresh = $thresh;
  if (exists $overrides{$key}) {
    $thresh = $overrides{$key};
  }

  if ($fsPct{$key} >= $thresh) {
    $send = 1;
    print $fh "      <tr>\n";
    print $fh <<"EOF";
        <td>$key</td>
        <td align="center">$fsSize{$key}G</td>
        <td align="center">$fsFree{$key}G</td>
        <td align="center">$fsPct{$key}%</td>
EOF
    print $fh "      </tr>\n";
  }
  $thresh = $origThresh;
}

# Close out the HTML
print $fh <<"EOF";

    </table>
  </body>
</html>
EOF

close($fh);

# Send the report; send email directly through sendmail avoiding
# any additional modules
if ($send) {
  my ($message_body, $subject, $from, $to, $cc);
  open($fh, '<', $repFile) or die "Cannot open $repFile:\n $!";
  {
    local $/;
    $message_body = <$fh>;
  }

  $subject = "Just a test";
  $from = "root\@$hostname";
  $to = 'email@address';
  #$cc = 'email@address';
  #Cc: $cc

  open(MAIL, "|/usr/sbin/sendmail -oi -t");

  print MAIL << "EOF";
Content-Type: text/html
Subject: $subject
To: $to
From: $from

$message_body
\n\n
EOF

  close(MAIL);

  unlink $repFile;
} else {
  exit;
}

The script is executed via SSH:

# ssh server "perl" < script.pl

EDIT 1

I can say that I've already found one issue: I receive an email even if there are no file systems that are at or above the threshold. The list is empty with just the headers being sent.

EDIT 2

Added code that sends the email if a file system is found which exceeds its threshold otherwise exits the script.

EDIT 3

Eliminated two system calls by using built-in Sys::Hostname and POSIX modules; Removed the $os variable and replaced it with direct use of $uname[0] established with POSIX::uname()

\$\endgroup\$
12
  • 2
    \$\begingroup\$ I'd remove your e-mail address from the code. Not just because of spammers, but because anyone trying to help might send you an e-mail by mistake. Not that there many of us perl developers left around, but you never know. \$\endgroup\$
    – ChatterOne
    Commented Apr 11, 2017 at 6:44
  • 1
    \$\begingroup\$ You could ssh from one machine and query all others with df. And df -P might give consistent results across various unix flavors. \$\endgroup\$
    – mpapec
    Commented Apr 11, 2017 at 6:49
  • \$\begingroup\$ That would certainly ensure that the columns are consistent. I'd still have to add an additional flag for AIX to display MiB instead of KiB which it is wont to do. \$\endgroup\$
    – theillien
    Commented Apr 11, 2017 at 18:34
  • \$\begingroup\$ Is /etc/override a standard system file? I tried google it but I could not find any documentation. What is the format of this file? \$\endgroup\$ Commented Apr 12, 2017 at 7:57
  • 1
    \$\begingroup\$ Not really code-review, but if you run this stuff not locally, but as part of configuration management, deployment or provisioning, you might want to look into Object::Remote. It's documentation is a bit challenging, but essentially it lets you take whatever code you want from one machine to another via SSH. As long as the other side has any Perl, you can basically run whatever you want. It will push any dependencies through the wire for you on demand. Great for one-off runs. Also see this talk by the author. \$\endgroup\$
    – simbabque
    Commented Apr 18, 2017 at 15:04

1 Answer 1

2
\$\begingroup\$
  • If you have a machine with something like a dvd reader/writer and a disk inserted, the use % will always be 100%. This is actually more common than one might think if, for example, you have a virtual machine with VirtualBox and the guest additions CD is mounted.

  • On my own machine (Ubuntu 14.04), the output of df -h is udev 3,9G 4,0K 3,9G 1% /dev none 3,9G 147M 3,8G 4% /run/shm

Which will not be parsed correctly because of the , instead of the .

As for the code itself:

  • You're not using a proper temp file. I understand not wanting to use external modules, but File::Temp is a core module and it will be there unless your perl environment is broken.
  • You don't have even a single sub and this makes the code a lot less readable.
  • You have a double negation in unless (!$split[1] || $split[1] !~ /^[0-9]+$/) {. This is the same as if ($split[1] || $split[1] =~ /^[0-9]+$/) which I think is more readable (btw, unless is the same as if not, but it's not exactly the same as if ! because of precedence).

These lines:

my @cols = split /\s+/, $line;
chop($cols[1]);
chomp($cols[3]);
chop($cols[4]);

can be replaced by

my @cols = map { substr($_, 0, length($_)-1) || 0 } split /\s+/, $line;

This part:

  if ($cols[1] >= 800) {
    $thresh = 98;
  } elsif ($cols[1] < 800 && $cols[1] >= 400) {
    $thresh = 96;
  } elsif ($cols[1] < 400 && $cols[1] >= 200) {
    $thresh = 94;
  } elsif ($cols[1] < 200 && $cols[1] >= 100) {
    $thresh = 92;
  } else {
    $thresh = 90;
  }

Is actually just:

  if ($cols[1] >= 800) {
    $thresh = 98;
  } elsif ($cols[1] >= 400) {
    $thresh = 96;
  } elsif ($cols[1] >= 200) {
    $thresh = 94;
  } elsif ($cols[1] >= 100) {
    $thresh = 92;
  } else {
    $thresh = 90;
  }
  • There is no reason to have <tr> and </tr> in a separate print.
  • You're not differentiating thresholds by filesystem, you have only one even if the filesystems have different sizes.
  • I think it would be more readable if you used only one hash to store everything and distinguish by key.

This is part of the script how I would write it. It's not complete, because it's missing the override and the sending part, but I'm sure you can figure out how to do that.

#!/usr/bin/perl

use strict;
use warnings;

use File::Temp qw(tempfile);
use Sys::Hostname;
use POSIX qw(uname);

use Data::Dumper;

my %overrides;
my $send     = 0;

my $filename = write_report_file( get_fs_data() );
print Dumper $filename; # This is the file you can send

sub get_df_command {
    my $df;
    my @uname = uname();
    if ( $uname[0] =~ 'AIX' ) {
        $df = "df -tg";
    }
    elsif ( $uname[0] =~ 'Linux' ) {
        $df = "df -h";
    }

    return $df;
}

sub get_fs_data {
    my %fs_data;
    my $df = get_df_command();
    foreach my $line (qx[$df |grep -E -v "(Filesystem|proc|tmpfs)"]) {
        $line =~ s/,/\./g;
        my @cols = map { substr( $_, 0, length($_) - 1 ) || 0 } split(/\s+/, $line);

        my $thresh;
        if ( $cols[1] >= 800 ) {
            $thresh = 98;
        }
        elsif ( $cols[1] >= 400 ) {
            $thresh = 96;
        }
        elsif ( $cols[1] >= 200 ) {
            $thresh = 94;
        }
        elsif ( $cols[1] >= 100 ) {
            $thresh = 92;
        }
        else {
            $thresh = 90;
        }

        $fs_data{ $cols[5] } = {
            total        => $cols[1],
            free         => $cols[3],
            percent_used => $cols[4],
            threshold    => $thresh,
        };
    }

    return \%fs_data;
}

sub write_report_file {
    my ($data) = @_;

    my %fs_data = %{$data};
    my $hostname = hostname();
    my ( $rep_fh, $rep_filename ) = tempfile( UNLINK => 0 ) or die "Unable to open temp file: $!";

    print $rep_fh <<"EOF";
<html>
  <body>
    <h1>Disk usage report for $hostname</h1>
    <table width="500">
      <tr>
        <th align="left">Filesystem</th>
        <th>Size</th>
        <th>Free</th>
        <th>Percent Used</th>
      </tr>
EOF

    foreach my $filesystem ( keys(%fs_data) ) {
        my $thresh = $overrides{$filesystem} // $fs_data{$filesystem}->{threshold};

        if ( $fs_data{$filesystem}->{percent_used} >= $thresh ) {
            $send = 1;
            print $rep_fh <<"EOF";
        <tr>
        <td>$filesystem</td>
        <td align="center">$fs_data{$filesystem}->{total}G</td>
        <td align="center">$fs_data{$filesystem}->{free}G</td>
        <td align="center">$fs_data{$filesystem}->{percent_used}%</td>
        </tr>
EOF
        }
    }

    print $rep_fh <<"EOF";

    </table>
  </body>
</html>
EOF

    close($rep_fh);
    return $rep_filename;
}
\$\endgroup\$
1
  • \$\begingroup\$ Thanks for the input. I had just realized the bit about the thresholds being the same regardless of the file system size and corrected it prior to reading your post. I'll work on incorporating your other suggestions. I'm sure there a few built-in modules that I could be using. I had already replaced the system calls that set $hostname and determined the OS once I discovered Sys::Hostname and POSIX. I also see what you mean about the single hash. I did wonder about the best way to do that since several hashes are a bit ugly. \$\endgroup\$
    – theillien
    Commented Apr 12, 2017 at 21:23

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.