# Copyright (c) 2012-2012 Zmanda, Inc.  All Rights Reserved.
# Copyright (c) 2013-2016 Carbonite, Inc.  All Rights Reserved.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
# or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
# for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
#
# Contact information: Carbonite Inc., 756 N Pastoria Ave
# Sunnyvale, CA 94085, or: http://www.zmanda.com

package Amanda::Storage;

use strict;
use warnings;
use Data::Dumper;
use vars qw( @ISA );
use IPC::Open2;

use Amanda::Paths;
use Amanda::Util;
use Amanda::Config qw( :getconf );
use Amanda::Device qw( :constants );
use Amanda::Debug qw( debug );
use Amanda::MainLoop;
use Amanda::Policy;
use Amanda::Changer;
use Amanda::Paths;

=head1 NAME

Amanda::Storage -- interface to storage scripts

=head1 SYNOPSIS

    use Amanda::Storage;

    # load defaulf storage and changer
    my $storage = Amanda::Storage->new(tapelist => $tapelist);
    # load specific storage and changer
    my $storage = Amanda::Storage->new(storage_name => $storage,
				       changer_name => $changer,
				       tapelist     => $tapelist);
    my $chg = $storage->{'chg'};
    $storage->quit();

=head1 INTERFACE

All operations in the module return immediately, and take as an argument a
callback function which will indicate completion of the changer operation -- a
kind of continuation.  The caller should run a main loop (see
L<Amanda::MainLoop>) to allow the interactions with the changer script to
continue.

A new object is created with the C<new> function as follows:

  my $storage = Amanda::Storage->new(storage_name   => $storage_name,
				     changer_name   => $changer_name,
				     tapelist       => $tapelist,
				     labelstr       => $labelstr,
				     autolabel      => $autolabel,
				     meta_autolabel => $meta_autolabel);

to create a named storage (a name provided by the user, either specifying a
storage directly or specifying a storage definition).

If there is a problem creating the new object, then the result is a
Amanda::Changer::Error.

Thus the usual recipe for creating a new storage is

  my $storage = Amanda::Storage->new(tapelist => $tapelist);
  if ($storage->isa("Amanda::Changer::Error")) {
    die("Error creating storage: $sorage");
  }
  my $chg = $storage->{'chg'};
  if ($chg->isa("Amanda::Changer::Error")) {
    die("Error creating changer:  $chg");
  }

C<tapelist> must be an Amanda::Tapelist object. It is required if you want to
use $chg->volume_is_labelable(), $chg->make_new_tape_label(),
$chg->make_new_meta_label(), $res->make_new_tape_label() or
$res->make_new_meta_label().
C<labelstr> must be like getconf($CNF_LABELSTR), that value is used if C<labelstr> is not set.
C<autolabel> must be like getconf($CNF_AUTOLABEL), that value is used if C<autolabel> is not set.
C<meta_autolabel> must be like getconf($CNF_META_AUTOLABEL), that value is used if C<meta_autolabel> is not set.
=head2 MEMBER VARIABLES

Note that these variables are not set until after the subclass constructor is
finished.

=over 4

=item C<< $storage>{'storage_name'} >>

Gives the name of the storage.  This name will make sense to the user.
It should be used to describe the storage in messages to the user.

=cut

sub DESTROY {
    my $self = shift;

    debug("Storage '$self->{'storage_name'}' not quit") if defined $self->{'storage_name'};
}

# this is a "virtual" constructor which instantiates objects of different
# classes based on its argument.  Subclasses should not try to chain up!
sub new {
    my $class = shift;
    $class eq 'Amanda::Storage'
	or die("Do not call the Amanda::Storage constructor from subclasses");
    my %params = @_;
    my $storage_name = $params{'storage_name'};
    my $changer_name = $params{'changer_name'};
    my ($uri, $cc);

    if (defined $changer_name and !$changer_name) {
        return Amanda::Changer::Error->new('fatal',
		source_filename => __FILE__,
		source_line     => __LINE__,
		code            => 1150000,
		severity	=> $Amanda::Message::ERROR);
    }

    if (!defined $storage_name) {
	my $il = getconf($CNF_STORAGE);
	$storage_name = $il->[0];
    }

    my $self = undef;

    # Create a storage
    if (!$storage_name) {
	return Amanda::Changer::Error->new('fatal',
		source_filename => __FILE__,
		source_line     => __LINE__,
		code            => 1150001,
		severity	=> $Amanda::Message::ERROR);
    }
    my $st = Amanda::Config::lookup_storage($storage_name);
    if (!$st) {
	return Amanda::Changer::Error->new('fatal',
		source_filename => __FILE__,
		source_line     => __LINE__,
		code            => 1150002,
		severity	=> $Amanda::Message::ERROR,
		storage    => $storage_name);
    }

    my $tpchanger = storage_getconf($st, $STORAGE_TPCHANGER);
    if (!exists $params{'changer_name'}) {
	$changer_name = $tpchanger if !$changer_name;
	if (!$changer_name || $changer_name eq '') {
	    my $tapedev = storage_getconf($st, $STORAGE_TAPEDEV);
	    if (!$tapedev || $tapedev eq '') {
		return Amanda::Changer::Error->new('fatal',
		    source_filename => __FILE__,
		    source_line     => __LINE__,
		    code            => 1150003,
		    severity	    => $Amanda::Message::ERROR,
		    storage         => $storage_name);
	    }
	    $changer_name = $tapedev;
	}
    }

    $self = {
	storage_name   => $storage_name,
    };
    $self->{'labelstr'} = storage_getconf($st, $STORAGE_LABELSTR);
    $self->{'autolabel'} = storage_getconf($st, $STORAGE_AUTOLABEL);
    $self->{'meta_autolabel'} = storage_getconf($st, $STORAGE_META_AUTOLABEL);
    $self->{'tpchanger'} = $tpchanger;
    $self->{'runtapes'} = storage_getconf($st, $STORAGE_RUNTAPES);
    $self->{'taperscan_name'} = storage_getconf($st, $STORAGE_TAPERSCAN);
    $self->{'tapetype_name'} = storage_getconf($st, $STORAGE_TAPETYPE);
    $self->{'max_dle_by_volume'} = storage_getconf($st, $STORAGE_MAX_DLE_BY_VOLUME);
    $self->{'taperalgo'} = storage_getconf($st, $STORAGE_TAPERALGO);
    $self->{'taper_parallel_write'} = storage_getconf($st, $STORAGE_TAPER_PARALLEL_WRITE);
    $self->{'policy'} = Amanda::Policy->new(policy => storage_getconf($st, $STORAGE_POLICY));
    $self->{'tapepool'} = storage_getconf($st, $STORAGE_TAPEPOOL);
    $self->{'eject_volume'} = storage_getconf($st, $STORAGE_EJECT_VOLUME);
    $self->{'erase_volume'} = storage_getconf($st, $STORAGE_ERASE_VOLUME);
    $self->{'device_output_buffer_size'} = storage_getconf($st, $STORAGE_DEVICE_OUTPUT_BUFFER_SIZE);
    $self->{'seen_device_output_buffer_size'} = storage_seen($st, $STORAGE_DEVICE_OUTPUT_BUFFER_SIZE);
    $self->{'autoflush'} = storage_getconf($st, $STORAGE_AUTOFLUSH);
    $self->{'flush_threshold_dumped'} = storage_getconf($st, $STORAGE_FLUSH_THRESHOLD_DUMPED);
    $self->{'flush_threshold_scheduled'} = storage_getconf($st, $STORAGE_FLUSH_THRESHOLD_SCHEDULED);
    $self->{'taperflush'} = storage_getconf($st, $STORAGE_TAPERFLUSH);
    $self->{'report_use_media'} = storage_getconf($st, $STORAGE_REPORT_USE_MEDIA);
    $self->{'report_next_media'} = storage_getconf($st, $STORAGE_REPORT_NEXT_MEDIA);
    $self->{'interactivity'} = storage_getconf($st, $STORAGE_INTERACTIVITY);
    $self->{'set_no_reuse'} = storage_getconf($st, $STORAGE_SET_NO_REUSE);
    $self->{'dump_selection'} = storage_getconf($st, $STORAGE_DUMP_SELECTION);
    $self->{'erase_on_failure'} = storage_getconf($st, $STORAGE_ERASE_ON_FAILURE);
    $self->{'erase_on_full'} = storage_getconf($st, $STORAGE_ERASE_ON_FULL);
    bless $self, $class;

    $self->{'tapetype'} = lookup_tapetype($self->{'tapetype_name'});
    $self->{'chg'} = Amanda::Changer->new($changer_name, storage => $self,
					  tapelist => $params{'tapelist'},
					  no_validate => $params{'no_validate'})
				if defined $changer_name;
    return $self;

}

sub erase_no_retention {
    my $self = shift;

    my @command = ("$sbindir/amrmtape", Amanda::Config::get_config_name(), "--remove-no-retention", "--keep-label", "--erase", "-ostorage=$self->{'storage_name'}");
    debug("Running: " . join(' ', @command));
    my ($child_out, $child_in);
    my $pid = open2($child_out, $child_in, @command);
    close($child_in);
    while (<$child_out>) {
	debug($_);
    }
    close($child_out);
    waitpid($pid, 0);
}

sub quit {
    my $self = shift;

    delete $self->{'storage_name'};
    delete $self->{'labelstr'};
    delete $self->{'autolabel'};
    delete $self->{'tpchanger'};
    $self->{'chg'}->quit() if defined $self->{'chg'};
    delete $self->{'chg'};
}

1;
