# -*-perl-*-

# $Id: AfsBits.plm,v 1.21 2011/11/03 17:45:27 koczan Exp $

# Any afs dependencies go here.

# $Log: AfsBits.plm,v $
# Revision 1.21  2011/11/03 17:45:27  koczan
# proper setquota arguments for RHEL 6 and beyond
#
# Revision 1.20  2011/10/08 17:38:54  koczan
# Proper dependencies
#
# Revision 1.19  2011/10/08 16:27:13  koczan
# system-independent paths (depend mechanism, damn you)
#
# Revision 1.18  2011/08/30 19:59:45  koczan
# System calls shouldn't be PATH-dependent
#
# Revision 1.17  2010/03/22 22:52:14  koczan
# also check main::donothing
#
# Revision 1.16  2010/03/22 22:50:37  koczan
# Make afsretire --donothing actually do nothing
#
# Revision 1.15  2010/03/16 21:00:48  koczan
# *** empty log message ***
#
# Revision 1.14  2010/03/10 17:47:45  koczan
# File::Path and mkpath was causing problems...let's go back to mkdir syscall
#
# Revision 1.13  2010/03/09 21:39:16  koczan
# let's make createDest not crap out when a directory already exists
#
# Revision 1.12  2009/11/11 22:38:44  koczan
# forcibly flush directories from the cache
# more info whenever a unMountVolume fails
#
# Revision 1.11  2008/12/16 20:30:34  koczan
# native perl mkdir instead of syscall
#
# Revision 1.10  2008/12/10 20:21:48  koczan
# Error message fixes
# Re-work the mount/unmount logic to avoid race conditions
#
# Revision 1.9  2008/12/05 21:43:58  koczan
# Fix the race condition causing sushi volumes to get unmounted at bad times.
#
# Revision 1.8  2007/02/22 20:48:21  thomas
# [CSL #323651] sushi "big atom" fix
#
# Revision 1.7  2004/02/24 15:02:03  thomas
# commit before 0.3.12 fork
#
# Revision 1.6  2003/06/12 21:57:33  thomas
# final save before 0.3.11 branch
#
# Revision 1.5  2003/06/09 14:45:11  thomas
# schema version of database
#
# Revision 1.4  2003/06/04 17:30:53  thomas
# *** empty log message ***
#
# Revision 1.3  2002/07/10 14:01:28  thomas
# sushi-0.2.4
#
# Revision 1.2  2002/07/08 15:56:32  thomas
# 0.2.3 release
#
# Revision 1.1.1.1  2002/05/21 14:13:28  thomas
# sushi - backup to disk
#

package Sushi::AfsBits;

use strict;
use Data::Dumper;
#use File::Path;

my $VOS     = '/s/sushi-0.3.19/depend/bin/vos';
my $FS      = '/s/sushi-0.3.19/depend/bin/fs';
my $PTS     = '/s/sushi-0.3.19/depend/bin/pts';
my $QUOTA   = 100 * 1024 * 1024 * 1024;   # 100 Gig
my @AFSACL  = (
	       "group:labstaff", "all",
	       "sushi",          "all",
	       "system:anyuser", "l"
	      );
my $AFSDF   = '/s/std/bin/afsdf';


# Mount anything I need here
my $MNTDIR = "/s/sushi/mountpoints";

# This ref holds the lab-sushi partitions.  I increment $QUOTA for each
# backup volume I create to figure out where the next volume goes.  Put each
# volume on the most % free server.
#
my @afsdf = ();

###############################################################################
#
#  Create volumes to handle the requested number of free gigabytes

sub createFreeSpace {
  my $gigs = shift;
  my $clearold = shift;
  my $sth;

  if ($clearold) {
    $::log->log (1, "Marking partial volumes unavailable...\n");
    if (!defined (my $rows = $::sudbh->do ("update afsvol_freespace set available = false where available and used_bytes>0"))) {
      $::log->error ("AfsBits::createFreeSpace: update partially used volumes failed\n");
      return 0;
    } else {
      $::log->log (1, "AfsBits::createFreeSpace: Updated $rows rows.\n");
    }
  }

  # Count non-allocated volumes as good to go
  if (!defined ($sth = $::sudbh->prepare ("select total_bytes from afsvol_freespace where available and used_bytes = 0"))) {
    $::log->error ("AfsBits::createFreeSpace: select failed\n");
    return 0;
  }
  if (!$sth->execute) {
    $::log->error ("AfsBits::createFreeSpace: execute failed\n");
    return 0;
  }
  while (my $row = $sth->fetchrow_hashref) {
    $gigs -= $row->{total_bytes}/(1024*1024*1024);
  }

  while ($gigs>0) {
    my ($serv, $part) = (undef, undef);
    my ($host, $destroot);

    ($serv, $part) = getServPart($QUOTA);

    if (!defined ($serv) || !defined ($part)) {
      $main::log->error ("AfsBits::createFreeSpace: Failed to find new serv/part\n");
      return 0;
    }

    # Get Volume name
    my $volume_name = "b.vol." . time;
    sleep 1;

    Sushi::Misc::runCmd ($VOS, "create", "-v", $serv, $part, $volume_name);

    my $usehalf = $QUOTA/2;

    if (!defined (my $rows = $::sudbh->do ("insert into afsvol_freespace (volume_name, available, total_bytes, used_bytes, deleted) values (" . $::sudbh->quote ($volume_name) . ", true, $usehalf, 0, false)"))) {
      $::log->error ("AfsBits::createFreeSpace: record insert failed\n");
      return 0;
    }

    my $mountpath = mountVolume ($volume_name, "createFreeSpace:$$");
    Sushi::Misc::runCmd ($FS, "setquota", $mountpath, 0);
    Sushi::Misc::runCmd ($FS, "setacl",   $mountpath, @AFSACL);
    unMountVolume ($volume_name, "createFreeSpace:$$");

    $::log->log (2, "AfsBits::createFreeSpace: Created volume $volume_name (" . ($usehalf/1024/1024) . "MB)\n");

    $gigs -= ($usehalf)/(1024*1024*1024);
  }
  $::sudbh->commit;
  return 1;
}

###############################################################################

sub getServPart {
  my $quota = shift;

  $#afsdf>-1 or &readAfsdf;
  $#afsdf>-1 or return (undef, undef, undef);
  my ($free, $sp, $href) = (0, undef, undef);
  foreach $href (@afsdf) {
    if ($href->{pfree}>0 && $href->{psize}>0 &&
	$href->{pfree}/$href->{psize}>$free) {
      $free = $href->{pfree}/$href->{psize};
      $sp = $href;
    }
  }
  defined $sp or return (undef, undef, undef);
  $sp->{pfree} -= $quota;

  return ($sp->{serv}, $sp->{part});
}

###############################################################################

sub readAfsdf {
  my $fd = new FileHandle "$AFSDF -c lab-sushi|";
  while (defined ($_=$fd->getline)) {
    if (/lab-sushi\s+\S+\s+(\S+)\s+(\S+)\s+(-?\d+)\s+(-?\d+)\s+-?\d+\%\s*$/) {
      push @afsdf, {
		    serv => $1,
		    part => $2,
		    psize => $3 * 1048576,
		    pfree => $4 * 1048576,
		   };
    }
  }
  undef $fd;
}

###############################################################################
# Find a destination volume for this atom
# [CSL #323651] sushi "big atom" fix
#    let one very large atom be allocated to an empty destination afs volume.

sub allocVolume {
  my $atom = shift;

  ref ($atom) or return undef;
  defined $atom->{estimated_size} or return undef;

  my $esize = $atom->{estimated_size};
  my $cmd = "select * from afsvol_freespace where available and ( used_bytes + $esize < total_bytes or used_bytes = 0 ) order by volume_name for update";
  $::log->log (4, "AfsBits::allocVolume: $cmd\n");
  my $sth = $::sudbh->prepare ($cmd);
  if (!$sth->execute) {
    $::log->error ("AfsBits::allocVolume: failed $cmd!\n");
    return undef;
  }
  while (defined (my $row = $sth->fetchrow_hashref)) {
    $row->{used_bytes} += $esize;
    my $cmd = "update afsvol_freespace set used_bytes=$row->{used_bytes} where volume_name = " . $::sudbh->quote($row->{volume_name});
    $::log->log (4, "AfsBits::allocVolume: $cmd\n");
    if ((my $rows = $::sudbh->do ($cmd))<1) {
      $::log->error ("AfsBits::allocVolume: failed $cmd!\n");
      $::sudbh->rollback;
      return undef;
    }
    $::log->log (3, "AfsBits::allocVolume: using $row->{volume_name} for " . $atom->commonName . "\n");
    $::sudbh->commit;
    return $row->{volume_name};
  }
}

###############################################################################

sub mountVolume {
  my $volume_name = shift;
  my $reason = shift;

  if ((!defined $volume_name) || (!defined $reason)) {
    $::log->error ("AfsBits::mountVolume: Volume name and reason required to mount a volume!\n");
    return undef;
  }

  my $res = $::sudbh->do ("lock afsvol_mount in access exclusive mode");
  # lock table returns -1 on success
  if (!defined $res) {
    $::log->error ("AfsBits::mountVolume: lock table failed\n");
    goto fail;
  }
  my $rows = $::sudbh->do ("insert into afsvol_mount (volume_name, reason) values (" . $::sudbh->quote($volume_name) . ", " . $::sudbh->quote($reason) . ")");
  if (!defined $rows) {
    $::log->error ("AfsBits::mountVolume: insert reason failed\n");
    goto fail;
  }
  if (!$rows) {
    $::log->log (3, "AfsBits::mountVolume: found dup entry for $volume_name:$reason!\n");
  }
  my $count = $::sudbh->selectrow_array ("select count(*) from afsvol_mount where volume_name = " . $::sudbh->quote($volume_name));
  if (!defined $count) {
    $::log->error ("AfsBits::mountVolume: select failed\n");
    goto fail;
  }
  if ($count == 1) {
    # Actually mount the volume on the first request
    if (Sushi::Misc::runCmd ($FS, "mkm", "$MNTDIR/$volume_name", $volume_name)) {
      $::log->error ("fs mkm $volume_name failed ($reason)!!\n");
      goto fail;
    }
    $::log->log ("AfsBits::mountVolume: $volume_name mounted\n");
  }

 succeed:
  $::sudbh->commit;
  return getMountPath ($volume_name);

 fail:
  $::sudbh->rollback;
  return undef;
}

###############################################################################

sub getMountPath {
  my $volume_name = shift;
  "$MNTDIR/$volume_name";
}


###############################################################################

sub unMountVolume {
  my $volume_name = shift;
  my $reason = shift;

  defined ($volume_name) && defined ($reason) or return 0;

  my $res = $::sudbh->do ("lock afsvol_mount in access exclusive mode");
  # lock table returns -1 on success
  if (!defined $res) {
    $::log->error ("AfsBits::unMountVolume: lock table failed\n");
    goto fail;
  }
   my $rows = $::sudbh->do ("delete from afsvol_mount where volume_name = " . $::sudbh->quote($volume_name) . " and reason =  " . $::sudbh->quote($reason));
  if (!defined $rows) {
    $::log->error ("AfsBits::unMountVolume: delete reason failed\n");
    goto fail;
  }
  if ($rows<1) {
    $::log->log (2, "AfsBits::unMountVolume: No entry for $volume_name:$reason!\n");
    goto fail;
  }
  my $count = $::sudbh->selectrow_array ("select count(*) from afsvol_mount where volume_name = " . $::sudbh->quote($volume_name));
  if (!defined $count) {
    $::log->error ("AfsBits::unMountVolume: select failed\n");
    goto fail;
  }
  $count and goto succeed;

  # Last reference to the volume gone -- update the free space and
  # remove the mount point.
  Sushi::Misc::runCmd ($FS, "rmm", "$MNTDIR/$volume_name");

 succeed:
  $::sudbh->commit;
  return 1;

 fail:
  $::sudbh->rollback;
  return 0;
}

###############################################################################
# Create the destination directory for an atom within a volume.
# The volume must already be mounted.

sub createDest {
  my $self = shift;
  my $timestamp = shift;
  my $mountpath = shift;

  my $afs_path = $self->getDest ($timestamp);

  Sushi::Misc::runCmd ("/bin/mkdir", "-p", "$mountpath/$afs_path");
  #my $mkdir_err;
  #mkpath("$mountpath/$afs_path", {error => \$mkdir_err});
  #foreach my $diag (@{$mkdir_err}) {
  #  my ($file, $message) = each %{$diag};
  #  $self->error("AfsBits::createDest: Problem creating $file: $message\n");
  #}

  # sanity check to make sure the directory *REALLY* got made
  if (! -d "$mountpath/$afs_path") {
    $self->error ("AfsBits::createDest: Failed to make destination dir $mountpath/$afs_path!\n");
    return undef;
  }

  # forcibly flush these directories to the fileserver
  Sushi::Misc::runCmd ($FS, "flush", "$mountpath/$afs_path");
  my $temp_path = $afs_path;
  while ($temp_path =~ /\//) {
    $temp_path =~ s:\/[^/]+$::;
    Sushi::Misc::runCmd ($FS, "flush", "$mountpath/$temp_path");
  }

  my $host;
  if (defined ($host = $self->needHostAcl)) {
    # Set acls for host atoms to host princ
    Sushi::Misc::runCmd ($FS, "setacl", "$mountpath/$afs_path", "wsbackup.$host", "write");
    # Also grant lookup to the rest of the dirs in $afs_path
    my $path1 = $afs_path;
    while ($path1 =~ /\//) {
      $path1 =~ s:\/[^/]+$::;
      Sushi::Misc::runCmd ($FS, "setacl", "$mountpath/$path1", "wsbackup.$host", "l");
    }
  }

  $afs_path;
}

###############################################################################
# Return the contents of the mount point relation

sub getMountPoints {
  my %parms = ( @_ );

  my $list = [];

  my $query = "select * from afsvol_mount order by volume_name, reason";
  $main::log->log (4, "AfsBits::getMountPoints $query\n");
  my $sth = $::sudbh->prepare ($query) or return undef;
  $sth->execute or return undef;
  while (defined (my $row = $sth->fetchrow_hashref)) {
    push @$list, $row;
  }
  return $list;
}

###############################################################################
# Clear the mount point relation

sub delMountPoints {
  my $cmd = "delete from afsvol_mount";
  $main::log->log (4, "AfsBits::delMountPoints: $cmd\n");
  my $rows = $::sudbh->do ($cmd);
  $::sudbh->commit;
  return $rows;
}

###############################################################################
# Create a host pts id for the supplied atom

sub createPts {
  my $self = shift;

  my ($host) = split (/:/, $self->name);

  if (!defined ($host)) {
    $self->error ("Can't get host name from " . $self->commonName . "\n");
    return 0;
  }

  # See if it exists
  my $fh = new FileHandle "pts exa wsbackup.$host |";
  if (!defined $fh) {
    $self->error ("Yikes!  pts exa wsbackup.$host failed\n");
    return 0;
  }
  while (<$fh>) {
    if (/^Name: wsbackup/) {
      return 1;
    }
  }

  # Doesn't exist, create it
  $self->log (2, "Creating pts id for " . $self->commonName . "\n");
  Sushi::Misc::runCmd ($PTS, 'createuser', "wsbackup.$host");
}

###############################################################################

sub retireVolume {
  my $vol = shift;
  my $donothing = shift;

  my ($server, $part, $nrows);

  # They should have already checked, but make sure there are no 
  # un-purged atoms attached to the volume.
  my $list = Sushi::GenDB::fetch (
				  {
				   on_disk => 'Purged',
				   afs_volume => $vol,
				  },
				  {
				   opers => {
					     on_disk => '!='
					    }
				  },
				  $::table_defs->{results}
				 );
  if (@$list) {
    $::log->log (1, "$vol still has " . scalar (@$list) . " un-purged atom(s); not deleting\n");
    return 0;
  }

  # Get the server/partition
  ($server, $part) = getServerPart ($::log, $vol);
  if (defined $server) {
    if ($donothing || $main::donothing) {
      $::log->log (1, "Would remove $vol from $server $part\n");
    } else {
      # Delete the volume
      $::log->log (1, "Removing $vol from $server $part\n");
      Sushi::Misc::runCmd ($VOS, 'remove', $server, $part, $vol);
    }
  } else {
    $::log->log (1, "Can't find server/partition for $vol -- skipping vos delete...\n");
  }

  # If not in donothing mode, also update the db
  ($donothing || $main::donothing) and return 1;
  $nrows = Sushi::GenDB::update (
				 {
				  volume_name => $vol,
				 },
				 {},
				 {
				  deleted => 1,
				 },
				 $::table_defs->{afsvol_freespace},
				);
  if ($nrows != 1) {
    $::log->error ("AfsBits::retireVolume modified $nrows rows (should be 1) for $vol!\n");
    return 0;
  }

  return 1;
}


###############################################################################

sub getServerPart {
  my $self = shift;
  my $vol = shift;

  my ($fh, $server, $part);
  $fh = new FileHandle "$VOS listvl $vol |";
  if (!defined $fh) {
    $self->error ("AfsBits::getServerPart can't 'vos listvl $vol'!\n");
    return (undef, undef);
  }
  ($server, $part) = (undef, undef);
  while (<$fh>) {
    (($server, $part) = ($_ =~ /server\s+(.*)\s+partition\s+(.*)\s+RW\s+Site/))
      and last;
  }
  undef $fh;
  if (!defined $server) {
    $self->error ("AfsBits::getServerPart can't find server/partition for $vol'!\n");
    return (undef, undef);
  }
  return ($server, $part);
}


###############################################################################

1;
