#!/usr/bin/perl -w
#
#  Copyright (c) 2000-2001 Silicon Graphics, Inc.  All Rights Reserved.
#
# fill2fs:
#   Fill a filesystem, or write a specified number of bits.
#   Script runs deterministically given a seed for the random number
#   generator. Will generate the same set of directories and files,
#   with the only source of variation the precise point at which the
#   filesystem fills up. When used with XFS filesystems, and when 
#   the filesize is small, XFS pre-allocation may cause the filesystem
#   to "fill up" before the actual disk space is gone. Using this
#   script repeatedly, with a delay between invocations, to account
#   for extent flushing, will allow more of the filesystem to be filled.
#
#   All generated files are checksummed and this sum is stored in the
#   filename (files are named: <sequence number>.<checksum>.data.
#   Normally script is invoked using the --list option, which causes all
#   data files to be written to standard output (--list=-) or a file
#   (--list=filename). This list can be fed into fill2fs_check, which
#   recomputes the checksums and flags any errors.
#
#   The --stddev=0 forces all files to be precisely --filesize bytes in
#   length, some other value (--stddev=val) produces filesizes with a 
#   normal distribution with standard deviation val. If not specified
#   most file sizes are set to lie within 30% of the requested file size.
#
#   fill2fs is not guaranteed to write the requested number of bytes, or
#   fill the filesystem completely. However, it and fill2 are both very
#   careful about establishing when write operations fail. When the 
#   --verbose option is used the number of bytes "actually written"
#   is guaranteed to be the number of bytes on disk.
#
#   $Id$
#

use Getopt::Long;
use File::Basename;

chomp($os = `uname`);

#
# fsinfo: get filesystem info put it into the global namespace, initialises:
#	$dev, $type, $blocks, $used, $avail, $cap, $mnt, $mnt_options
#	$fsblocks, $fsblocksize, $agblocks, $agcount, $imax_pct, $logblocks
#	$logstart, $internal
#
# if $verbose is set then output fs info to STDERR
#

sub fsinfo {
  my($file) = $_[0];

  # filesystem space and mount point
  
  $cmd = "df" if ($os =~ /IRIX/);
  $cmd = "df -P -T --block-size=512" if ($os =~ /Linux/);
  chomp($_ = `$cmd $file | tail -1`);
  $n = ($dev, $type, $blocks, $used, $avail, $cap, $mnt) = split(/ +/);
  die("df failed") if ($n != 7);

  if ($fscheck && $type ne "xfs") {
    die("Error: $progname: filesystem is not xfs")
  }

  # how was this filesystem mounted
  $_ = `grep $dev /etc/mtab`;
  @mtab = split(/ +/);
  $mnt_options = $mtab[3];

  # if we're running as root run xfs_db on the filesystem
  if ($> == 0) {
    # xfs_db: read only, use the default superblock, print everything
    die("Error: $progname: can't read device: \"$dev\"\n") if (! -r $dev);
    $_=`xfs_db -r -c sb -c p $dev`;
    # multiline matching ^$ refers to individual lines...
    ($fsblocks) = /^dblocks = (\d+)$/m;
    ($fsblocksize) = /^blocksize = (\d+)$/m;
    ($agblocks) = /^agblocks = (\d+)$/m;
    ($agcount) = /^agcount = (\d+)$/m;
    ($imax_pct) = /^imax_pct = (\d+)$/m;
    ($logblocks) = /^logblocks = (\d+)$/m;
    ($logstart) = /^logstart = (\d+)$/m;
    $internal = $logstart > 0 ? " (internal)" : "";

    $verbose && print STDERR <<"EOF"
Filesystem information:
  type=$type; device=$dev
  mount point=$mnt; mount options=$mnt_options
  percent full=$cap; size (512 byte blocks)=$blocks; used=$used; avail=$avail
  total filesystem size (fs blocks)=$fsblocks; fs block size=$fsblocksize; imax_pct=$imax_pct
  agcount=$agcount; agblocks=$agblocks; logblocks=$logblocks; logstart=$logstart$internal
EOF
  }
}


# returns numbers with a normal distribution
sub normal {
  my($mean) = $_[0];
  my($stddev) = $_[1];

  $x = -6.0;
  for ($i = 0; $i < 12; $i++) {
    $x += rand;
  }

  $x = $mean + $stddev * $x;
  return $x;
}

#
# determine script location and find fill2
#

chomp($cwd = `pwd`);
chomp($_ = `which fill2 2>&1 | head -1`);
if (-x $_) {
  # look in the path
  $fill2 = fill2;
}
else {
  # in the same directory - get absolute path
  chomp($dirname = dirname $0);
  if ($dirname =~ m!^/.*!) {
    $fill2 = $dirname . "/fill2";
  }
  else {
    # relative
    $fill2 = $cwd . "/" . $dirname . "/fill2";
  }    
  if (! -x $fill2) {
    die("Error: $progname: can't find fill2, tried \"$fill2\"\n");
  }
}

#
# process/check args
#

$progname=$0;
GetOptions("bytes=f" => \$bytes,
	   "dir=s" => \$root,
	   "filesize=i" => \$filesize,
	   "force!" => \$force,
	   "help!" => \$help,
	   "list=s" => \$list,
	   "fscheck!" => \$fscheck,
	   "percent=f" => \$percentage,
	   "seed=i" => \$seed,
	   "stddev=i" => \$stddev,
	   "sync=i" => \$sync_bytes,
	   "verbose!" => \$verbose);


# check/remove output directory, get filesystem info
if (defined $help
    || (! defined $root && @ARGV != 1)
    || (defined $root && @ARGV == 1))
{
  # newline at end of die message suppresses line number
  print STDERR <<"EOF";
Usage: $progname [options] root_dir
Options:
  --bytes=num       total number of bytes to write
  --dir=name        where to write files
  --filesize=num    set all files to num bytes in size
  --force           overwrite any existing files
  --help            print this help message
  --list=filename   store created files to filename (- for stdout)
  --percent=num     percentage of filesystem to fill
  --seed=num        seed for random number generator
  --stddev          set file size standard deviation
  --sync=num	    sync every num bytes written
  --verbose         verbose output
EOF
  exit(1) unless defined $help;
  # otherwise...
  exit(0);
    
}


#
# lots of boring argument checking
#

# root directory and filesystem info
$root = $ARGV[0] if (@ARGV == 1);
if (-e $root) {
  if (! $force) {
    die("Error: $progname: \"$root\" already exists\n");
  }
  else {
    $verbose && print STDERR "Removing \"$root\"... ";
    system("rm -rf $root");
    $verbose && print STDERR "done\n";
  }
}
chomp($root_dir = dirname $root);
fsinfo $root_dir;

# $list can be "-" for stdout, perl open ">-" opens stdout
open LIST, ">$list" if (defined $list);

# how many bytes should we write
if (defined $bytes && defined $percentage) {
  die("Error: $progname: can't specify --bytes and --percent\n");
}
# check percentage
if (defined $percentage && ($percentage < 0 || $percentage > 100)) {
  die("Error: $progname: invalid percentage\n");
}
if (! defined $bytes && ! defined $percentage) {
  $bytes = $avail * 512;
  $verbose && print STDERR <<"EOF";
Neither --bytes nor --percent specified: filling filesystem ($bytes bytes)
EOF
}
elsif (! defined $bytes) {
  $bytes = int($blocks * 512 * $percentage / 100.0);
}
if (($bytes > $blocks * 512) || (! $force && $bytes > $avail * 512))
{
  die("Error: $progname: not enough free disk space, disk is $cap full\n");
}



#
# To get fix sized files set stddev to 0. The default is to make most files
# within 30% of the requested filesize (or 4k if filesize is not specified).
# Set the standard deviation to be half of the required percentage: ~95% of
# samples lie within 2 standard deviations of the mean.
#

$filesize = 4096 if (! defined $filesize);
die("Error: $progname: --filesize must be >= 1 byte") if ($filesize < 1);
$stddev = 0.5 * 0.3 * $filesize if (! defined $stddev);
$seed = time ^ $$ if (! defined $seed);
srand $seed;
umask 0000;
mkdir $root, 0777;
chdir $root;
$total = 0;
$files = 0;
$dirs = 0;
$d = 0;
$sync_cnt = 1;

#
# fill filesystem
#

$verbose && print STDERR "Writing $bytes bytes (seed is $seed)... ";
while ($total < $bytes) {
  $r = rand(3.0);

  if (($d == 0 && $r < 0.5) || ($d > 0 && $r >= 0.0 && $r < 2.4)) {
    # create a new data file
    $n = sprintf("%04d", $names[$d]++);
    if ($stddev == 0) {
      $size = $filesize;
    }
    else {
      $size = int(normal($filesize, $stddev));
    }
    $left = $bytes - $total;
    $size = 0 if ($size < 0);
    $size = $left if ($size > $left);

    # fill2 will fail if the filesystem is full - not an error!
    $cmd = "$fill2 -d nbytes=$size,linelength=72,seed=$n -b 4k $n";
    $cmd .= " > /dev/null 2>&1" if (! $verbose);
    if (system($cmd) != 0) {
      if ($verbose) {
	warn("can't create a file - assuming filesystem is full\n");
      }
      if (-e $n && unlink($n) != 1) {
	warn("couldn't delete \"$n\"");
      }
      last;
    }
    $_ = `sum -r $n`;
    ($sum) = split(/ +/);
    $name = "$n.$sum.data";
    $cmd = "mv $n $name";	# perl rename failed earlier than using mv
    $cmd .= " > /dev/null 2>&1" if (! $verbose);
    if (system($cmd) != 0) {
      if ($verbose) {
	warn("can't rename a file - assuming filesystem is full\n");
      }
      if (-e $name && unlink($name) != 1) {
	warn("couldn't delete \"$name\"");
      }
      last;
    }
    if (defined $list) {
      chomp($_ = `pwd`);
      printf LIST ("%s/%s\n", $_, $name);
    }
    $total += $size;
    $files++;

    if (defined $sync_bytes && int($total / $sync_bytes) > $sync_cnt) {
      $sync_cnt++;
      system("sync");
    }
  }
  # note that if d==0 create directories more frequently than files
  elsif (($d == 0 && $r >= 0.5) || ($d > 0 && $r >= 2.4 && $r < 2.7)) {
    # create a new directory and descend
    $name = sprintf("%04d.d", $names[$d]++);
    if (! mkdir($name, 0777)) {
      warn("can't make a directory - assuming filesystem is full\n");
      last;
    }
    chdir($name) or die();
    $d++;
    $dirs++;
  }
  elsif ($r >= 2.7 && $r < 3.0) {
    # pop up to the parent directory same probability as descend
    die("Error: $progname: panic: shouldn't be here!") if ($d == 0);
    chdir("..") or die();
    $d--;
  }
}
# make sure we return to the original working directory
chdir($cwd) or die();
$verbose && print STDERR "done\n";
$verbose && print STDERR "$total bytes (in $files files and $dirs directories) were actually written\n";
close LIST;
exit(0) if ($total = $bytes);
exit(1) if ($total == 0);
exit(2) if ($total > 0 && $total < $bytes);

# - to sum all generated data:
#   find /home/fill/ -name \*data | xargs ls -al | awk '{total = total + $5; } END { printf("total = %d bytes\n", total); }'
# - to find any files not of the required size
#   find . -name \*data -a \! -size 4096c
# - count new files
#   find ./fill -name \*.data | wc
# - count new directories
#   find ./fill -name \*.d | wc
