#!/usr/bin/perl -w use strict; # ============================================================================== # xdf - eXtended Disk Free # ============================================================================== # Collect df information and format identically on all supported platforms, # particularly adding -h support ("human-readable output") to platforms # that do not have this in their native df command. Also, xdf only reports on # local filesystems, except temp filesystems are excluded by default. Sorts # by mountpoint, but other sort options are available. # ----------------------------------------------------------------------------- # Author: Jeremy Holland # ----------------------------------------------------------------------------- # Usage: xdf [--csv] [--swap] [--sort=mode] # --csv: report output in minimal CSV format, for use with other scripts # --sort: sort output by size, used, free, device, full, or mountpoint # --swap: include swap partitions and temp filesystems (normally excluded) # ----------------------------------------------------------------------------- # Rev History # 2012-02-02 - J. Holland - Initial release # 2012-02-07 - J. Holland - Added ZFS reporting for Solaris 10+ # 2012-06-09 - J. Holland - Added CSV mode for use with Big Brother monitor # 2012-09-12 - J. Holland - Added sort modes # # Known Bugs/Feature Requests # - Add support for a directory argument (as with df) that only reports on # the filesystem containing that directory # ----------------------------------------------------------------------------- use Getopt::Long; use Switch; # ------------------------------------------------------------------------------ # PROCESS COMMAND LINE AND CHECK ARGUMENTS # ------------------------------------------------------------------------------ sub USAGE { "USAGE: xdf [--csv] [--swap] [--sort=mode]\n"; } my $csv_output = 0; my $include_swap = 0; my $sort_mode = "mountpoint"; GetOptions( "csv" => \$csv_output, "swap" => \$include_swap, "sort=s" => \$sort_mode, ) or die USAGE; die USAGE if @ARGV; my %sort_comparators = ( mountpoint => sub { $a->{mountpoint}{SORT} cmp $b->{mountpoint}{SORT} }, # ascending size => sub { $b->{size}{SORT} <=> $a->{size}{SORT} }, # descending used => sub { $b->{used}{SORT} <=> $a->{used}{SORT} }, # descending free => sub { $b->{free}{SORT} <=> $a->{free}{SORT} }, # descending full => sub { $b->{full}{SORT} <=> $a->{full}{SORT} }, # descending device => sub { $a->{device}{SORT} cmp $b->{device}{SORT} }, # ascending ); my $sort_comparator = $sort_comparators{lc $sort_mode}; if (not defined $sort_comparator) { die "Error: valid sort modes are: " . (join ", ", sort keys %sort_comparators) . "\n"; } # Storage for output variables my $output = [ ]; my $mountpoint_maxlength = 0; # ------------------------------------------------------------------------------ # SET UP BASED ON ENVIRONMENT # ------------------------------------------------------------------------------ # All supported O/S's df commands output in this format (so far): my $RGXCAP_df_output_general = qr{ ^ (\S+) # CAP $1: device \s+ (\d+) # CAP $2: size \s+ (\d+) # CAP $3: used \s+ (\d+) # CAP $4: free \s+ (\d+)% # CAP $5: use % \s+ (\/.*) # CAP $6: mount point $ }x; # Set external commands and output-matching regexes based on O/S my (@df_cmd, @mount_cmd, $RGX_local_partition, $RGX_swap_partition, $RGXCAP_df_output); my @zpool_cmd = qw(/sbin/zpool list -H); # --- SOLARIS ------------------------------------------------------------------ if ("SOLARIS" eq uc $^O) { @df_cmd = qw(/bin/df -kl); @mount_cmd = qw(/sbin/mount); $RGX_local_partition = qr{ ^ /dev/dsk/ c\d+ t\d+ d\d+ s\d+ | /dev/md/dsk/ d \d{1,3} | swap }x; $RGX_swap_partition = qr{ ^ (?: swap ) }x; $RGXCAP_df_output = $RGXCAP_df_output_general; } # --- SUNOS -------------------------------------------------------------------- elsif ("SUNOS" eq uc $^O) { @df_cmd = qw(/bin/df -t 4.2); @mount_cmd = qw(/usr/etc/mount); $RGX_local_partition = qr{ ^ /dev/sd \d[a-z] }x; undef $RGX_swap_partition; $RGXCAP_df_output = $RGXCAP_df_output_general; } # --- LINUX -------------------------------------------------------------------- elsif ("LINUX" eq uc $^O) { @df_cmd = qw(/bin/df -klP); @mount_cmd = qw(/bin/mount); $RGX_local_partition = qr{ ^ /dev/.*? | tmpfs | LABEL=/tmp }x; $RGX_swap_partition = qr{ ^ tmpfs | LABEL=/tmp }x; $RGXCAP_df_output = $RGXCAP_df_output_general; } # --- UNKNOWN ------------------------------------------------------------------ else { die "Operating system $^O is not supported!\n"; } # ------------------------------------------------------------------------------ # PROCESS DF OUTPUT # ------------------------------------------------------------------------------ my @df_output = ext_command(@df_cmd); # Filter out any non-local partitions my @local_partitions = grep { /$RGX_local_partition/ } @df_output; # Remove swap partitions unless running with --swap if (defined $RGX_swap_partition and not $include_swap) { @local_partitions = grep { $_ !~ $RGX_swap_partition } @local_partitions; } # Remove duplicates (Linux reports the /tmp partition twice) @local_partitions = keys %{{ map { $_ => 1 } @local_partitions }}; # Process df output and store formatted output and sort fields. foreach my $partition (@local_partitions) { if ($partition =~ m/$RGXCAP_df_output/) { my ($device, $size, $used, $free, $full, $mountpoint) = ($1, $2, $3, $4, $5, $6); push @{$output}, { device => { SORT => $device, DISPLAY => $device }, size => { SORT => $size, DISPLAY => kb_to_readable($size) }, used => { SORT => $used, DISPLAY => kb_to_readable($used) }, free => { SORT => $free, DISPLAY => kb_to_readable($free) }, full => { SORT => $full, DISPLAY => "$full%" }, mountpoint => { SORT => $mountpoint, DISPLAY => $mountpoint }, }; if ($mountpoint_maxlength < length $mountpoint) { $mountpoint_maxlength = length $mountpoint; } } else { die "Could not parse: \"$partition\"\n"; } } # ------------------------------------------------------------------------------ # PROCESS ZFS POOLS (IF SUPPORTED) # ------------------------------------------------------------------------------ if (-x $zpool_cmd[0]) { my @zpool_output = ext_command(@zpool_cmd); if (@zpool_output > 1 or $zpool_output[0] !~ m/no pools/) { my @mount_output = ext_command(@mount_cmd); foreach my $zpool_line (@zpool_output) { # zpool output looks like (fields are tab-delimited): # pool_name X.XXG X.XXG X.XXG XX% <more fields> # size fields are total, used, free, used% my ($device, $size, $used, $free, $full) = split m/\t/, $zpool_line; my $mountpoint = "unknown"; foreach my $mount_line (@mount_output) { if ($mount_line =~ m/(\/\S*)[ ]on[ ]$device[ ]/) { $mountpoint = $1; last; } } foreach ($size, $used, $free) { s/(\d)([A-Z])/$1 $2b/; } (my $full_sort = $full) =~ s/%$//; push @{$output}, { device => { SORT => "ZPOOL:$device", DISPLAY => "ZPOOL:$device" }, size => { SORT => readable_to_kb($size), DISPLAY => $size }, used => { SORT => readable_to_kb($used), DISPLAY => $used }, free => { SORT => readable_to_kb($free), DISPLAY => $free }, full => { SORT => $full_sort, DISPLAY => $full }, mountpoint => { SORT => $mountpoint, DISPLAY => $mountpoint }, }; if ($mountpoint_maxlength < length $mountpoint) { $mountpoint_maxlength = length $mountpoint; } } } } # ------------------------------------------------------------------------------ # SORT AND PRINT THE OUTPUT # ------------------------------------------------------------------------------ my $format = "%-${mountpoint_maxlength}s %8s %8s %8s %4s %s\n"; if ($csv_output) { foreach my $output_item (sort $sort_comparator @{$output}) { print join ",", map { $_->{DISPLAY} } @{$output_item}{qw(mountpoint size used free full device)}; print "\n"; } } else { print "\n"; printf $format, qw(mountpoint size used free full device); print "-" x 80, "\n"; foreach my $output_item (sort $sort_comparator @{$output}) { printf $format, map { $_->{DISPLAY} } @{$output_item}{qw(mountpoint size used free full device)}; } print "\n"; } exit; # ===== SUBROUTINES ========================================================== # Convert kilobytes to "human readable" appropriate units sub kb_to_readable { my $kb = $_[0]; if ($kb < 900) { return "$kb Kb"; } elsif ($kb < 900_000) { return sprintf "%1.1f Mb", $kb / 1024; } elsif ($kb < 900_000_000) { return sprintf "%1.1f Gb", $kb / 1024**2; } else { return sprintf "%1.1f Tb", $kb / 1024**3; } } # Convert human readable units to raw Kb (for sorting) sub readable_to_kb { my $size_str = uc $_[0]; my ($num, $unit) = ($size_str =~ m/^([\d.]+)[ ]*(?:([BKMGTP])B?)?$/); $unit = "" if not $unit; switch ($unit) { case "" { return $num; } case "B" { return $num / 1024; } case "K" { return $num; } case "M" { return $num * 1024; } case "G" { return $num * 1024**2; } case "T" { return $num * 1024**3; } case "P" { return $num * 1024**4; } } die "Invalid strings passed to readable_to_kb()\n"; } # Execute an external command and return the ouput sub ext_command { my @command_and_args = @_; open COMMAND, "-|", @command_and_args or die "Could not execute $command_and_args[0]: $!\n"; my @output_lines = <COMMAND>; close COMMAND; my $retval = $? >> 8; if ($retval != 0) { die "$command_and_args[0] returned nonzero exit code ($retval)\n"; } return @output_lines; }