#!/usr/bin/perl
# Warn on low disk space.
# Run it from cron, so cron takes care of mailing.

# POD documentation at end, or type
#   df.pl -h
# or
#   df.pl --help

my $VERSION = 0.48;

my $max_pc = 80;
my $max_small_pc = 60;
my $min_mb = 120;

my @mount = ();

my (%mounted, @umounts, @warnings, $header);
my ($mountall, $debug, $verbose, $h, $help);

use Getopt::Long; # qw(:config debug);
use Pod::Usage;
use Data::Dumper;
use strict;

my $result = GetOptions (
                         "max-pc=f"   => \$max_pc,
                         "max-small-pc=f" => \$max_small_pc,
                         "min-mb=f" => \$min_mb,
                         "mount=s" => \@mount,
			 "mountall" => \$mountall,
                         "debug" => \$debug,
                         "verbose" => \$verbose,
                         "h|?"    => \$h,
                         "help"    => \$help,
);

pod2usage(-verbose => 1) if $h;
pod2usage(-verbose => 2) if $help;

if ($debug) {
    warn "$0 version $VERSION\n",
         "  max_pc       = $max_pc \%\n",
         "  max_small_pc = $max_small_pc \%\n",
         "  min_mb       = $min_mb MB.\n",
         "  mount        = ", join(", ", @mount), "\n",
         ;


}

foreach (`cat /etc/mtab`) {
	#my @mounted = map { if(m!^/dev/[\w]+ (\S+)!) {$1} else {} } `cat /etc/mtab`;
    if(m!^(/dev/[\w]+) (\S+)!) {
		$mounted{$1} = $2;
		$mounted{$2} = $1;
	}
}

if ($mountall) {
    my %allparts;
    map { if ( m!^(/dev/[sh]d[a-z]\d)\s.*?(\S+)$! ) {$allparts{$1}=$2} } `fdisk -l`;
    warn Dumper(\%allparts) if $debug;
    foreach my $dev (keys %allparts) {
	unless ($allparts{$dev} =~ /Extended|Solaris/) {
	    tempmount($dev);
	}
    }
}

sub tempmount {
    my $dev = shift;
    return if exists($mounted{$dev});
    if ($dev =~ m!^/dev.*/(.*)!) { #allow /dev/sdxn or /dev/disk/by-label/xxx
		my $name = '/tmp/' . $1;
		if ( -d $name || mkdir($name) ) {
	    	if (system("mount -t auto $dev $name") == 0) {
				push @umounts, $name;
		    }
		    else {
				warn "ERROR: Cannot mount '$dev' on $name\n";
		    }
		}
		else {
		    warn "ERROR: Cannot create directory $name for mounting $dev : $!\n";
		}
    }
    else {
		warn "ERROR: '$dev' looks weird\n";
    }
}

foreach my $m (@mount) {
    next if (exists $mounted{$m});
    print "Trying to open '$m' or to mount it\n" if $debug;
    if (opendir(D, $m)) {
        # opening should have triggered automount. we can close it straight away
        closedir(D);
	}
    else {
		tempmount($m);
    }
}


my @df = `df -T -x tmpfs -B1000`; # -m sets unit to Megabytes -B1000 to 1000bytes blocks

foreach (@df) {
    if (/^Filesystem/) {
		s/(\S+-blocks)/sprintf('%'.length($1).'s', 'Size')/e;
        $header = $_;
        next;
    }

    #Filesystem    Type   1M-blocks      Used Available Use% Mounted on
    my ($Filesystem, $Type, $Size, $Used, $Avail, $Use, $Mounted) = split /\s+/;

    $Use =~ s/\%//;
    if (    ($Size*1000 > $min_mb and $Use > $max_pc)
         or ($Size*1000 > $min_mb and $Avail < $min_mb )
         or ($Size*1000 < $min_mb and $Use > $max_small_pc )
       )
    {
		# make it human-readable
		warn "Orig: $_" if ($debug);
		#1 while ( s/(\s+)(\d+)(\s)/readable($2, length($1)) . $3/e );
		while ( /(\s+)(\d+) / ) {
			my ($pad, $num) = ($1, $2);
			my $hr=readable($num, length($pad)+length($num)-2); # 1=size of unit string
			s/$pad$num/$hr/;
		}
		warn "New : $_" if ($debug);
		push @warnings, $_;
    }
    elsif ($debug) {
        print "OK $Filesystem : Use=$Use \%, Avail=$Avail\n";
    }
}

foreach (@umounts) {
	system("umount $_") == 0
		or warn "Error umounting $_\n";
}

print scalar(@warnings), " warnings\n" if $debug;

exit 0 unless (@warnings);

print $header;

foreach (@warnings) {
        chomp;
        my ($dev) = m!^(/dev\S+)!;
        my $label = `e2label $dev 2>/dev/null`;
        chomp($label);
        if ($label) {
            $label = " ( Label = '$label' )";
        }
#/dev/sdb5     ext3        2819      2367       309  89% /tmp/sdb5 ( Label = 'bak-var' )
        printf "%-68s %-s\n", $_, $label;
}

sub readable {
	# prettyBytes http://www.perlmonks.org/?node_id=378538,
	# adapted for neg numbers and 1000 vs 1024, and base in KB
	my ($size, $padding) = @_;
	foreach ('K','M','G','T','P') {
		return sprintf("%$padding.2f $_", $size) if abs($size) < 1000;
		$size /= 1000;
	}
}


__END__

=head1 NAME

df.pl - Report disks with insufficient disk space

=head1 DESCRIPTION

df.pl calls the system's df command and parses it. It then list any filesystems which
are dangerously close to full. The options determine what is considered too full.

=head1 SYNOPSIS

df.pl [options]

=head1 OPTIONS

 Options:
    -?, -h          Brief help
    --help          Full documentation
    --max-pc        Maximum percentage of disk capacity used
    --max-small-pc  Maximum percentage of disk capacity used, but for small disks
    --min-mb        Minimum free megabytes and also minimum size under which a disk is considered small
    --mount         Automount points which need to be monted for the disk to be checked
    --mountall      Check all partitions found by cat /proc/partitions
    --debug         Print what is going on
    --verbose       Complain about missing directories given in --mount option


=head1 COMMAND LINE OPTIONS

=over 8

=item B<-?> or B<-h>

Print a brief help message and exits.

=item B<--help>

Print the manual page and exits.

=item B<--max-pc>

Maximum percentage of disk capacity used. If the use percentage is higher, a warning is printed.

Default: 80 %

=item B<--max-small-pc>

For very small disks, a different maximum percentage applies. If the use percentage of a small
disk is higher than --max-small-pc, a warning is printed.

A disk is considered "small" if it's size is smaller than --min-mb.

Default: 60 %

=item B<--min-mb>

This option serves 2 purposes:

It gives the size under which a disk is considered "small", so that the maximum use percentage
which applies to it is --max-small-pc instead of --max-pc
.
It is a minimum size that must remain free on normal disks, regardless of the value of --max-pc.

Default: 120 MB.

=item B<--mount>

The system's df utility will only check mounted filesystems. When using autofs, most disks may not be mounted
at the time the disk free check runs. This will force them to be mounted by first trying to open the
directory.

Multiple paths can be given by repeating the option.

If opening the directory fails, an error is printed if either B<--debug> or the B<--verbose> option was used.
Otherwise, errors are silently ignored.

Example:

    df.pl --mount /media/backup --mount /media/usb


=item B<--mountall>

Find all partitions using cat /proc/partitions, and mount all which are not mounted already
to chek them.

Example:

    df.pl --mountall


=item B<--debug>

Print debugging messages. Also print errors when trying to open directories given with the --mount option

=item B<--verbose>

Print errors when trying to open directories given with the --mount option

=back

=head1 AUTHOR

mi at alma.ch, 2008, 2009


=cut


#print Dumper(\$debug, \$max_pc, \$max_pc_low, \$min_mb, \@mount, \$help), "\n";
