The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#
# Copyright (c) 2002 Paul Winkeler.  All Rights Reserved.
# This program is free software; you may redistribute it and/or modify it under
# the same terms as Perl itself.
#
package NBU::Media;

use strict;
use Carp;

use Date::Parse;

use NBU::Robot;

BEGIN {
  use Exporter   ();
  use AutoLoader qw(AUTOLOAD);
  use vars       qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $AUTOLOAD);
  use vars       qw(%densities %mediaTypes);
  $VERSION =	 do { my @r=(q$Revision: 1.45 $=~/\d+/g); sprintf "%d."."%02d"x$#r,@r };
  @ISA =         qw();
  @EXPORT =      qw(%densities);
  @EXPORT_OK =   qw();
  %EXPORT_TAGS = qw();
}

%densities = (
  13 => "dlt",
  16 => "8mm",
  12 => "4mm",
  6 => "hcart",
  19 => "dtf",
  9 => "odiskwm",
  10 => "odiskwo",
  0 => "qscsi",
  15 => "dlt2",
  14 => "hcart2",
  20 => "hcart3",
  21 => "dlt3",
);

my %mediaCodes = (
  'DLT' => 11,
  'DLT_CLN'=> 12,
  'DLT2' => 16,
  'DLT2_CLN' => 17,
  'DLT3' => 26,
  'DLT3_CLN' => 27,
  'HCART' => 6,
  'HC_CLN' => 13,
  'HCART2' => 14,
  'HC2_CLN' => 15,
  'HCART3' => 24,
  'HC3_CLN' => 25,
  '4MM' => 9,
  '4MM_CLN' => 10,
  '8MM' => 4,
  '8MM_CLN' => 5,
#  '8MM2' => 
#  '8MM2_CLN' => 
#  'D2' => 
#  'D2_CLN' => 
  'DTF' => 22,
  'DTF_CLN' => 23,
  'DEFAULT' => 0,
);

%mediaTypes = (
  0  => "DEFAULT",
  11 => "DLT cartridge tape",
  12 => "DLT cleaning tape",
  16 => "DLT cartridge tape 2",
  17 => "DLT cleaning tape 2",
  26 => "DLT cartridge tape 3",
  27 => "DLT cleaning tape 3",
  6  => "1/2\" cartridge tape",
  13 => "1/2\" cleaning tape",
  14 => "1/2\" cartridge tape 2",
  15 => "1/2\" cleaning tape 2",
  24 => "1/2\" cartridge tape 3",
  25 => "1/2\" cleaning tape 3",
  4  => "8MM cartridge tape",
  5  => "8MM cleaning tape",
  9  => "4MM cartridge tape",
  10 => "4MM cleaning tape",
  22 => "DTF cartridge tape",
  23 => "DTF cleaning tape",
  1  => "Rewritable optical disk",
  2  => "WORM optical disk",
  8  => "QIC - 1/4\" cartridge tape",
);

my %mediaList;
my %barcodeList;

sub new {
  my $proto = shift;
  my $media = {};

  bless $media, $proto;

  if (@_) {
    my $mediaID = shift;
    my $voldbHost = shift;

    if (exists($mediaList{$mediaID})) {
      $media = $mediaList{$mediaID};
    }
    else {
      $media->{VOLDBHOST} = $voldbHost;
      $media->{RVSN} = $mediaID;
      $mediaList{$media->{RVSN}} = $media;
    }
    if (@_) {
      my $removable = shift;
      $media->{REMOVABLE} = $removable;
    }
    else {
      $media->{REMOVABLE} = 1;
    }
  }
  return $media;
}

my $filled;
sub populate {
  my $proto = shift;
  my $volume;
  my $updateRobot = shift;

  my $pipe;

  $filled = 0;

  #
  # Have to force the pool information to load or we dead-lock.  It appears
  # the VM database deamon is single threaded and won't answer a pool query
  # until the volume listing is completed...
  NBU::Pool->populate;

  #
  # The rule is to populate with information from all the volume databases
  # maintained by the master and its active media servers (allowing for the
  # master in fact not to be a media server).
  my %voldbHosts;
  my @masters = NBU->masters;  my $master = $masters[0];
  $voldbHosts{$master->name} = $master;
  foreach my $ms (NBU::StorageUnit->mediaServers($master)) {
    if (defined(my $EMMserver = $ms->EMMserver)) {
      $voldbHosts{$EMMserver->name} = $EMMserver;
    }
    else {
      $voldbHosts{$ms->name} = $ms;
    }
  }

  foreach my $voldbHost (values %voldbHosts) {
    $pipe = NBU->cmd("vmquery -a -w -h ".$voldbHost->name." |");
    $_ = <$pipe>; $_ = <$pipe>; $_ = <$pipe>;
    while (<$pipe>) {
      my ($id,
	  $opticalPartner,
	  $mediaCode,
	  $barcode, $barcodePartner,
	  $robotHostName, $robotType, $robotNumber, $slotNumber,
	  $side,
	  $volumeGroup,
	  $volumePool, $volumePoolNumber, $previousVolumePool,
	  $mountCount, $maxMounts, $cleaningCount,
	  $creationDate, $creationTime,
	  $assignDate, $assignTime,
	  $firstMountDate, $firstMountTime,
	  $lastMountDate, $lastMountTime,
	  $expirationDate, $expirationTime,
	  $status,
	  $offsiteLocation,
	  $offsiteSentDate, $offsiteSentTime,
	  $offsiteReturnDate, $offsiteReturnTime,
	  $offsiteSlot,
	  $offsiteSessionID,
	  $version,
	  $description,
	)
	= split(/[\s]+/, $_, 37);

      #
      # "Normal" installations will not use the same serial number in more than 
      # one volume database.  Thus our test here more or less expects not to
      # find this id:
      $volume = NBU::Media->byID($id, $voldbHost);
      if (defined($volume)) {
	if (NBU->debug) {
	  print STDERR "Volume $id in voldb on ".$voldbHost->name." conflicts with existing volume\n";
	  print STDERR "  Host: ".$volume->voldbHost."\n";
	}
	next;
      }
      else {
	$volume = NBU::Media->new($id, $voldbHost);

	$filled += 1;
      }

      $volume->barcode($barcode);

      if (!exists($mediaCodes{$mediaCode})) {
      }
      $volume->{MEDIATYPE} = $mediaCodes{$mediaCode};

      $volume->{CLEANINGCOUNT} = $cleaningCount;
      $volume->{MOUNTCOUNT} = $mountCount;
      $volume->{MAXMOUNTS} = $maxMounts;
      $volume->{GROUP} = ($volumeGroup eq "---") ? undef : $volumeGroup,


      $volume->{POOL} = NBU::Pool->byID($volumePoolNumber);
      $volume->{PREVIOUSPOOL} = NBU::Pool->byName($previousVolumePool);

      $volume->{OFFSITELOCATION} = $offsiteLocation unless ($offsiteLocation eq "-");
      $volume->{OFFSITESLOT} = $offsiteSlot unless (($offsiteSlot eq "-") || ($offsiteSlot == 0));
      $volume->{OFFSITESESSIONID} = $offsiteSessionID unless (($offsiteSessionID eq "-") || ($offsiteSessionID == 0));

      my $rd = $offsiteReturnDate." ".$offsiteReturnTime;  $rd = str2time($rd);
      $volume->{OFFSITERETURN} = $rd if (defined($rd));

      $volume->{VERSION} = $version;
      $volume->{DESCRIPTION} = $description;

      if ($updateRobot && ($robotType ne "NONE")) {
	my $robot;
	if (!defined($robot = NBU::Robot->byID($robotNumber))) {
	  $robot = NBU::Robot->new($robotNumber, $robotType);
	}
	$robot->insert($slotNumber, $volume);
	$volume->robot($robot);
	$volume->slot($slotNumber);
      }

      $volume->{NETBACKUP} = ($status == 1);
    } 
    close($pipe);
  }

  $pipe = NBU->cmd("bpmedialist -L |");
  my $mmdbHost;
  while (<$pipe>) {

    if (/^Server Host = ([\S]+)[\s]*$/) {
      $mmdbHost = NBU::Host->new($1);
      next;
    }

    if (/^media_id = ([A-Z0-9]+), partner_id.*/) {
      if ($volume) {
        print STDERR "New media $1 encountered when old one ".$volume->id." still active!\n";
        exit 0;
      }
      $volume = NBU::Media->byID($1);
      if (!defined($volume)) {
	print STDERR "Media id $1 in mmdb on ".$mmdbHost->name." was not found in any voldb!\n" if (NBU->debug);
	$volume = NBU::Media->new($1);
	$filled += 1;
      }
      $volume->{MMLOADED} = 1;
      $volume->{MMDBHOST} = $mmdbHost;

      $filled += 1;
      next;
    }

    if (/^density = ([\S]+) \(([\d]+)\)/) {
      $volume->{DENSITY} = $2;
      next;
    }
    if (/^allocated = .* \(([0-9]+)\)/) {
      $volume->{ALLOCATED} = $1;
      next;
    }
    if (/^last_written = .* \(([0-9]+)\)/) {
      $volume->{LASTWRITTEN} = $1;
      next;
    }
    if (/^expiration = .* \(([0-9]+)\)/) {
      $volume->{LASTIMAGEEXPIRES} = $1;
      next;
    }
    if (/^last_read = .* \(([0-9]+)\)/) {
      $volume->{LASTREAD} = $1;
      next;
    }

    if (/retention_level = ([\d]+), num_restores = ([\d]+)/) {
      $volume->retention(NBU::Retention->byLevel($1));
      $volume->{RESTORECOUNT} = $2;
      next;
    }

    if (/^kbytes = ([\d]+), nimages = ([\d]+), vimages = ([\d]+)/) {
      $volume->{SIZE} = $1;
      $volume->{IMAGECOUNT} = $2;
      $volume->{VIMAGECOUNT} = $3;
      next;
    }

    if (/^status = 0x([0-9A-Fa-f]+)/) {
      my $status = $1;
      my $result = 0;
      foreach my $d (split(/ */, $status)) {
	$d =~ tr/a-z/A-Z/;  $d = (ord($d) - ord('A') + 10) if ($d =~ /[A-F]/);
        $result *= 16;
        $result += $d;
      }
      $volume->{STATUS} = $result;
      next;
    }

    if (/^res1 = /) {
      next;
    }

    if (/^vmpool = /) {
      next;
    }

    if (/^[\s]*$/) {
      $volume = undef;
      next;
    }
print STDERR "Unknown line\n \"$_\"\n";
  }
  close($pipe);
}

my $mediaErrors = "/usr/local/etc/media-errors.csv";
sub loadErrors {
  my $proto = shift;
  my $errorCount;

  if (open(PIPE, "<$mediaErrors")) {
    # Place this use directive inside an eval to postpone missing
    # module diagnostics until run-time
    eval "use Text::CSV_XS";
    my $csv = Text::CSV_XS->new();

    # Throw the header line away and read the remaining error lines
    $_ = <PIPE>;
    while (<PIPE>) {
      if ($csv->parse($_)) {
	my @fields = $csv->fields;
	my $volume = NBU::Media->byID($fields[1]);
	if ($volume) {
	  $volume->logError($fields[0], $fields[5]);
	  $errorCount += 1;
	}
      }
    }
    close(PIPE);
  }
  elsif (NBU->debug) {
    print STDERR "Could not load media errors from $mediaErrors\n";
  }
  return $errorCount;
}

sub listIDs {
  my $proto = shift;

  return (keys %mediaList);
}

sub listVolumes {
  my $proto = shift;

  return (values %mediaList);
}

sub list {
  my $proto = shift;

  return ($proto->listVolumes);
}

sub voldbHost {
  my $self = shift;

  if (@_) {
    $self->{VOLDBHOST} = shift;
  }
  return $self->{VOLDBHOST};
}

sub mmdbHost {
  my $self = shift;

  if (@_) {
    $self->{MMDBHOST} = shift;
  }
  return $self->{MMDBHOST};
}

sub density {
  my $self = shift;

  if (@_) {
    my $density = shift;
    $self->{DENSITY} = $density;
  }

  return $self->removable ? $densities{$self->{DENSITY}} : "disk";
}

sub retention {
  my $self = shift;

  if (@_) {
    my $retention = shift;
    $self->{RETENTION} = $retention;
  }

  return $self->{RETENTION};
}

sub barcode {
  my $self = shift;

  if (@_) {
    if (my $oldBarcode = $self->{EVSN}) {
      delete $barcodeList{$oldBarcode};
      $self->{EVSN} = undef;
    }
    if (my $barcode = shift) {
      $barcodeList{$barcode} = $self;
      $self->{EVSN} = $barcode;
    }
  }
  return $self->{EVSN};
}

sub previousPool {
  my $self = shift;

  return $self->{PREVIOUSPOOL};
}

sub pool {
  my $self = shift;

  if (@_) {
    my $newPool = shift;

    if ((my $oldPool = $self->{POOL}) != $newPool) {
      my @masters = NBU->masters;  my $master = $masters[0];
      NBU->cmd("vmchange".
	      " -h ".$master->name.
	      " -m ".$self->id.
	      " -p ".$newPool->id);
      $self->{PREVIOUSPOOL} = $oldPool;
      $self->{POOL} = $newPool;
    }
  }
  return $self->{POOL};
}

sub group {
  my $self = shift;

  if (@_) {
    my $group = shift;
    my $update;

    if (defined($group)) {
      $update = !defined($self->{GROUP}) || ($group ne $self->{GROUP});
    }
    else {
      $update = defined($self->{GROUP});
    }
    if ($update) {
      my @masters = NBU->masters;  my $master = $masters[0];
      NBU->cmd("vmchange".
	      " -h ".$master->name.
	      " -m ".$self->id.
	      " -new_v ".(defined($group) ? $group : "---"), 0);
      $self->{GROUP} = $group;
    }
  }
  else {
    $self->populate if (!defined($filled));
  }
  return $self->{GROUP};
}

sub type {
  my $self = shift;

  if (@_) {
    $self->{MEDIATYPE} = shift;
  }

  return $self->{MEDIATYPE};
}

sub logError {
  my $self = shift;
  my ($eDate, $eType) = @_;
  $eDate = str2time($eDate);

  if (!defined($self->{ERRORHIST})) {
    $self->{ERRORHIST} = {};
  }
  my $ehR = $self->{ERRORHIST};
  $$ehR{$eDate} = $eType;

  $self->{LASTERRORDATE} = $eDate;
  $self->{LASTERRORTYPE} = $eType;

  return $self->{ERRORCOUNT} += 1;
}

sub lastError {
  my $self = shift;

  if ($self->{ERRORCOUNT} > 0) {
    return ($self->{LASTERRORDATE}, $self->{LASTERRORTYPE});
  }
  else {
    return (0, undef);
  }
}

sub errorList {
  my $self = shift;
  if (!defined($self->{ERRORHIST})) {
    $self->{ERRORHIST} = {};
  }
  my $ehR = $self->{ERRORHIST};
  return %$ehR;
}

sub errorCount {
  my $self = shift;

  return $self->{ERRORCOUNT};
}

my %cleaningTypes = (
  12 => 1,
  17 => 1,
  27 => 1,
  13 => 1,
  15 => 1,
  25 => 1,
  5 => 1,
  9 => 1,
  23 => 1,
);
sub cleaningTape {
  my $self = shift;

  return exists($cleaningTypes{$self->type});
}

sub cleaningCount {
  my $self = shift;

  if (@_ && $self->cleaningTape) {
    my $newCount = shift;
    NBU->cmd("vmchange -m ".$self->id." -n $newCount", 0);
    $self->{CLEANINGCOUNT} = $newCount;
  }
  return $self->{CLEANINGCOUNT};
}

sub mountCount {
  my $self = shift;

  if ($self->cleaningTape) {
    return $self->{CLEANINGCOUNT};
  }
  else {
    return $self->{MOUNTCOUNT};
  }
}

sub firstMounted {
  my $self = shift;

  if (@_) {
    if (@_ > 1) {
      # convert date and time to epoch seconds first
    }
    else {
      $self->{FIRSTMOUNTED} = shift;
    }
  }

  return $self->{FIRSTMOUNTED};
}

sub lastMounted {
  my $self = shift;

  if (@_) {
    if (@_ > 1) {
      # convert date and time to epoch seconds first
    }
    else {
      $self->{LASTMOUNTED} = shift;
    }
  }

  return $self->{LASTMOUNTED};
}

sub byBarcode {
  my $proto = shift;
  my $barcode = shift;


  if (my $volume = $barcodeList{$barcode}) {
    return $volume;
  }
  return undef;
}

#
# The Recorded Volume Serial Number (rvsn) is the same as the media ID hence
# the two variants of id and byID.
sub byID {
  my $proto = shift;
  my $mediaID = shift;
  my $voldbHost = shift;


  if (my $volume = $mediaList{$mediaID}) {
    return $volume;
  }
  return undef;
}

sub byRVSN {
  my $self = shift;

  return $self->byID(@_);
}

sub id {
  my $self = shift;

  if (@_) {
    $self->{RVSN} = shift;
    $mediaList{$self->{RVSN}} = $self;
  }

  return $self->{RVSN};
}
sub rvsn {
  my $self = shift;

  return $self->id(@_);
}

#
# This is the External Volume Serial Number which can sometimes be
# different than the Recorded Volume Serial Number (RVSN).
sub evsn {
  my $self = shift;

  if (@_) {
    $self->{EVSN} = shift;
  }

  return $self->{EVSN};
}

sub robot {
  my $self = shift;

  if (@_) {
    $self->{ROBOT} = shift;
  }

  return $self->{ROBOT};
}

sub slot {
  my $self = shift;

  if (@_) {
    $self->{SLOT} = shift;
  }

  return $self->{SLOT};
}

sub selected {
  my $self = shift;

  if (@_) {
    $self->{SELECTED} = shift;
  }
  return $self->{SELECTED};
}

sub mount {
  my $self = shift;

  if (@_) {
    my ($mount, $drive) = @_;
    $self->{MOUNT} = $mount;
    $self->{DRIVE} = $drive;
  }
  return $self->{MOUNT};
}

sub drive {
  my $self = shift;

  return $self->{DRIVE};
}

sub unmount {
  my $self = shift;
  my ($tm) = @_;

  if (my $mount = $self->mount) {
    $mount->unmount($tm);
  }

  $self->mount(undef, undef);
  return $self;
}

sub read {
  my $self = shift;

  my ($size, $speed) = @_;

  $self->{SIZE} += $size;
  $self->{READTIME} += ($size / $speed);
}

sub write {
  my $self = shift;

  my ($size, $speed) = @_;

  $self->{SIZE} += $size;
  $self->{WRITETIME} += ($size / $speed);
}

sub writeTime {
  my $self = shift;

  return $self->{WRITETIME};
}

sub dataWritten {
  my $self = shift;

  if (@_) {
    $self->{SIZE} = shift;
  }
  return $self->{SIZE};
}

sub allocated {
  my $self = shift;

  if (@_) {
    $self->{ALLOCATED} = shift;
  }

  return $self->{ALLOCATED};
}

sub lastWritten {
  my $self = shift;

  if (@_) {
    $self->{LASTWRITTEN} = shift;
  }

  return $self->{LASTWRITTEN};
}

sub lastRead {
  my $self = shift;

  if (@_) {
    $self->{LASTREAD} = shift;
  }

  return $self->{LASTREAD};
}

#
# This refers to the date on which the youngest image on the volume expires
# and hence the earliest date on which the volume can be de-allocated
# Note to be confused with date on which the media itself expires and henceforth
# cano no longer be used for backups altogether.
sub expires {
  my $self = shift;

  if (@_) {
    $self->{LASTIMAGEEXPIRES} = shift;
  }

  return $self->{LASTIMAGEEXPIRES};
}

sub status {
  my $self = shift;

  if (@_) {
    $self->{STATUS} = shift;
  }

  return $self->{STATUS};
}

sub maxMounts {
  my $self = shift;

  if (@_) {
    my $maxMounts = shift;
    NBU->cmd("vmchange".
            " -m ".$self->id.
            " -maxmounts $maxMounts", 0);
    $self->{MAXMOUNTS} = $maxMounts;
  }

  return $self->{MAXMOUNTS};
}

sub frozen {
  my $self = shift;

  return $self->allocated ? ($self->{STATUS} & 0x1) : undef;
}

sub freeze {
  my $self = shift;

  if ($self->allocated && !($self->{STATUS} & 0x1)) {
    # issue freeze command:
    NBU->cmd("bpmedia".
            " -h ".$self->mmdbHost->name.
            " -ev ".$self->id.
            " -freeze\n");
    $self->{STATUS} |= 0x1;
  }
  return $self;
}

sub unfreeze {
  my $self = shift;

  if ($self->allocated && ($self->{STATUS} & 0x1)) {
    # issue unfreeze command:
    NBU->cmd("bpmedia".
            " -h ".$self->mmdbHost->name.
            " -ev ".$self->id.
            " -unfreeze\n");
    $self->{STATUS} &= ~0x11;
  }
  return $self;
}

sub suspended {
  my $self = shift;

  return $self->allocated ? ($self->{STATUS} & 0x2) : undef;
}

sub unsuspend {
  my $self = shift;

  if ($self->allocated && ($self->{STATUS} & 0x2)) {
    # issue unfreeze command:
    NBU->cmd("bpmedia".
            " -h ".$self->mmdbHost->name.
            " -ev ".$self->id.
            " -unfreeze\n");
    $self->{STATUS} &= ~0x2;
  }
  return $self;
}

#
# If a Media Manager allocates a volume only to fail to write to it,
# it is possible for the volume to be frozen without having any data
# written to it, i.e. it does not even have a valid header.  This state of
# the volume is identified by status bit 4.
# This author has only observed this bit in conjunction with bit 0.  As a
# matter of fact, unfreezing such a volume will also remove bit 4
sub unmountable {
  my $self = shift;

  return (defined($self->{STATUS}) ? $self->{STATUS} & 0x10 : 0);
}

sub multipleRetentions {
  my $self = shift;

  return (defined($self->{STATUS}) ? $self->{STATUS} & 0x40 : 0);
}

sub imported {
  my $self = shift;

  return (defined($self->{STATUS}) ? $self->{STATUS} & 0x80 : 0);
}

sub mpx {
  my $self = shift;

  return (defined($self->{STATUS}) ? $self->{STATUS} & 0x200 : 0);
}

sub offsiteSessionID {
  my $self = shift;

  if (@_) {
    my $offsiteSessionID = shift;
    my $update;

    if (defined($offsiteSessionID)) {
      $update = !defined($self->{OFFSITESESSIONID}) || ($offsiteSessionID ne $self->{OFFSITESESSIONID});
    }
    else {
      $update = defined($self->{OFFSITESESSIONID});
    }
    if ($update) {
      my @masters = NBU->masters;  my $master = $masters[0];
      NBU->cmd("vmchange".
	      " -h ".$master->name.
	      " -m ".$self->id.
	      " -offsid ".(defined($offsiteSessionID) ? $offsiteSessionID : "-"), 0);
      $self->{OFFSITESESSIONID} = $offsiteSessionID;
    }
  }
  return $self->{OFFSITESESSIONID};
}

sub offsiteReturnDate {
  my $self = shift;

  if (@_) {
    my $offsiteReturnDate = shift;
    my $update;

    if (defined($offsiteReturnDate)) {
      $update = !defined($self->{OFFSITERETURN}) || ($offsiteReturnDate ne $self->{OFFSITERETURN});
    }
    else {
      $update = defined($self->{OFFSITERETURN});
    }
    if ($update) {
      my @masters = NBU->masters;  my $master = $masters[0];
      NBU->cmd("vmchange".
	      " -h ".$master->name.
	      " -m ".$self->id.
	      " -offreturn ".(defined($offsiteReturnDate) ? NBU->date($offsiteReturnDate) : "0"), 0);
      $self->{OFFSITERETURN} = $offsiteReturnDate;
    }
  }
  return $self->{OFFSITERETURN};
}

sub offsiteSentDate {
  my $self = shift;

  if (@_) {
    my $offsiteSentDate = shift;
    my $update;

    if (defined($offsiteSentDate)) {
      $update = !defined($self->{OFFSITESENT}) || ($offsiteSentDate ne $self->{OFFSITESENT});
    }
    else {
      $update = defined($self->{OFFSITESENT});
    }
    if ($update) {
      my @masters = NBU->masters;  my $master = $masters[0];
      NBU->cmd("vmchange".
	      " -h ".$master->name.
	      " -m ".$self->id.
	      " -offsent ".(defined($offsiteSentDate) ? NBU->date($offsiteSentDate) : "0"), 0);
      $self->{OFFSITESENT} = $offsiteSentDate;
    }
  }
  return $self->{OFFSITESENT};
}

sub offsiteLocation {
  my $self = shift;

  if (@_) {
    my $offsiteLocation = shift;
    my $update;

    if (defined($offsiteLocation)) {
      $update = !defined($self->{OFFSITELOCATION}) || ($offsiteLocation ne $self->{OFFSITELOCATION});
    }
    else {
      $update = defined($self->{OFFSITELOCATION});
    }
    if ($update) {
      my @masters = NBU->masters;  my $master = $masters[0];
      NBU->cmd("vmchange".
	      " -h ".$master->name.
	      " -m ".$self->id.
	      " -offloc ".(defined($offsiteLocation) ? $offsiteLocation : "-"), 0);
      $self->{OFFSITELOCATION} = $offsiteLocation;
    }
  }
  return $self->{OFFSITELOCATION};
}

sub offsiteSlot {
  my $self = shift;

  if (@_) {
    my $offsiteSlot = shift;
    my $update;

    if (defined($offsiteSlot)) {
      $update = !defined($self->{OFFSITESLOT}) || ($offsiteSlot ne $self->{OFFSITESLOT});
    }
    else {
      $update = defined($self->{OFFSITESLOT});
    }
    if ($update) {
      my @masters = NBU->masters;  my $master = $masters[0];
      NBU->cmd("vmchange".
	      " -h ".$master->name.
	      " -m ".$self->id.
	      " -offslot ".(defined($offsiteSlot) ? $offsiteSlot : "-"), 0);
      $self->{OFFSITESLOT} = $offsiteSlot;
    }
  }
  return $self->{OFFSITESLOT};
}

#
# Return true, that is a non-zero value, if the tape is indeed full.
sub full {
  my $self = shift;

  return (defined($self->{STATUS}) ? $self->{STATUS} & 0x8 : 0);
}

#
# Is the volume allocated to backing up NetBackup itself?  If not, then
# it is "available" to the media managers
sub netbackup {
  my $self = shift;

  return $self->{NETBACKUP};
}
sub available {
  my $self = shift;

  return !$self->{NETBACKUP};
}

#
# The particular value returned is the number of seconds elapsed since
# the tape was taken into service (ALLOCATED) and when it was last written.
# Think of this as the volume's retirement age :-)
# Note that this value can in fact be zero even if the tape is full.
sub fillTime {
  my $self = shift;

  return $self->full ? ($self->{LASTWRITTEN} - $self->{ALLOCATED}) : undef;
}

sub eject {
  my $self = shift;

  if ($self->robot) {
    NBU->cmd("vmchange -res"." -m ".$self->id." -mt ".$self->id.
	      " -rn ".$self->robot->id." -rc1 ".$self->slot.
	      " -rh ".$self->robot->host->name.
	      " -e -sec 1", 0);
    return $self;
  }
  else {
    return undef;
  }
}

sub removable {
  my $self = shift;

  return (defined($self->{REMOVABLE}) ? $self->{REMOVABLE} : 0);
}

#
# Insert a single fragment into this volume's table of contents
sub insertFragment {
  my $self = shift;
  my $index = shift;
  my $fragment = shift;
  
  #
  # The table of contents has one entry per file on the tape
  $self->{TOC} = [] if (!defined($self->{TOC}));
  my $toc = $self->{TOC};

  #
  # Non-removable media means disk storage unit "media".  These only
  # contain a single fragment so we force the index to zero.
  $index = 0 if (!$self->removable);

  #
  # In turn, a file on the tape can contain multiple fragments of images
  # whenever multiplexing is enabled, hence we keep so-called mpx lists for each
  # file.
  $$toc[$index] = [] if (!defined($$toc[$index]));
  my $mpxList = $$toc[$index];
  push @$mpxList, $fragment;
}

#
# Load the list of fragments for this volume into its table of
# contents.
sub loadImages {
  my $self = shift;

  $self->{TOC} = [] if (!defined($self->{TOC}));

  if (!$self->{MMLOADED} || ($self->allocated && ($self->expires > time))) {
    NBU::Image->loadImages(NBU->cmd("bpimmedia -l -mediaid ".$self->id." |"));
  }
  return $self->{TOC};
}

sub tableOfContents {
  my $self = shift;

  if (!defined($self->{TOC})) {
    $self->loadImages;
  }

  my $toc = $self->{TOC};
  return (@$toc);
}

1;

__END__

=head1 NAME

NBU::Media - Every backup volume is represented by an NBU::Media object

=head1 SUPPORTED PLATFORMS

=over 4

=item * 

Solaris

=item * 

Windows/NT

=back

=head1 SYNOPSIS

    To come...

=head1 DESCRIPTION

This module provides support for ...

=head1 SEE ALSO

=over 4

=item L<NBU::Media|NBU::Media>

=back

=head1 AUTHOR

Winkeler, Paul pwinkeler@pbnj-solutions.com

=head1 COPYRIGHT

Copyright (C) 2002-2007 Paul Winkeler

=cut