#!/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/cpiotool-0.65,v $ # $Revision: 1.2 $ # $Date: 2001/11/14 23:18:34 $ # $Author: nickb $ # $Locker: $ # $State: Exp $ # # Purpose: interface to cpio # # Description: # # Directions: 'perldoc cpiotool' # # 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 Fcntl; use FileHandle; use File::Basename "basename"; use File::Find; use Getopt::Long; use POSIX "strftime"; use Sys::Hostname; use strict; use vars '$VERSION'; $VERSION = '0.65a'; my $usage = q/ cpiotool -c cpiotool --help cpiotool -v cpiotool [ --cpio ] [ --level <0-9> ] [ --dd
] [ -h ] [ -b ] ([ -d ] | [ -f ]) [ --block-size ] [ --reset-atime ] [ --logdir ] [ --keep-logs <0-99999> ] [ --rsh ] [ --set <1-99999> ] [ --maxsize ] [ --ziplog [ ] ] [ --confdir ] [ --header ] [ --notify [ --notify ] ... ] [ --verbose ] [ --debug ]/ . "\n\n"; my $version = qq/cpiotool 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 @COMPRESSPATH = qw( /bin /usr/bin /usr/local/bin ); my @CPIOPATH = qw( /usr/local/bin /usr/bin /bin ); my @DDPATH = @COMPRESSPATH; my @MAILPATH = @COMPRESSPATH; my @RSHPATH = @COMPRESSPATH; # constants my $BASENAME = basename $0; my $BYTECOUNT = 0; my $DATESTAMP = strftime "%Y%m%d-%H", localtime; my $HOSTNAME = hostname(); my $MEDIACOUNT = 1; # flags my ($EXCLUDELIST, $NO_DIRLIST, $NO_INCREMENTAL, $NO_LOGGING, $NO_MAXSIZE, $NOTIFY, $REMOTE_TRANSPORT, $TAR_HEADER); # program-wide vars my ($AFILE, $ARCHIVE, $ATIME, $BACKUP_ROOT, $BLOCK_SIZE, $BYTES, $CMD, $CONF_DIR, $CONFIG_FILE, $CPIO_BIN, $DD, $DEBUG, $DEBUG_LEVEL, $DEVICE, @DIRS, @EXCLUDES, $FH, $GNU_CPIO, $HEADER, $HEADER_SIZE, $HELP, $KEEP_LOGS, $LEVEL, $LOG, $LOGDIR, $MAILER, $MAXBYTES, $MAXSIZE, @NOTIFY, $PADDING, %PARAMS, $RECIP, $REMOTE_HOST, $RSH, $SET, $TIMESTAMP, $TRAILER, $v, $VERBOSE, $ZIPLOG); GetOptions( 'b=s' => \$BACKUP_ROOT, # dir to start backup 'block-size=i' => \$BLOCK_SIZE, # set (n * 512) byte blocks 'c=s' => \$CONFIG_FILE, # config file location 'confdir=s' => \$CONF_DIR, # config file dir 'cpio=s' => \$CPIO_BIN, # location of cpio 'dd=s' => \$DD, # dd binary 'debug!' => \$DEBUG, # debug 'd=s' => \$DEVICE, # tape dev 'f=s' => \$AFILE, # archive file (instead of dev) 'h=s' => \$REMOTE_HOST, # remote tapehost 'header=s' => \$HEADER, # cpio header 'help' => \$HELP, # display usage 'keep-logs=i' => \$KEEP_LOGS, # how long to keep backup logs 'level=i' => \$LEVEL, # backup level 'logdir=s' => \$LOGDIR, # logdir 'maxsize=f' => \$MAXSIZE, # max archive size MB 'notify=s' => \@NOTIFY, # email addr 'reset-atime!' => \$ATIME, # reset atime after read 'rsh=s' => \$RSH, # rsh binary 'set=i' => \$SET, # tapeset number 'v' => \$v, # program version 'verbose!' => \$VERBOSE, # display warnings 'ziplog:s' => \$ZIPLOG # compress log ); # -- sig handlers -- $SIG{HUP} = sub { warn "$BASENAME: caught HUP\n" if $DEBUG_LEVEL > 1 }; $SIG{PIPE} = sub { warn "$BASENAME: broken pipe: $!\n" }; $SIG{INT} = sub { warn "$BASENAME: caught INT\n" if $DEBUG_LEVEL > 1 }; my $TRAP = 'trap "" 0 1 2 3 15 17 18'; # ----------------------------------------------------------------------- # main # ----------------------------------------------------------------------- # load config file if (defined($CONFIG_FILE)) { die "$BASENAME: no such file $CONFIG_FILE\n" unless -e $CONFIG_FILE; &parseConfig; } # set debug level $DEBUG_LEVEL = &setDebugLevel; warn "$BASENAME: debug level $DEBUG_LEVEL\n" if $DEBUG_LEVEL > 1; # check cmd-line params &chkOpts; # create timestamp file for incremental backups &setTimestamp unless $NO_INCREMENTAL; # read dirlist: set @DIRS and @EXCLUDES from config file &readDirs; # read files to cpio while removing @EXCLUDES &findFiles(@DIRS); # send report ¬ify('REPORT') if $NOTIFY; # compress log if ($DEBUG_LEVEL >= 1) { warn "$BASENAME: compressing $LOG\n" if $ZIPLOG; } system "$ZIPLOG $LOG" if $ZIPLOG; # remove old logs &removeLogs unless $NO_LOGGING; # ----------------------------------------------------------------------- # subroutines # ----------------------------------------------------------------------- # ----------------------------------------------------------------------- # parseConfig: extract params, @DIRS, and @EXCLUDES from config file # caller: main # parameters: # returns: # ----------------------------------------------------------------------- sub parseConfig { my @params = qw(block-size conf-dir cpio dd debug device file header keep-logs level logdir maxsize notify remote-host reset-atime root-dir rsh verbose set ziplog); my ($key, $readconf, $readdirs, $readexclude); open(CONFIG, $CONFIG_FILE) or die "$BASENAME: can't open config file: $!\n"; LINE: while () { my $line = $_; next if $line =~ /^#/; if ($line =~ /\[conf]/i) { $readconf = 1; $readdirs = 0; $readexclude = 0; next; } if ($line =~ /\[dirlist]/i) { $readconf = 0; $readdirs = 1; $readexclude = 0; next; } if ($line =~ /\[exclude]/i) { $readconf = 0; $readdirs = 0; $readexclude = 1; next; } if ($readconf) { foreach $key (@params) { if ($line =~ /^($key)\s+(.*)\n$/) { chomp($line); $PARAMS{$key} = $2; next LINE; } } } elsif ($readdirs) { if ($line =~ /^\S+/) { chomp($line); push(@DIRS, $line); next; } } else { # ($readexclude) if ($line =~ /^\S+/) { chomp($line); push(@EXCLUDES, $line); next; } } } close(CONFIG); } # ----------------------------------------------------------------------- # setDebugLevel: set debug level: none, verbose, or debug # caller: main # parameters: # returns: 0, 1, or 2 # ----------------------------------------------------------------------- sub setDebugLevel { my $no_debug; ## -- check opts -- if (defined($DEBUG)) # $DEBUG set on cmd-line to 0 or 1 { if ($DEBUG) # $DEBUG = 1 { return 2; # full debug } else { # $DEBUG = 0 $no_debug = 1; } } 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 } } ## -- check config -- if (defined($PARAMS{debug})) { if ($PARAMS{debug} eq '1') { $DEBUG = 1 unless $no_debug; return 2 if $DEBUG; # full debug } else { # $PARAMS{debug} != 1 or $no_debug $no_debug = 1; } } if (defined($PARAMS{verbose})) { if ($PARAMS{verbose} eq '1') { return 1; # partial debug } else { # $PARAMS{verbose} != 1 return 0; # no debug } } } # ----------------------------------------------------------------------- # chkOpts: override config file params & check cmd-line args # should be split into smaller routines # caller: main # parameters: # returns: # ----------------------------------------------------------------------- sub chkOpts { die $version if $v; die $usage if $HELP; my ($addr, @addrlist, $DIR, $dir, $tmpaddr, @tmpdirs, $trailer); # -- backup root-dir -- if (defined($BACKUP_ROOT)) { die "$BASENAME: no directory $BACKUP_ROOT\n" unless -d $BACKUP_ROOT; } elsif (defined($PARAMS{'root-dir'})){ die "$BASENAME: no directory $PARAMS{'root-dir'}\n" unless -d $PARAMS{'root-dir'}; $BACKUP_ROOT = $PARAMS{'root-dir'}; } else { $BACKUP_ROOT = '/'; } warn "$BASENAME: set backup root to $BACKUP_ROOT\n" if $DEBUG_LEVEL >= 1; # -- cpio binary -- if (defined($CPIO_BIN)) { die "$BASENAME: $CPIO_BIN not found: $!\n" unless -e $CPIO_BIN; die "$BASENAME: $CPIO_BIN not executable: $!\n" unless -x $CPIO_BIN; $GNU_CPIO = 1 if &_isGNUcpio($CPIO_BIN); } elsif (defined($PARAMS{cpio})) { die "$BASENAME: $PARAMS{cpio} not found: $!\n" unless -e $PARAMS{cpio}; die "$BASENAME: $PARAMS{cpio} not executable: $!\n" unless -x $PARAMS{cpio}; $CPIO_BIN = $PARAMS{cpio}; $GNU_CPIO = 1 if &_isGNUcpio($CPIO_BIN); } else { foreach $DIR (@CPIOPATH) { if (-x "$DIR/cpio") { $CPIO_BIN = "$DIR/cpio"; $GNU_CPIO = 1 if &_isGNUcpio($CPIO_BIN); last; } } die "$BASENAME: cannot find cpio. use --cpio to specify\n" unless defined $CPIO_BIN; } warn "$BASENAME: using $CPIO_BIN\n" if $DEBUG_LEVEL >= 1; $CMD = "$TRAP\; $CPIO_BIN -o"; # -- dd binary -- unless ($GNU_CPIO) { if (defined($DD)) { die "$BASENAME: $DD not found: $!\n" unless -e $DD; die "$BASENAME: $DD not executable: $!\n" unless -x $DD; } elsif (defined($PARAMS{dd})) { die "$BASENAME: $PARAMS{dd} not found: $!\n" unless -e $PARAMS{dd}; die "$BASENAME: $PARAMS{dd} not executable: $!\n" unless -x $PARAMS{dd}; $DD = $PARAMS{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 "$BASENAME: using $DD for remote write\n" if $DEBUG_LEVEL >= 1; } # -- rsh binary -- unless ($GNU_CPIO) { if (defined($RSH)) { die "$BASENAME: $RSH not found: $!\n" unless -e $RSH; die "$BASENAME: $RSH not executable: $!\n" unless -x $RSH; } elsif (defined($PARAMS{rsh})) { die "$BASENAME: $PARAMS{rsh} not found: $!\n" unless -e $PARAMS{rsh}; die "$BASENAME: $PARAMS{rsh} not executable: $!\n" unless -x $PARAMS{rsh}; $RSH = $PARAMS{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 "$BASENAME: using $RSH for transport\n" if $DEBUG_LEVEL >= 1; } # -- cpio header type -- if (defined($HEADER)) { die "$BASENAME: invalid header type\n" unless $HEADER =~ /(bin|odc|newc|crc|tar|ustar|ascii)/; die "$BASENAME: invalid header type\n" if $GNU_CPIO && $HEADER =~ /ascii/; die "$BASENAME: invalid header type\n" if ! $GNU_CPIO && $HEADER =~ /newc/; die "$BASENAME: invalid header type\n" if ! $GNU_CPIO && $HEADER =~ /bin/; } elsif (defined($PARAMS{header})) { die "$BASENAME: invalid header type\n" unless $PARAMS{header} =~ /(bin|odc|newc|crc|tar|ustar|ascii)/; $HEADER = $PARAMS{header}; die "$BASENAME: invalid header type\n" if $GNU_CPIO && $HEADER =~ /ascii/; die "$BASENAME: invalid header type\n" if ! $GNU_CPIO && $HEADER =~ /newc/; die "$BASENAME: invalid header type\n" if ! $GNU_CPIO && $HEADER =~ /bin/; } else { $HEADER = 'newc' if $GNU_CPIO; $HEADER = 'ascii' if ! $GNU_CPIO; } warn "$BASENAME: set header type $HEADER\n" if $DEBUG_LEVEL >= 1; # -- blocking factor -- if (defined($BLOCK_SIZE)) { die "$BASENAME: block size out of range\n" if $BLOCK_SIZE < 1; } elsif (defined($PARAMS{'block-size'})) { die "$BASENAME: block size out of range\n" if $PARAMS{'block-size'} < 1; $BLOCK_SIZE = $PARAMS{'block-size'}; } else { $BLOCK_SIZE = 128; # (128 blocks * 512 bytes/block) = 65536 bytes/block } $BYTES = $BLOCK_SIZE * 512; # -- atime -- if (defined($ATIME)) { if ($ATIME) { $CMD .= 'a'; } } elsif (defined($PARAMS{'reset-atime'})) { if ($PARAMS{'reset-atime'} eq '1') { $CMD .= 'a'; } } # -- assign header and block-size to $CMD -- if ($HEADER eq 'ascii') { if ($BYTES == 5120) { $CMD .= 'cvB'; } else { $CMD .= "cv -C $BYTES"; } } elsif ($GNU_CPIO) { if ($BYTES == 5120) { $CMD .= "vB -H $HEADER"; } else { $CMD .= "v -H $HEADER --block-size=$BLOCK_SIZE"; } } else { $CMD .= "v -C $BYTES -H $HEADER"; } warn "$BASENAME: set block-size $BYTES bytes\n" if $DEBUG_LEVEL >= 1; # -- device -- if (defined($DEVICE)) { die "$BASENAME: invalid device specification\n" unless $DEVICE =~ /\/dev\//; if (defined($AFILE) or defined($PARAMS{file})) { die "$BASENAME: cannot specify device file and archive file\n"; } } elsif (defined($PARAMS{device})) { die "$BASENAME: invalid device specification\n" unless $PARAMS{device} =~ /\/dev\//; if (defined($AFILE) or defined($PARAMS{file})) { die "$BASENAME: cannot specify device file and archive file\n"; } $DEVICE = $PARAMS{device}; } else { # cannot check remote tape device so make best guess unless (defined($AFILE) or defined($PARAMS{file})) { $DEVICE = '/dev/rmt/0'; warn "$BASENAME: set default tape device $DEVICE\n" if $DEBUG_LEVEL >= 1; } } # -- archive file -- if (defined($AFILE)) { if (defined($DEVICE) or defined($PARAMS{device})) { die "$BASENAME: cannot specify disk file and device file\n"; } $NO_MAXSIZE = 1; } elsif (defined($PARAMS{file})) { if (defined($DEVICE) or defined($PARAMS{device})) { die "$BASENAME: cannot specify disk file and device file\n"; } $AFILE = $PARAMS{file}; $NO_MAXSIZE = 1; } else { die "$BASENAME: no archive defined\n" unless defined $DEVICE; } # -- remote host -- if (defined($REMOTE_HOST)) { if (defined($DEVICE)) { if ($GNU_CPIO) { $ARCHIVE = "$REMOTE_HOST:$DEVICE"; $CMD .= " -O $ARCHIVE"; } else { $ARCHIVE = "| \($TRAP\; $RSH $REMOTE_HOST \'$TRAP\; $DD of=$DEVICE obs=$BYTES conv=noerror\'\)"; $REMOTE_TRANSPORT = 1; } } if (defined($AFILE)) { if ($GNU_CPIO) { $ARCHIVE = "$REMOTE_HOST:$AFILE" if defined $AFILE; $CMD .= " -F $ARCHIVE"; } else { $ARCHIVE = "| \($TRAP\; $RSH $REMOTE_HOST \'$TRAP\; $DD of=$AFILE\'\)"; $REMOTE_TRANSPORT = 1; } } warn "$BASENAME: set remote archive $ARCHIVE\n" if $DEBUG_LEVEL >= 1; } elsif (defined($PARAMS{'remote-host'})) { $REMOTE_HOST = $PARAMS{'remote-host'}; if (defined($DEVICE)) { if ($GNU_CPIO) { $ARCHIVE = "$REMOTE_HOST:$DEVICE"; $CMD .= " -O $ARCHIVE"; } else { $ARCHIVE = "| \($TRAP\; $RSH $REMOTE_HOST \'$TRAP\; $DD of=$DEVICE obs=$BYTES conv=noerror\'\)"; $REMOTE_TRANSPORT = 1; } } if (defined($AFILE)) { if ($GNU_CPIO) { $ARCHIVE = "$REMOTE_HOST:$AFILE" if defined $AFILE; $CMD .= " -F $ARCHIVE"; } else { $ARCHIVE = "| \($TRAP\; $RSH $REMOTE_HOST \'$TRAP\; $DD of=$AFILE\'\)"; $REMOTE_TRANSPORT = 1; } } warn "$BASENAME: set remote archive $ARCHIVE\n" if $DEBUG_LEVEL >= 1; } else { # device file is local so check here if (defined($DEVICE)) { unless (-c $DEVICE or -l $DEVICE or -b $DEVICE) { die "$BASENAME: no device $DEVICE: $!\n"; } $ARCHIVE = $DEVICE; if ($GNU_CPIO) { $CMD .= " -O $ARCHIVE"; } else { $CMD .= " >$ARCHIVE"; } } else { $ARCHIVE = $AFILE; if ($GNU_CPIO) { $CMD .= " -F $ARCHIVE"; } else { $CMD .= " -O $ARCHIVE"; } } warn "$BASENAME: set archive $ARCHIVE\n" if $DEBUG_LEVEL >= 1; } # -- length of time to keep logs -- if (defined($KEEP_LOGS)) { if ($KEEP_LOGS < 0 or $KEEP_LOGS > 99999) { die "$BASENAME: logging specification $KEEP_LOGS out of range\n"; } } elsif (defined($PARAMS{'keep-logs'})) { $KEEP_LOGS = $PARAMS{'keep-logs'}; if ($KEEP_LOGS < 0 or $KEEP_LOGS > 99999) { die "$BASENAME: logging specification $KEEP_LOGS out of range\n"; } } else { $KEEP_LOGS = 365; # default one year } $NO_LOGGING = 1 if $KEEP_LOGS == 0; $ZIPLOG = 0 if $NO_LOGGING; # -- define simulated dump-level -- if (defined($LEVEL)) { if ($LEVEL < 0 or $LEVEL > 9) { die "$BASENAME: level $LEVEL out of range\n"; } } elsif (defined($PARAMS{level})) { $LEVEL = $PARAMS{level}; if ($LEVEL < 0 or $LEVEL > 9) { die "$BASENAME: level $LEVEL out of range\n"; } } else { $LEVEL = 0; # default: full backup warn "$BASENAME: set default backup level 0\n" if $DEBUG_LEVEL >= 1; } # -- conf-dir -- if (defined($CONF_DIR)) { die "$BASENAME: no directory $CONF_DIR\n" unless -d $CONF_DIR; die "$BASENAME: $CONF_DIR not writable: $!\n" unless -w $CONF_DIR; } elsif (defined($PARAMS{'conf-dir'})) { $CONF_DIR = $PARAMS{'conf-dir'}; die "$BASENAME: no directory $CONF_DIR\n" unless -d $CONF_DIR; die "$BASENAME: $CONF_DIR not writable: $!\n" unless -w $CONF_DIR; } elsif (-d '/share/backup/conf' and -w '/share/backup/conf') { $CONF_DIR = '/share/backup/conf'; warn "$BASENAME: set default conf dir $CONF_DIR\n" if $DEBUG_LEVEL >= 1; } else { die "$BASENAME: must specify conf dir to run incremental backup\n" unless $LEVEL == 0; $NO_INCREMENTAL = 1; warn "$BASENAME: conf dir unspecified, no incremental data will be written\n" if $DEBUG_LEVEL >= 1; } # -- dirlist: must be defined in config file -- if (@DIRS) { foreach $dir (@DIRS) { push (@tmpdirs, $dir) if -d "$BACKUP_ROOT/$dir"; warn "$BASENAME: removing dir $BACKUP_ROOT/$dir from list:$!\n" unless -d "$BACKUP_ROOT/$dir"; } die "$BASENAME: dirlist invalid\n" unless (@tmpdirs) >= 1; @DIRS = @tmpdirs; } else { warn "$BASENAME: dirlist not found. backing up all directories under $BACKUP_ROOT\n" if $DEBUG_LEVEL >= 1; $NO_DIRLIST = 1; } # -- logdir -- if (defined($LOGDIR)) { die "$BASENAME: directory $LOGDIR not found\n" unless -d $LOGDIR; die "$BASENAME: directory $LOGDIR not writable\n" unless -w $LOGDIR; warn "$BASENAME: set logdir $LOGDIR\n" if $DEBUG_LEVEL >= 1; } elsif (defined($PARAMS{logdir})) { $LOGDIR = $PARAMS{logdir}; die "$BASENAME: directory $LOGDIR not found\n" unless -d $LOGDIR; die "$BASENAME: directory $LOGDIR not writable\n" unless -w $LOGDIR; warn "$BASENAME: set logdir $LOGDIR\n" if $DEBUG_LEVEL >= 1; } elsif (-d "/var/log/backup" and -w "/var/log/backup") { $LOGDIR = "/var/log/backup"; warn "$BASENAME: set logdir $LOGDIR\n" if $DEBUG_LEVEL >= 1; } else { if ($DEBUG_LEVEL >= 1) { warn "$BASENAME: no log directory specified. no logfile will be written\n" unless $NO_LOGGING; } $NO_LOGGING = 1; $ZIPLOG = 0; } # -- max archive size -- unless ($NO_MAXSIZE) { if (defined($MAXSIZE)) { $NO_MAXSIZE = 1 if $MAXSIZE == 0; die "$BASENAME: archive size out of range\n" if $MAXSIZE < 0; $MAXBYTES = $MAXSIZE * 1024000; # size in MB * 1000 * 1024 $PADDING = $BYTES; if ($HEADER =~ /tar/) { $HEADER_SIZE = 512; # bytes/file, not counting fname } else { $HEADER_SIZE = 110; # bytes/file, not counting fname $TRAILER = 3; # cpio EOF trailer (not present w/tar hdr) $trailer = 124; # cpio EOT trailer $MAXBYTES -= $trailer; # final trailer } warn "$BASENAME: set maxsize $MAXBYTES bytes\n" if $DEBUG_LEVEL >= 1; } elsif (defined($PARAMS{maxsize})) { die "$BASENAME: archive size out of range\n" if $PARAMS{maxsize} < 0; $MAXSIZE = $PARAMS{maxsize}; $NO_MAXSIZE = 1 if $MAXSIZE == 0; $MAXBYTES = $MAXSIZE * 1024000; # size in MB * 1000 * 1024 $PADDING = $BYTES; if ($HEADER =~ /tar/) { $HEADER_SIZE = 512; # bytes/file, not counting fname } else { $HEADER_SIZE = 110; # bytes/file, not counting fname $TRAILER = 3; # cpio EOF trailer (not present w/tar hdr) $trailer = 124; # cpio EOT trailer $MAXBYTES -= $trailer;# final trailer } warn "$BASENAME: set maxsize $MAXBYTES bytes\n" if $DEBUG_LEVEL >= 1; } else { $NO_MAXSIZE = 1; } } # -- check notification addr -- if (@NOTIFY) { foreach $addr (@NOTIFY) { unless ($addr =~ /^(\w+)\@(.*\.\w+)$/) { warn "chkOpts(): invalid email address $addr\n" if $DEBUG_LEVEL >= 1; next; } $addr =~ /^(\w+)\@(.*\.\w+)$/; $RECIP = $1 . '\@' . $2; push(@addrlist, $RECIP); } if (@addrlist) { foreach $tmpaddr (@addrlist) { $NOTIFY .= "$tmpaddr "; } } else { $NOTIFY = 0; } } elsif (defined($PARAMS{notify})) { @NOTIFY = split(/\s+/,$PARAMS{notify}); foreach $addr (@NOTIFY) { unless ($addr =~ /^(\w+)\@(.*\.\w+)$/) { warn "chkOpts(): invalid email address $addr\n" if $DEBUG_LEVEL >= 1; next; } $addr =~ /^(\w+)\@(.*\.\w+)$/; $RECIP = $1 . '\@' . $2; push(@addrlist, $RECIP); } if (@addrlist) { foreach $tmpaddr (@addrlist) { $NOTIFY .= "$tmpaddr "; } } else { $NOTIFY = 0; } } else { $NOTIFY = 0; } # -- find /bin/mail or equivalent -- if ($NOTIFY) { $MAILER = &_findMailer || warn "$BASENAME: no mailer found\n"; if (defined($MAILER)) { $RECIP = $NOTIFY; } else { $NOTIFY = 0; } if ($DEBUG_LEVEL >= 1) { warn "$BASENAME: set mailer to $MAILER\n" if defined($MAILER); warn "$BASENAME: recipient(s) $RECIP\n"; } } # -- tapeset number: arbitrary user-definable integer # can be used to identify archive, otherwise set to 1 -- if (defined($SET)) { if ($SET < 1 or $SET > 99999) { die "$BASENAME: tapeset specification $SET out of range\n"; } warn "$BASENAME: set tapeset number to $SET\n" if $DEBUG_LEVEL >= 1; } elsif (defined($PARAMS{set})) { $SET = $PARAMS{set}; if ($SET < 1 or $SET > 99999) { die "$BASENAME: tapeset specification $SET out of range\n"; } warn "$BASENAME: set tapeset number to $SET\n" if $DEBUG_LEVEL >= 1; } else { $SET = 1; warn "$BASENAME: set tapeset number to $SET\n" if $DEBUG_LEVEL >= 1; } # -- logfile compressor -- if (defined($ZIPLOG) && -x $ZIPLOG) { $ZIPLOG = 0 if $NO_LOGGING; $ZIPLOG .= " -f9" if $ZIPLOG =~ /gzip/; $ZIPLOG .= " -f9" if $ZIPLOG =~ /bzip2/; warn "$BASENAME: using \'$ZIPLOG\' to compress log\n" if $DEBUG_LEVEL >= 1; } elsif (defined($PARAMS{ziplog}) && -x $PARAMS{ziplog}) { $ZIPLOG = $PARAMS{ziplog}; $ZIPLOG = 0 if $NO_LOGGING; $ZIPLOG .= " -f9" if $ZIPLOG =~ /gzip/; $ZIPLOG .= " -f9" if $ZIPLOG =~ /bzip2/; warn "$BASENAME: using \'$ZIPLOG\' to compress log\n" if $DEBUG_LEVEL >= 1; } elsif (defined($ZIPLOG) && ! -x $ZIPLOG) { $ZIPLOG = 0 if $NO_LOGGING; $ZIPLOG = &_findZiplog; } elsif (defined($PARAMS{ziplog}) && ! -x $PARAMS{ziplog}) { $ZIPLOG = 0 if $NO_LOGGING; $ZIPLOG = &_findZiplog; } else { $ZIPLOG = 0; } } # ----------------------------------------------------------------------- # _isGNUcpio: check version of cpio # caller: chkOpts() # parameters: path to cpio binary # returns: true (1) or false (0) # ----------------------------------------------------------------------- sub _isGNUcpio { my $CPIO_BIN = shift; my $version = `$CPIO_BIN --version 2>/dev/null`; chomp($version); return 0 unless $version =~ /GNU cpio/; return 1; } # ----------------------------------------------------------------------- # _findMailer: find /bin/mail or equivalent in @MAILPATH # caller: chkOpts() # parameters: # returns: absolute path of mailer or undef # ----------------------------------------------------------------------- sub _findMailer { my $DIR; my $mailer; my @mailers = qw(mailx mail); OUTER: foreach $mailer (@mailers) { foreach $DIR (@MAILPATH) { if (-x "$DIR/$mailer") { $mailer = "$DIR/$mailer"; return $mailer; last OUTER; } } } return undef unless defined($mailer); } # ----------------------------------------------------------------------- # _findZiplog: find absolute path of compression util in @COMPRESSPATH # caller: chkOpts() # parameters: # returns: absolute path of compression util or false (0) # ----------------------------------------------------------------------- sub _findZiplog { my $compress; my $ziplog; foreach $compress (@COMPRESSPATH) { if (-x "$compress/compress") { $ziplog = "$compress/compress -f"; warn "$BASENAME: set default log compressor \'$ziplog\'\n" if $DEBUG_LEVEL >= 1; return $ziplog; last; } } if ($DEBUG_LEVEL >= 1) { warn "$BASENAME: could not find log compressor\n" unless -x $ziplog; } return 0 unless -x $ziplog; } # ----------------------------------------------------------------------- # setTimestamp: write and/or remove timestamp files for this tapeset # caller: main # parameters: # returns: # ----------------------------------------------------------------------- sub setTimestamp { my $file = "$CONF_DIR/level$LEVEL-s$SET.timestamp"; # reset incremental data by removing timestamps of corresponding tapeset # if $LEVEL == 0 if ($LEVEL == 0) { my @flist = glob("$CONF_DIR/level*-s$SET.timestamp"); my $oldfile; foreach $oldfile (@flist) { unlink $oldfile; } sysopen(TS, $file, O_RDWR|O_CREAT) or die "$BASENAME: could not open $file: $!\n"; close(TS); } else { if (-f $file) { my $time = time(); utime($time, $time, $file) or die "$BASENAME: could not touch $file:\n"; } else { sysopen(TS, $file, O_RDWR|O_CREAT) or die "$BASENAME: could not open $file: $!\n";; close(TS); } } } # ----------------------------------------------------------------------- # readDirs: basic checking on @DIRS. remove later. # caller: main # parameters: # returns: # ----------------------------------------------------------------------- sub readDirs { if ($NO_DIRLIST) { ##@DIRS = ($BACKUP_ROOT); @DIRS = qw(.); } die "$BASENAME: no directories specified\n" unless (@DIRS) >= 1; if ($DEBUG_LEVEL >= 1) { warn "$BASENAME: excluding @EXCLUDES\n" if @EXCLUDES; } } # ----------------------------------------------------------------------- # findFiles: open IO handle(s) and start appropriate file-finder # caller: main # parameters: @DIRS from readDirs() # returns: # ----------------------------------------------------------------------- sub findFiles { $EXCLUDELIST = 1 if (@EXCLUDES) >= 1; my $DIR; my @DIRS = @_; my $retval; if ($NO_LOGGING) { $CMD .= " $ARCHIVE" if $REMOTE_TRANSPORT; warn "findFiles(): $CMD\n" if $DEBUG_LEVEL >= 1; chdir $BACKUP_ROOT; $FH = FileHandle->new("| $CMD") || die "findFiles(): can't fork: $!\n"; $FH->autoflush(1); } else { $LOG = "$LOGDIR/level$LEVEL-s$SET.$DATESTAMP"; warn "findFiles(): set log to $LOG\n" if $DEBUG_LEVEL >= 1; if ($REMOTE_TRANSPORT) { $CMD .= " 2>>$LOG $ARCHIVE"; warn "findFiles(): $CMD\n" if $DEBUG_LEVEL >= 1; chdir $BACKUP_ROOT; $FH = FileHandle->new("| $CMD") || die "findFiles(): can't fork: $!\n"; $FH->autoflush(1); } else { warn "findFiles(): $CMD\n" if $DEBUG_LEVEL >= 1; chdir $BACKUP_ROOT; $FH = FileHandle->new("| $CMD 2>>$LOG") || die "findFiles(): can't fork: $!\n"; $FH->autoflush(1); } } if ($NO_INCREMENTAL or $LEVEL == 0) { foreach $DIR (@DIRS) { chdir $BACKUP_ROOT; warn "findFiles(): finding files in $DIR\n" if $DEBUG_LEVEL > 1; find({ wanted => \&_allFiles, no_chdir => 1 }, $DIR); } } else { &_findLastTimestamp; foreach $DIR (@DIRS) { chdir $BACKUP_ROOT; warn "findFiles(): finding files in $DIR\n" if $DEBUG_LEVEL > 1; find({ wanted => \&_newFiles, no_chdir => 1 }, $DIR); } } warn "findFiles(): waiting for write\n" if $DEBUG_LEVEL > 1; $retval = $FH->close; my $waitpid = wait; if ($DEBUG_LEVEL > 1) { warn "findFiles(): wait status $? from IO\n"; warn "findFiles(): reaped $waitpid\n" if $waitpid; warn "findFiles(): closed IO\n" if $retval; } } # ----------------------------------------------------------------------- # _allFiles: print files to $FH after checking @EXCLUDES # caller: findFiles() # parameters: # returns: # ----------------------------------------------------------------------- sub _allFiles { my $file = $File::Find::name; my $fsize; my $excluded; my $newbytecount; my $remove; $file = $1 if ($file =~ /^\.\/(.+)$/); if ($EXCLUDELIST) { foreach $excluded (@EXCLUDES) { if ($file =~ /$excluded/) { $remove = 1; last; } } if ($remove) { warn "_allFiles(): --> exclude $file\n" if $DEBUG_LEVEL > 1; $remove = undef; } else { unless ($NO_MAXSIZE) { $fsize = (lstat($file))[7]; if ($newbytecount = &_incrByteCount($fsize,$file)) { warn "_allFiles(): sleeping\n" if $DEBUG_LEVEL > 1; sleep; &_resetIO($newbytecount); } } print $FH "$file\n"; if ($DEBUG_LEVEL > 1) { if ($NO_MAXSIZE) { warn "_allFiles(): write $file\n"; } else { warn "_allFiles(): write $file $fsize bytes\n"; } } } } else { unless ($NO_MAXSIZE) { $fsize = (lstat($file))[7]; if ($newbytecount = &_incrByteCount($fsize,$file)) { warn "_allFiles(): sleeping\n" if $DEBUG_LEVEL > 1; sleep; &_resetIO($newbytecount); } } print $FH "$file\n"; if ($DEBUG_LEVEL > 1) { if ($NO_MAXSIZE) { warn "_allFiles(): write $file\n"; } else { warn "_allFiles(): write $file $fsize bytes\n"; } } } } # ----------------------------------------------------------------------- # _newFiles: print files newer than $timestampfile to WTRFH after # checking @EXCLUDES # caller: findFiles() # parameters: # returns: # ----------------------------------------------------------------------- sub _newFiles { my ($dev,$ino,$mode,$nlink,$uid,$gid); my $timestampfile = $TIMESTAMP; my $max_file_age = -M $timestampfile; my $file = $File::Find::name; my $fsize; my $excluded; my $newbytecount; my $remove; $file = $1 if ($file =~ /^\.\/(.+)$/); if ($EXCLUDELIST) { foreach $excluded (@EXCLUDES) { if ($file =~ /$excluded/) { $remove = 1; last; } } if ($remove) { # debug (($dev,$ino,$mode,$nlink,$uid,$gid) = lstat($_)) && (-M _ < $max_file_age) && warn "_newFiles(): --> exclude $file\n" if $DEBUG_LEVEL > 1; $remove = undef; } else { unless ($NO_MAXSIZE) { if ((($dev,$ino,$mode,$nlink,$uid,$gid) = lstat($_)) && (-M _ < $max_file_age)) { $fsize = (lstat($file))[7]; if ($newbytecount = &_incrByteCount($fsize,$file)) { warn "_newFiles(): sleeping\n" if $DEBUG_LEVEL > 1; sleep; &_resetIO($newbytecount); } } } (($dev,$ino,$mode,$nlink,$uid,$gid) = lstat($_)) && (-M _ < $max_file_age) && print $FH "$file\n"; # debug if ($DEBUG_LEVEL > 1) { if ($NO_MAXSIZE) { (($dev,$ino,$mode,$nlink,$uid,$gid) = lstat($_)) && (-M _ < $max_file_age) && warn "_newFiles(): write $file\n"; } else { (($dev,$ino,$mode,$nlink,$uid,$gid) = lstat($_)) && (-M _ < $max_file_age) && warn "_newFiles(): write $file $fsize bytes\n"; } } } } else { unless ($NO_MAXSIZE) { if ((($dev,$ino,$mode,$nlink,$uid,$gid) = lstat($_)) && (-M _ < $max_file_age)) { $fsize = (lstat($file))[7]; if ($newbytecount = &_incrByteCount($fsize,$file)) { warn "_newFiles(): sleeping\n" if $DEBUG_LEVEL > 1; sleep; &_resetIO($newbytecount); } } } (($dev,$ino,$mode,$nlink,$uid,$gid) = lstat($_)) && (-M _ < $max_file_age) && print $FH "$file\n"; # debug if ($DEBUG_LEVEL > 1) { if ($NO_MAXSIZE) { (($dev,$ino,$mode,$nlink,$uid,$gid) = lstat($_)) && (-M _ < $max_file_age) && warn "_newFiles(): write $file\n"; } else { (($dev,$ino,$mode,$nlink,$uid,$gid) = lstat($_)) && (-M _ < $max_file_age) && warn "_newFiles(): write $file $fsize bytes\n"; } } } } # ----------------------------------------------------------------------- # _findLastTimestamp: find $TIMESTAMP in $CONF_DIR # caller: findFiles() # parameters: # returns: # ----------------------------------------------------------------------- sub _findLastTimestamp { my $i; for ($i = $LEVEL - 1; $i >= 0; $i--) { if (-f "$CONF_DIR/level$i-s$SET.timestamp") { $TIMESTAMP = "$CONF_DIR/level$i-s$SET.timestamp"; warn "$BASENAME: found timestampfile $TIMESTAMP\n" if $DEBUG_LEVEL >= 1; last; } } die "$BASENAME: no timestamp file\n" unless defined($TIMESTAMP); } # ----------------------------------------------------------------------- # _incrByteCount: increment $BYTECOUNT and check against $MAXBYTES # caller: _allFiles() or _newFiles() # parameters: $fsize (in bytes), $file (filename) # returns: 0 or $total if $BYTECOUNT >= $MAXBYTES # ----------------------------------------------------------------------- sub _incrByteCount { my $retval; my $fsize = $_[0]; my $file = $_[1]; my $flength = length($file); my $header_size = $HEADER_SIZE + $flength; my $padding = &_padTrailer($fsize); my $total = $fsize + $header_size + $padding; die "_incrByteCount(): file $file size $fsize bytes larger than\nmedia capacity\n" if $total > $MAXBYTES; # -- increment bytecount -- $BYTECOUNT += $total; warn "_incrByteCount(): +$header_size bytes header\n" if $DEBUG_LEVEL > 1; warn "_incrByteCount(): +$padding bytes padding\n" if $DEBUG_LEVEL > 1; warn "_incrByteCount(): count $BYTECOUNT bytes\n" if $DEBUG_LEVEL > 1; if ($BYTECOUNT >= $MAXBYTES) { my $msg = "_incrByteCount(): reached $BYTECOUNT bytes\nchange media on $ARCHIVE\n"; warn "_incrByteCount(): waiting for write\n" if $DEBUG_LEVEL > 1; $retval = $FH->close; my $waitpid = wait; if ($DEBUG_LEVEL > 1) { warn "_incrByteCount(): wait status $? from IO\n"; warn "_incrByteCount(): reaped $waitpid\n" if $waitpid; warn "_incrByteCount(): closed IO\n" if $retval; } if ($NOTIFY) { ¬ify('EOT',$msg); } else { print $msg; } unless ($NO_LOGGING) { open(LOG, ">>$LOG"); print LOG "EOT $MEDIACOUNT\n"; close(LOG); } ++$MEDIACOUNT; return $total; } else { return 0; } } # ----------------------------------------------------------------------- # _padTrailer: calculate trailer padding # caller: _incrByteCount() # parameters: $fsize in bytes # returns: $padding in bytes # ----------------------------------------------------------------------- sub _padTrailer { my $fsize = shift; my $modulus; my $padding; $padding += $TRAILER if defined($TRAILER); if ($fsize < $PADDING) # file smaller than block-size i.e. $PADDING { $padding = $PADDING - $fsize; return $padding; } elsif ($fsize == $PADDING) { # file size equal to block-size $padding = $PADDING; # return full block of padding return $padding; } else { # file larger than block-size $modulus = $fsize % $PADDING; if ($modulus == 0) { $padding = $PADDING; return $padding; # return full block of padding } else { $padding = $modulus; return $padding; } } } # ----------------------------------------------------------------------- # _resetIO: reopen IO handle closed by _incrByteCount() and reset # $BYTECOUNT # caller: _allFiles() or _newFiles() # parameters: # returns: # ----------------------------------------------------------------------- sub _resetIO { my $newbytecount = shift; if ($NO_LOGGING or $REMOTE_TRANSPORT) { unless ($NO_LOGGING) { warn "_resetIO(): appending to $LOG\n" if $DEBUG_LEVEL > 1; } chdir $BACKUP_ROOT; $FH = FileHandle->new("| $CMD") || die "_resetIO(): can't fork: $!\n"; $FH->autoflush(1); } else { warn "_resetIO(): appending to $LOG\n" if $DEBUG_LEVEL > 1; chdir $BACKUP_ROOT; $FH = FileHandle->new("| $CMD 2>>$LOG") || die "_resetIO(): can't fork: $!\n"; $FH->autoflush(1); } $BYTECOUNT = $newbytecount; } # ----------------------------------------------------------------------- # removeLogs: pass $LOGDIR to _oldLogs() # caller: main # parameters: # returns: # ----------------------------------------------------------------------- sub removeLogs { find(\&_oldLogs, $LOGDIR); } # ----------------------------------------------------------------------- # _oldLogs: find and remove old logfiles in $LOGDIR for removeLogs() # caller: removeLogs() # parameters: # returns: # ----------------------------------------------------------------------- sub _oldLogs { my ($dev,$ino,$mode,$nlink,$uid,$gid); my $min_file_age = $KEEP_LOGS; my $file = $File::Find::name; if ($DEBUG_LEVEL >= 1) { (($dev,$ino,$mode,$nlink,$uid,$gid) = lstat($_)) && (int(-M _) > $min_file_age) && warn "$BASENAME: unlinking $file\n" if $file =~ /$LOGDIR\/level$LEVEL-s$SET\.\d{8}-\d{2}/; } (($dev,$ino,$mode,$nlink,$uid,$gid) = lstat($_)) && (int(-M _) > $min_file_age) && unlink $file if $file =~ /$LOGDIR\/level$LEVEL-s$SET\.\d{8}-\d{2}/; } # ----------------------------------------------------------------------- # notify: send email msg # caller: main and/or _IO_hander() # parameters: (msg_type (REPORT|EOT), msg (EOT only)) # returns: # ----------------------------------------------------------------------- sub notify { my $msg_type = shift; if ($msg_type eq 'REPORT') { my $subject = "-s \'level $LEVEL backup on $HOSTNAME\'"; if ($NO_LOGGING) { system "$MAILER $subject $RECIP"; } else { my $blocks = &_getBlocks; my $errors = &_getErrors; open(MAILER, "| $MAILER $subject $RECIP"); print MAILER "size:\n$blocks\n\nerrors:\n$errors\n"; close(MAILER); } } else { # $msg_type eq 'EOT' my $msg = shift; my $subject = "-s \'end of media notification on $HOSTNAME\'"; open(MAILER, "| $MAILER $subject $RECIP"); print MAILER $msg; close(MAILER); warn "notify(): sent notification\n" if $DEBUG_LEVEL >= 1; } } # ----------------------------------------------------------------------- # _getBlocks: add block count in $LOG # caller: notify() # parameters: # returns: block string (success) or "" (failure) # ----------------------------------------------------------------------- sub _getBlocks { my $blockcount; my $msg; open(LOG, $LOG) or die "$BASENAME: can't open log $LOG for reading: $!\n"; while () { my $line = $_; if ($line =~ /(^[0-9]+)\sblock/) { $blockcount += $1; next; } } close(LOG); if ($blockcount && $NO_MAXSIZE) { $msg = "total $blockcount block(s)"; warn "_getBlocks(): returning \'$msg\' to $MAILER\n" if $DEBUG_LEVEL > 1; return $msg; } elsif ($blockcount) { $msg = "total $blockcount block(s) on $MEDIACOUNT volume(s)"; warn "_getBlocks(): returning \'$msg\' to $MAILER\n" if $DEBUG_LEVEL > 1; return $msg; } else { return ""; } } # ----------------------------------------------------------------------- # _getErrors: find errors in $LOG # caller: notify() # parameters: # returns: error string (success) or "" (failure) # ----------------------------------------------------------------------- sub _getErrors { my $cpio_bin = basename($CPIO_BIN); my (@errors, $err, $errors); open(LOG, $LOG) or die "$BASENAME: can't open log $LOG for reading: $!\n"; while () { my $line = $_; if ($line =~ /^$CPIO_BIN:\s.*/ || $line =~ /^$cpio_bin:\s.*/) { push(@errors, $line); } } close(LOG); foreach $err (@errors) { $errors .= $err; } if (defined($errors)) { return $errors; warn "_getErrors(): returned error string \`$errors\' to $MAILER\n" if $DEBUG_LEVEL > 1; } else { return ""; } } # ----------------------------------------------------------------------- # documentation # ----------------------------------------------------------------------- =head1 NAME cpiotool - wrapper for cpio =head1 SYNOPSIS B B<-c> Econfig fileE B B<--help> B B<-v> B [ B<--cpio> Ecpio binaryE ] [ B<--level> E0-9E ] [ B<--dd> Edd binaryE ] [ B<-h> Eremote hostE ] [ B<-b> Ebackup root dirE ] ([ B<-d> EdeviceE ] | [ B<-f> EfileE ]) [ B<--block-size> EnE ] [ B<--reset-atime> ] [ B<--logdir> ElogdirE] [ B<--keep-logs> E0-99999E ] [ B<--rsh> Ersh|ssh binaryE ] [ B<--set> E1-99999E ] [ B<--maxsize> EnE ] [ B<--ziplog> [ Ecompression utilE ] ] [ B<--confdir> Econfig dirE ] [ B<--header> Ebin|odc|newc|crc|tar|ustar|asciiE ] [ B<--notify> Eaddr@domainE [ B<--notify> Eaddr@domainE ] ... ] [ B<--verbose> ] [ B<--debug> ] =head1 DESCRIPTION I is a config-file-based wrapper for cpio(1L). the wrapper combines several commonly-used cpio copy-out options with additional options for supporting multilevel incremental backups, email notification using mail(1), logging, and include/exclude lists. options specified on the command-line override config-file parameters. include/exclude lists are only available using the config file. =head1 OPTIONS B displays usage and exits if no options are specified. specifying a single option such as B<--verbose> will start a level 0 backup from the root directory with suitable defaults: C =over 4 =item B<-c> absolute path of config file. see PARAMETERS. =item B<--help> print usage and exit. =item B<-v> print version and exit. =item B<--cpio> absolute path of cpio binary. if not specified, B will search /usr/local/bin, /usr/bin, and /bin. =item B<--level> backup level 0-9, similar to dump(8). level 0 is a full backup. A level number above 0 specifies an incremental backup of files modified more recently than the highest-numbered previous level backup. defaults to 0. =item B<--dd> absolute path of dd(1) binary. dd is used for remote writes if GNU cpio is not detected. =item B<-h> remote host. name of remote host to write archive. the host must be accessible via rsh(1) or a drop-in replacement such as ssh(1). GNU cpio only recognizes rsh, so the replacement should be called 'rsh' if GNU cpio is used. =item B<-b> backup root directory. top-level directory where B recursively searches for files to be archived. defaults to '/'. =item B<-d> device file to write archive. defaults to '/dev/rmt/0'. mutually exclusive with B<-f>. =item B<-f> absolute path of disk file to write archive. mutually exclusive with B<-d>. =item B<--block-size> cpio option to set I/O block size to (n * 512) bytes. must be positive integer. defaults to 128, i.e. 65536 bytes. =item B<--reset-atime> reset file access time after reading. default behavior updates atime as file is read for backup. =item B<--logdir> absolute path of directory to write backup log. defaults to '/var/log/backup'. =item B<--keep-logs> number of days, 0-99999, to keep logs in this tapeset. logs in this tapeset older than B<--keep-logs> days are removed. setting this number to 0 disables logging. default is 365. =item B<--rsh> absolute path of alternate remote transport: rsh(1) or ssh(1). alternate remote transport is used if GNU cpio is not detected. =item B<--set> tapeset number. arbitrary integer, 1-99999, used to identify file groupings that share the same incremental backup data and logfiles. tapeset number determines which timestamp is used to set maximum file age for files in the same backup group. log files are also named by tapeset number. defaults to 1. =item B<--maxsize> maximum archive size in MB, specified as integer or floating-point. B keeps a running byte-count and uses this value to determine when to request additional media. when using native cpio headers, B subtracts a full block from the maxsize value for end-of-archive padding, 124 bytes for the final end-of-archive trailer, 110 bytes per file plus the length of the filename for each header, and 3 bytes per file to allow for each trailer. when using tar or ustar headers, B subtracts 512 bytes per file plus the length of the filename from the maxsize value to allow for each header, and subtracts an additional number of bytes from the maxsize value to allow for padding up to each block-boundry. the maxsize value is only used when writing to a device. if set to 0 or unspecified, the cpio binary will send media requests to the controlling terminal. =item B<--ziplog> if specified backup log will be compressed with compress(1). B<--ziplog> takes an optional argument of the absolute path to an alternate compression utility, e.g. '--ziplog /usr/local/bin/bzip2'. =item B<--confdir> configuration directory. B writes incremental backup timestamps in this directory. defaults to '/share/backup/conf'. =item B<--header> cpio option to set archive header type. must be one of B, B, B, B, B, B or B. see cpio(1L) for details. defaults to B, or B if B is unavailable. =item B<--notify> email address of recipient to send error report. B uses '/bin/mail' to send the message, so the system must have a properly configured MTA for this option to work. more than one address can be specified using multiple B<--notify> options. =item B<--verbose> display verbose output to STDERR. =item B<--debug> display very verbose output to STDERR. =back =head1 PARAMETERS the config file is divided in three sections. the [CONF] section lists general configuration options. [DIRLIST] is a list of directories relative to the backup root directory to be archived. [EXCLUDES] is a list of directories relative to the backup root directory that should be excluded from the archive. comments begin with '#'. config file parameters in the [CONF] section can be overridden on the command-line. config file defaults are the same as command-line defaults. see OPTIONS. =head2 [CONF] the [CONF] section accepts the following keys. values are specified after whitespace. =over 4 =item B top level directory where B recursively searches for files to be archived. =item B absolute path of cpio. =item B backup level 0-9. =item B
absolute path of dd binary. used if GNU cpio is not detected. =item B name of remote host to write archive. =item B device file to write archive. mutually exclusive with B. =item B disk file to write archive. mutually exclusive with B. =item B positive integer that sets I/O block size to (n * 512) bytes. =item B set to 1 to reset file access time after file is read by cpio. =item B directory to write backup log. =item B tapeset number, 1-99999. =item B number of days to keep backup logs of specified tapeset, 0-99999. setting to 0 disables logging. =item B absolute path of alternate transport. used if GNU cpio is not detected. =item B floating-point number that sets maximum archive size in MB. used only if archive is a device. set to 0 to disable. =item B directory where B writes timestamp files. =item B
sets cpio header type. must be one of B, B, B, B, B, B or B. =item B email address(es) to send error report. requires '/bin/mail' and properly configured MTA. multiple addresses can be separated by whitespace. =item B absolute path of compression utility used to compress backup log. if set to 1 B will use compress. =item B set to 1 to display verbose output to STDERR. =item B set to 1 to display very verbose output to STDERR. =back =head2 [DIRLIST] list of subdirectories relative to the B to be backed up. if this section is empty B will archive all files and directories under the B. note, directories must be specified relative to the B. using absolute paths will break the file-finding routine. =head2 [EXCLUDES] list of subdirectories relative to the B to be excluded from the archive. if this section is empty B will not exclude any files. note, directories must be specified relative to the B. =head2 SAMPLE CONFIGURATION # cpiotool level 2 config [CONF] root-dir /home/nickb cpio /usr/local/bin/cpio level 2 #remote-host device /dev/fd0 #file block-size 20 logdir /var/log/backup set 3 keep-logs 14 conf-dir /usr/local/backup/conf header ustar notify ops@domain.org backup@domain.org maxsize 1.44 ziplog /usr/local/bin/bzip2 verbose 1 [DIRLIST] # relative to root-dir GNUstep/Library/AfterStep site [EXCLUDE] GNUstep/Library/AfterStep/non-configurable GNUstep/Library/AfterStep/start/Applications/Editors trash unused # end config =head1 MULTIPLE-VOLUME ARCHIVES the cpio binary supports archives that span multiple volumes. B implements this in one of 3 ways, provided the archive is written to a device: =over 4 =item 1. if B<--maxsize> is unspecified, the cpio binary will detect end of media and send a request to the controlling terminal. this is easiest but requires the backup to be run manually from a terminal. it is also time-consuming and error-prone in the event a restore is needed, as each tape in the series must be read in order to restore a file at the end of the archive. if one of the tapes in the set is corrupt, it's possible that all data on subsequent tapes in the archive will be inaccessible. =item 2. if B<--maxsize> is specified but B<--notify> is unspecified or a mailer is not found, B will estimate the total size of all cpio headers and keep a running total of the number of bytes written to the device. when the number approaches B<--maxsize>, IO to the cpio binary is closed and a request for media is printed to standard out. sending a HUP signal to B continues the backup. this option still requires the backup to be run manually from a terminal, but has the advantage of writing archives separately to individual tapes rather than writing one large archive across multiple tapes. this decreases time required for restores and reduces the potential for data loss. =item 3. if B<--maxsize> and B<--notify> are specified, and a mailer is found, B writes archives in the same manner as B<2> above, but sends media requests to the email address specified in B<--notify>. this allows some degree of automation as no interaction with a terminal is required. =back =head1 VOLUME MANAGEMENT not supported (yet). eventually B will store tape information in a mysql database, but this is a project for the distant future. =head1 INCREMENTAL BACKUPS the simplified concept of tapesets, along with the use of timestamp files, determines which files to archive for incremental (level 1-9) backups. if a configuration directory is specified B writes a timestamp file called C. the last modification time of every file in each directory in the [DIRLIST] is compared to the last modification time of the timestamp file to determine if the file should be archived. timestamp files are simply zero-length files with a naming convention recognized by B. creating, removing, or changing the modification times of timestamp files will affect the behavior of B. =head1 README backup script that uses cpio =head1 PREREQUISITES perl 5.6.0 or newer =head1 VERSION B v0.65a, Copyright (C) 2001 Assentive Solutions =head1 AUTHOR Nick Balthaser =head1 OSNAMES freebsd solaris =head1 BUGS =over 4 =item * if the remote transport (rsh or ssh) hangs up while using non-GNU cpio, IO stops and cpiotool exits. =item * when running in a terminal in the background and writing to a remote host with dd, sending SIGINT by pressing ^C can have unpredictable results, even if B is running under a subshell or nohup. =item * error "dd: unexpected short write, wrote 0 bytes, expected " while writing to remote device. the remote dd detected end of media and quit prematurely. this can mean the B<--maxsize> value is greater than the capacity of the media. now that _padTrailer() is in place this should happen far less often. adjusting B<--maxsize> can compensate for this in the interim. =item * no error checking on the B<--maxsize> value. if an incorrect value is specified, the cpio binary and/or dd may reach end-of-media before the byte-counter. this will cause dd to hang up abruptly on remote backups or conflicting media requests on local backups. =item * if a single file being backed up is larger than the media capacity, the cpio binary could reach end-of-media before the byte-counter and conflicting media requests would be sent. currently an exception is thrown and the script exits if this happens. the only solution currently is to disable B<--maxsize> by setting it to 0 or leaving it unspecified. this allows the cpio binary to create a single archive that spans multiple tapes, which can be unreliable. see B above. =item * the number of bytes subtracted from the B<--maxsize> value to allow for headers is an (improving) rough estimate and may be inaccurate. in the meantime adjusting B<--maxsize> can compensate. =item * the log-remover might not recognize logs with a compressed suffix (e.g. .gz, .bz2). =item * for full functionality B relies on the presence of mail or mailx, dd, and rsh or ssh. dd always exits on SIGINT. it's not always possible to trap SIGINT. =item * no database interface, reliance on easily-manipulated timestamp files. =back =head1 SCRIPT CATEGORIES UNIX/System_administration =cut