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()
1 Answer 1
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
isudev 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 asif ($split[1] || $split[1] =~ /^[0-9]+$/)
which I think is more readable (btw,unless
is the same asif not
, but it's not exactly the same asif !
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;
}
-
\$\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 discoveredSys::Hostname
andPOSIX
. 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– theillien2017年04月12日 21:23:37 +00:00Commented Apr 12, 2017 at 21:23
Explore related questions
See similar questions with these tags.
df
. Anddf -P
might give consistent results across various unix flavors. \$\endgroup\$/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\$