#!/bin/perl -w
#############################################################################
#
#                 NOTE: This file under revision control using RCS
#                       Any changes made without RCS will be lost
#
#              $Source: /home/nickb/perl/backup-scripts/RCS/rmtcopy-0.11,v $
#            $Revision: 1.1 $
#                $Date: 2001/11/09 22:04:37 $
#              $Author: nickb $
#              $Locker:  $
#               $State: Exp $
#
#              Purpose: 
#
#          Description:
#
#           Directions: 'perldoc rmtcopy'
#
#     Default Location:
#
#           Invoked by:
#
#
#           Depends on:
#
#	Copyright (c) 2001 Assentive Solutions. All rights reserved.
#
#       This program is free software; you can redistribute it and/or
#       modify it under the terms of version 2 of the GNU General Public
#       License as published by the Free Software Foundation available at
#
#       http://www.gnu.org/copyleft/gpl.html
#
#       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.
#
#
#############################################################################

use 5.6.0;
use File::Basename "basename";
use Getopt::Long;
use Sys::Hostname;
use strict;
use vars '$VERSION';

$VERSION = '0.11';

my $usage =
q/
rmtcopy --help
rmtcopy -v

rmtcopy --src [srchost:]device --dest [desthost:]device --block-size
[ --dd <dd binary> ] [ --rsh <rsh|ssh binary> ] [ --verbose ] [ --debug ]/ .
"\n\n";

my $version =
qq/rmtcopy version $VERSION, Copyright (C) 2001 Assentive Solutions

This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.  See the GNU General Public License for more
details.\n\n/;

die $usage if (@ARGV) == 0;

# paths
my @DDPATH  = qw( /bin /usr/bin /usr/local/bin );
my @RSHPATH = @DDPATH;

# constants
my $BASENAME   = basename $0;
my $HOSTNAME   = hostname();

# flags
my $DESTFILE       = 0;
my $LOCAL_DESTHOST = 0;
my $LOCAL_SRCHOST  = 0;

# program-wide vars
my ($BLOCK_SIZE, $CMD, $CP_IN, $CP_OUT, $DEBUG, $DEBUG_LEVEL, $DEST, $DESTDEV,
    $DESTHOST, $DD, $HELP, $RSH, $SRC, $SRCDEV, $SRCHOST, $v, $VERBOSE);

GetOptions( 'block-size=i' => \$BLOCK_SIZE,# block size for dd
	    'debug!'	   => \$DEBUG,	   # debug
	    'dest=s'	   => \$DEST,	   # destination host and device
	    'dd=s'	   => \$DD,	   # dd binary
	    'help'	   => \$HELP,	   # display usage
	    'rsh=s'	   => \$RSH,	   # rsh binary
	    'src=s'	   => \$SRC,	   # source host and device
	    'v'		   => \$v,	   # program version
	    'verbose!'	   => \$VERBOSE	   # display warnings
	   );

# -- sig handlers --
$SIG{INT} = 'IGNORE';
$SIG{PIPE} = sub { die "$BASENAME: broken pipe: $!\n" };
my $TRAP = 'trap "" 0 1 2 3 15 17 18';

# -----------------------------------------------------------------------
#                                main
# -----------------------------------------------------------------------

# set debug level
$DEBUG_LEVEL = &setDebugLevel;
warn "$BASENAME: debug level $DEBUG_LEVEL\n" if $DEBUG_LEVEL > 1;

# check cmd-line parameters
&chkOpts;

# set cmd
&setCmd;

# run cmd
&runCopy;

# -----------------------------------------------------------------------
#                             subroutines
# -----------------------------------------------------------------------

# -----------------------------------------------------------------------
# setDebugLevel: set debug level: none, verbose, or debug
# caller: main
# parameters:
# returns: 0, 1, or 2
# -----------------------------------------------------------------------

sub setDebugLevel
{
    ## -- check opts --
    if (defined($DEBUG)) # $DEBUG set on cmd-line to 0 or 1
    {
	if ($DEBUG)      # $DEBUG = 1
	{
	    return 2;    # full debug
	}
    }

    if (defined($VERBOSE)) # $VERBOSE set on cmd-line to 0 or 1
    {
	if ($VERBOSE) # $VERBOSE = 1
	{
	    return 1; # partial debug
	} else {      # $VERBOSE = 0
	    return 0; # no debug
	}
    }
}

# -----------------------------------------------------------------------
# chkOpts: check cmd-line args
# caller: main
# parameters:
# returns:
# -----------------------------------------------------------------------

sub chkOpts
{
    die $version if $v;
    die $usage   if $HELP;

    my $dir;

    # -- source host & device --
    if ($SRC)
    {
	if ($SRC =~ /(^\w+.*):(\/dev\/\w+.*$)/) # remote host and device
	{
	    $SRCHOST = $1;
	    $SRCDEV  = $2;
	    $LOCAL_SRCHOST = 1 if $SRCHOST eq $HOSTNAME;
	    if ($LOCAL_SRCHOST)
	    {
		die "$BASENAME: no device $SRCDEV\n" unless -e $SRCDEV;
		warn "chkOpts(): src host is local\n" if $DEBUG_LEVEL > 1;
	    }
	    warn "chkOpts(): set src host $SRCHOST\n" if $DEBUG_LEVEL > 1;
	    warn "chkOpts(): set src $SRCDEV\n" if $DEBUG_LEVEL > 1;
	} elsif ($SRC =~ /^\/dev\/\w+.*$/) { # local device
	    $LOCAL_SRCHOST = 1;
	    warn "chkOpts(): src host is local\n" if $DEBUG_LEVEL > 1;
	    if (-e $SRC)
	    {
		$SRCDEV = $SRC;
		warn "chkOpts(): set src $SRCDEV\n" if $DEBUG_LEVEL > 1;
	    } else {
		die "$BASENAME: no device $SRCDEV\n";
	    }
	} else {
	    die "$BASENAME: invalid src specification\n";
	}
    } else {
	$LOCAL_SRCHOST = 1; # assume /dev/rmt/0 on local srchost
	$SRCDEV = '/dev/rmt/0';
	warn "chkOpts(): src host is local\n" if $DEBUG_LEVEL > 1;
	warn "chkOpts(): set src $SRCDEV\n" if $DEBUG_LEVEL > 1;
    }

    # -- dest host & device. may be disk file --
    if ($DEST)
    {
	if ($DEST =~ /(^\w+.*):(\/dev\/\w+.*$)/) # remote device
	{
	    $DESTHOST = $1;
	    $DESTDEV  = $2;
	    $LOCAL_DESTHOST = 1 if $DESTHOST eq $HOSTNAME;
	    if ($LOCAL_DESTHOST)
	    {
		die "$BASENAME: no device $DESTDEV\n" unless -e $DESTDEV;
		warn "chkOpts(): dest host is local\n" if $DEBUG_LEVEL > 1;
	    }
	    warn "chkOpts(): set dest host $DESTHOST\n" if $DEBUG_LEVEL > 1;
	    warn "chkOpts(): set dest $DESTDEV\n" if $DEBUG_LEVEL > 1;
	} elsif ($DEST =~ /(^\w+.*):(\/\w+.*$)/) { # remote disk file
	    $DESTHOST = $1;
	    $DESTDEV  = $2;
	    $DESTFILE = 1;
	    $LOCAL_DESTHOST = 1 if $DESTHOST eq $HOSTNAME;
	    if ($LOCAL_DESTHOST)
	    {
		warn "chkOpts(): dest host is local\n" if $DEBUG_LEVEL > 1;
	    }
	} elsif ($DEST =~ /^\/dev\/\w+.*$/) { # local device
	    $LOCAL_DESTHOST = 1;
	    warn "chkOpts(): dest host is local\n" if $DEBUG_LEVEL > 1;
	    if (-e $DEST)
	    {
		$DESTDEV = $DEST;
		warn "chkOpts(): set dest $DESTDEV\n" if $DEBUG_LEVEL > 1;
	    } else {
		die "$BASENAME: no device $DESTDEV\n";
	    }
	} elsif ($DEST =~ /^\/\w+.*$/) { # local disk file
	    $DESTDEV = $DEST;
	    $DESTFILE = 1;
	    $LOCAL_DESTHOST = 1;
	    warn "chkOpts(): dest host is local\n" if $DEBUG_LEVEL > 1;
	    warn "chkOpts(): set dest $DESTDEV\n" if $DEBUG_LEVEL > 1;
	} else {
	    die "$BASENAME: invalid dest specification\n";
	}
    } else {
	$LOCAL_DESTHOST = 1; # assume /dev/rmt/0 on local desthost
	$DESTDEV = '/dev/rmt/0';
	warn "chkOpts(): dest host is local\n" if $DEBUG_LEVEL > 1;
	warn "chkOpts(): set dest $DESTDEV\n" if $DEBUG_LEVEL > 1;
    }

    if (defined($SRCHOST) && defined($DESTHOST))
    {
	if ($LOCAL_SRCHOST and $LOCAL_DESTHOST && $SRCDEV eq $DESTDEV)
	{
	    die "$BASENAME: host and device overlap\n";
	} elsif ($SRCHOST eq $DESTHOST && $SRCDEV eq $DESTDEV) {
	    die "$BASENAME: host and device overlap\n";
	}
    }

    # -- block size --
    if ($BLOCK_SIZE)
    {
	die "$BASENAME: block size out of range\n" if $BLOCK_SIZE < 1;
	warn "chkOpts(): set block size $BLOCK_SIZE\n" if $DEBUG_LEVEL > 1;
    } else {
	die "$BASENAME: block size undefined. use --block-size to specify\n";
    }

    # -- dd binary --
    if ($LOCAL_DESTHOST or $LOCAL_SRCHOST)
    {
	if ($DD)
	{
	    die "$BASENAME: $DD not found: $!\n" unless -e $DD;
	    die "$BASENAME: $DD not executable: $!\n" unless -x $DD;
	} else {
	    foreach $dir (@DDPATH)
	    {
		if (-x "$dir/dd")
		{
		    $DD = "$dir/dd";
		    last;
		}
	    }
	    die "$BASENAME: cannot find dd.  use --dd to specify\n"
		unless defined $DD;
 	}
	warn "chkOpts(): set local dd to $DD\n" if $DEBUG_LEVEL > 1;
    } else {
	$DD = '/bin/dd';
	warn "chkOpts(): assuming remote /bin/dd\n" if $DEBUG_LEVEL > 1;
    }

    # -- rsh binary --
    if ($RSH)
    {
	die "$BASENAME: $RSH not found: $!\n" unless -e $RSH;
	die "$BASENAME: $RSH not executable: $!\n" unless -x $RSH;
    } else {
	foreach $dir (@RSHPATH)
	{
	    if (-x "$dir/rsh")
	    {
		$RSH = "$dir/rsh";
		last;
	    } elsif (-x "$dir/ssh") {
		$RSH = "$dir/ssh";
		last;
	    } else {
		next;
	    }
	}
	die "$BASENAME: cannot find rsh.  use --rsh to specify\n"
	    unless defined $RSH;
    }
    warn "chkOpts(): using $RSH for transport\n" if $DEBUG_LEVEL > 1;
}

# -----------------------------------------------------------------------
# setCmd: set dd cmds for copying
# caller: main
# parameters:
# returns:
# -----------------------------------------------------------------------

sub setCmd
{

    # -- copy in --
    if ($LOCAL_SRCHOST)
    {
	$CP_IN = "$TRAP\; $DD if=$SRCDEV ibs=$BLOCK_SIZE";
    } else {
	$CP_IN = "\($TRAP\; $RSH $SRCHOST \'$TRAP\; $DD if=$SRCDEV ibs=$BLOCK_SIZE\'\)";
    }

    # -- copy out --
    if ($LOCAL_DESTHOST && $DESTFILE)
    {
	$CP_OUT = "$DD of=$DESTDEV";
    } elsif ($LOCAL_DESTHOST) {
	$CP_OUT = "$DD of=$DESTDEV obs=$BLOCK_SIZE";
    } elsif ($DESTFILE) {
	$CP_OUT = "\($TRAP\; $RSH $DESTHOST \'$TRAP\; $DD of=$DESTDEV\'\)";
    } else {
	$CP_OUT = "\($TRAP\; $RSH $DESTHOST \'$TRAP\; $DD of=$DESTDEV obs=$BLOCK_SIZE conv=noerror\'\)";
    }
}

# -----------------------------------------------------------------------
# runCopy: run dd cmds
# caller: main
# parameters:
# returns:
# -----------------------------------------------------------------------

sub runCopy
{
    my $cmd = "$CP_IN | $CP_OUT";
    warn "runCopy(): running cmd \'$cmd\'\n" if $DEBUG_LEVEL >= 1;
    system($cmd);
}

# -----------------------------------------------------------------------
#                            documentation
# -----------------------------------------------------------------------

=head1 NAME

rmtcopy - copy remote tapes using dd pipe

=head1 SYNOPSIS

B<rmtcopy> B<--help>

B<rmtcopy> B<-v>

B<rmtcopy> [ B<--src> [B<srchost:>]B<device> ]
[ B<--dest> [B<desthost:>]B<device> ] B<--block-size> E<lt>bytesE<gt>
[ B<--dd> E<lt>dd binaryE<gt> ] [ B<--rsh> E<lt>rsh|ssh binaryE<gt> ]
[ B<--verbose> ] [ B<--debug> ]

=head1 DESCRIPTION

I<rmtcopy> copies tapes from one remote tape drive to another using dd(1)
and an rsh(1) or ssh(1) pipe.  B<rmtcopy> can also copy a remote tape to
a local disk file, or copy a local tape to a remote disk file.

=head1 OPTIONS

=over 4

=item B<--help>

display usage and exit.

=item B<-v>

display program version and exit.

=item B<--src>

source host.  defaults to /dev/rmt/0 on the local host.  may be followed by
a device specification.

=item B<--dest>

destination host.  defaults to /dev/rmt/0 on the local host.  may be followed
by a device specification.

=item B<--block-size>

block size of source tape archive for dd, in bytes. must be positive integer
and must be defined.

=item B<--dd>

absolute path of local dd(1) binary.  defaults to /bin/dd.

=item B<--rsh>

absolute path of rsh(1) or replacement (ssh).  defaults to /bin/rsh.

=item B<--verbose>

display verbose output to STDERR.

=item B<--debug>

display debugging output to STDERR.

=back

=head1 README

copies remote tapes using dd and ssh pipe

=head1 PREREQUISITES

perl 5.6.0 or newer

=head1 VERSION

B<rmtcopy> v0.11, Copyright (C) 2001 Assentive Solutions

=head1 AUTHOR

Nick Balthaser <nb9001@yahoo.com>

=head1 OSNAMES

freebsd
solaris

=head1 BUGS

=over 4

=item *

a cpio(1L) archive that spans multiple volumes may cause dd to hang up
unpredictably.

=back

=head1 SCRIPT CATEGORIES

UNIX/System_administration

=cut