#!/usr/bin/perl -w

#----------------------------------------------------------------------
# affa - Automatische Festplatten Fernarchivierung
#
# Copyright (C) 2004-2009 Michael Weinberger, neddix Stuttgart, Germany
# 
# 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., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
#----------------------------------------------------------------------

use strict;
use Affa::lib qw(ExecCmd setlog lg dbg);
use Date::Format;
use Digest::MD5;
use Errno;
use esmith::Backup;
use esmith::ConfigDB;
use esmith::DB::db;
use esmith::templates;
use File::Path;
use Cwd;
use Filesys::DiskFree;
use Getopt::Long;
use Mail::Send;
use Time::Local;
use Net::DNS;
use Compress::Bzip2;

$|=1; # flush output

$ENV{LANG} = "en_US"; # Filesys::DiskFree only works with english LANG setting

my $VERSION='2.0.0';


# sub prototypes
sub affaErrorExit($);
sub affaExit($);
sub checkArchiveExists($$$);
sub checkConnection();
sub checkConnectionsAll();
sub checkConnection_silent($$$);
sub checkCrossFS($$);
sub chunk($$$$$);
sub chunkArchive($);
sub chunkfiles($$);
sub cleanup();
sub compareRPMLists($);
sub countUnit($);
sub createBackupFile();
sub cronSetup();
sub dbg($);
sub deleteJob();
sub df($);
sub DiskSpaceWarn();
sub DiskUsage();
sub DiskUsageRaw();
sub execPostJobCommand($);
sub execPreJobCommand();
sub formatHTime($);
sub	fullRestore();
sub getDefaultConfig();
sub getExcludedString();
sub getIncludedString();
sub getJobConfig($);
sub getLinkdest($);
sub getLock($);
sub getSourceDirs();
sub getSourceDirsString();
sub getStatus();
sub getStatusRaw();
sub hTime2Timestamp($);
sub imapIndexFilesDelete();
sub imapIndexFilesDeleteCommand();
sub installedRPMsList($);
sub installWatchdog($);
sub isMounted($$);
sub killJob();
sub killProcessGroup($$);
sub lg($);
sub listArchives();
sub listArchivesRaw($);
sub listJobs();
sub mailTest();
sub mailTestWatchdogRemote();
sub mount($$$);
sub moveArchive();
sub moveFileorDir($$);
sub putDoneDates($);
sub remoteCopy($$);
sub removeDir($);
sub removeLock();
sub renameJob();
sub revokeKeys($);
sub riseFromDeath();
sub runRiseRsync($$);
sub saveMySoul();
sub sendErrorMesssage();
sub sendKeys();
sub sendStatus();
sub sendSuccessMesssage();
sub setLock();
sub shiftArchives();
sub showHelp($);
sub showSchedule();
sub showVersion();
sub SignalHandler();
sub signalPostBackupEvent();
sub signalPreBackupEvent();
sub sizeUnit($);
sub startServices();
sub stopServices();
sub trim($);
sub unchunkfiles($$);
sub undoRise();
sub unmount($$);
sub updateReportDB();

my $cwd=getcwd;
chdir "/tmp";

my $allow_retry=0;
my $lockfile;
my $defaultEmail='admin';
our $rsyncLocal = '/usr/bin/rsync';
my $rsyncRemote = '/usr/bin/rsync';
our $rsyncOptions='';
my $rsyncd=0;
my $sshQuiet="-q";
my $ESX=0;
my $config = esmith::ConfigDB->open or die "Error: Couldn't open config db.";
my $LocalIP=$config->get("LocalIP")->value;
my $SystemName=$config->get("SystemName")->value;
my $DomainName=$config->get("DomainName")->value;
my $affaTitle="Affa version $VERSION on $SystemName.$DomainName ($LocalIP)";
my $ServerBasename="AFFA.$SystemName.$DomainName-$LocalIP";
my $affa = esmith::DB::db->open("/home/e-smith/db/affa");
if( not $affa )
	{
	$affa = esmith::DB::db->create("/home/e-smith/db/affa") or die "Error: Couldn't create /home/e-smith/db/affa db file.";
	}
if( not $affa->get('AffaGlobalDisable') )
	{
	my $init = $affa->new_record('AffaGlobalDisable');
	$init->set_prop('type','no');
	$affa = esmith::DB::db->open("/home/e-smith/db/affa");
	}
if( not $affa->get('DefaultAffaConfig') )
	{
	my $init = $affa->new_record('DefaultAffaConfig');
	$init->set_prop('type','default');
	$init->set_prop('sendStatus','weekly');
	$init->set_prop('status','enabled');
	$affa = esmith::DB::db->open("/home/e-smith/db/affa");
	}


my @services=('crond','smb','atalk','qmail','qpsmtpd','sqpsmtpd');
my $curtime=time(); # now timestamp
my $thisDay=time2str("%Y%j",$curtime); # day of year 1-366
my $thisWeek=time2str("%Y%W",$curtime); # week of year 1-53
my $thisMonth=time2str("%Y%m",$curtime); # month 1-12
my $thisYear=time2str("%Y",$curtime); # 4-digit year
our $process_id=$$; # my PID
my $lockdir = '/var/lock/affa'; # Process lock
my $LockisSet=0; # Flag
my $rpmlist="/home/e-smith/db/affa-rpmlist"; # list of installed RPMs
our $jobname='START';
our $Command = '';
my $interactive=0;
my $ServicesStopped=0;
my %autoMounted=();
my $interrupt='';
our $ChunkThresholdSize;

my %opts;
my $runninglog="Affa $VERSION: Running $0 @ARGV";
my $getRes = GetOptions( 
	"run"=>\$opts{'run'},
	"backup"=>\$opts{'run'}, # same as --run
	"version"=>\$opts{'version'},
	"help"=>\$opts{'help'},
	"_shorthelp"=>\$opts{'shorthelp'},
	"_jobs"=>\$opts{'jobs'},
	"make-cronjobs"=>\$opts{'make-cronjobs'},
	"list-archives"=>\$opts{'list-archives'},
	"csv"=>\$opts{'csv'},
	"rise"=>\$opts{'rise'},
	"all"=>\$opts{'all'},
	"undo-rise"=>\$opts{'undo-rise'},
	"full-restore"=>\$opts{'full-restore'},
	"send-keys"=>\$opts{'send-keys'},
	"revoke-keys"=>\$opts{'revoke-keys'},
	"check-connections"=>\$opts{'check-connections'},
	"create-backup-file"=>\$opts{'create-backup-file'},
	"outfile=s"=>\$opts{'outfile'},
	"host=s"=>\$opts{'keys-host'},
	"remoteOS=s"=>\$opts{'remoteOS'},
	"port=s"=>\$opts{'keys-port'},
	"status"=>\$opts{'status'},
	"disk-usage"=>\$opts{'disk-usage'},
	"show-schedule"=>\$opts{'show-schedule'},
	"15"=>\$opts{'15'},
	"30"=>\$opts{'30'},
	"debug"=>\$opts{'debug'},
	"send-status"=>\$opts{'send-status'},
	"mailtest"=>\$opts{'mailtest'},
	"cleanup"=>\$opts{'cleanup'},
	"delete-job"=>\$opts{'delete-job'},
	"rename-job"=>\$opts{'rename-job'},
	"move-archive"=>\$opts{'move-archive'},
	"kill"=>\$opts{'kill'},
	"watchdog=s"=>\$opts{'watchdog'},
	"RetryAttempts=s"=>\$opts{'RetryAttempts'},
	"RetryAfter=s"=>\$opts{'RetryAfter'},
	"chunk-archive"=>\$opts{'chunk-archive'},
	"unchunk-archive"=>\$opts{'unchunk-archive'},
);

my $remoteOS;


our %job;
getDefaultConfig();


if( $opts{'version'} )
	{
	showVersion();
	exit 0;
	}
if( $opts{'help'} )
	{
	showHelp(0);
	exit 0;
	}
if( $opts{'jobs'} )
	{
	listJobs();
	exit 0;
	}
if( $opts{'shorthelp'} )
	{
	showHelp(1);
	exit 0;
	}

lg( $runninglog );

if( $opts{'list-archives'} )
	{
	print listArchives();
	affaExit('Done.');
	}
elsif( $opts{'send-keys'} )
	{
	$jobname = 'send keys';
	sendKeys();
	affaExit('Done.');
	}
elsif( $opts{'rise'} )
	{
	$jobname = 'rising from archive';
	riseFromDeath();
	affaExit('Done.');
	}
elsif( $opts{'undo-rise'} )
	{
	$jobname = 'undo-rise';
	undoRise();
	affaExit('Done.');
	}
elsif( $opts{'disk-usage'} )
	{
	undef $jobname;
	print DiskUsage();
	affaExit('Done.');
	}
elsif( $opts{'status'} )
	{
	undef $jobname;
	print getStatus();
	affaExit('Done.');
	}
elsif( $opts{'send-status'} )
	{
	undef $jobname;
	sendStatus();
	affaExit('Done.');
	}
elsif( $opts{'mailtest'} )
	{
	mailTest();
	affaExit('Done.');
	}
elsif( $opts{'full-restore'} )
	{
	fullRestore();
	affaExit('Done.');
	}
elsif( $opts{'cleanup'} )
	{
	cleanup();
	affaExit('Done.');
	}
elsif( $opts{'delete-job'} )
	{
	deleteJob();
	affaExit('Done.');
	}
elsif( $opts{'rename-job'} )
	{
	renameJob();
	affaExit('Done.');
	}
elsif( $opts{'move-archive'} )
	{
	moveArchive();
	affaExit('Done.');
	}
elsif( $opts{'revoke-keys'} )
	{
	$jobname = 'revoke keys';
	revokeKeys($ARGV[0]||'');
	affaExit('Done.');
	}
elsif( $opts{'check-connections'} )
	{
	$jobname = 'check-connections';
	checkConnectionsAll();
	affaExit('Done.');
	}
elsif( $opts{'create-backup-file'} )
	{
	createBackupFile();
	affaExit('Done.');
	}
elsif( $opts{'kill'} )
	{
	$jobname = 'kill';
	killJob();
	affaExit('Done.');
	}
elsif( $opts{'show-schedule'} )
	{
	showSchedule();
	affaExit('Done.');
	}
elsif( $opts{'chunk-archive'} )
	{
	chunkArchive(0);
	affaExit('Done.');
	}
elsif( $opts{'unchunk-archive'} )
	{
	chunkArchive(1); # 1=unchunk
	affaExit('Done.');
	}

if( $opts{'make-cronjobs'} )
	{
	$jobname = 'make cronjobs';
	cronSetup();
	affaExit('Done.');
	}

if ( not $opts{'run'} or not  $ARGV[0] )
	{
	showHelp(1);
	affaErrorExit( "Missing arguments");
	}



# run job

my $StartTime=time();
$jobname = $ARGV[0];
$jobname =~ /([a-z0-9_\.-]*)/i; $jobname = $1; # untaint
if( not $affa->get($jobname)||'' )
	{
	my $txt= "Job '$jobname' undefined."; print("$txt\n");
	affaErrorExit( "$txt" );
	}

$Command =  defined $ARGV[1] ? lc($ARGV[1]) : 'scheduled';
$Command =~ /([a-z]*)/i; $Command = $1; # untaint
if( not $Command =~ /^(scheduled|daily|weekly|monthly|yearly)$/ )
	{
	affaErrorExit( "Unkown command '$Command'");
	}

getJobConfig( $jobname );
setlog( "$jobname.log" );
{my $txt="# Affa $VERSION #"; lg( '#' x length($txt) ); lg($txt); lg( '#' x length($txt) )}
lg( "Starting job $jobname $Command ($job{'remoteHostName'})" );
lg( "Description: ".$job{'Description'} ) if defined $job{'Description'};
lg( "Bandwidth limit: $job{'BandwidthLimit'} KBytes/sec") if $job{'BandwidthLimit'};

# check if same job already running
$Command eq "scheduled" and getLock($lockfile) 
	and affaErrorExit( "Lock found. Another job (pid=" . getLock($lockfile) . ") is still running." );

setLock() if $Command eq "scheduled";
$SIG{'TERM'} = 'SignalHandler';
$SIG{'INT'} = 'SignalHandler';

if( $opts{'RetryAfter'} )
	{
	lg( "Sleeping $opts{'RetryAfter'} seconds. Continuing at " . Date::Format::time2str("%T",time()+$opts{'RetryAfter'}) );
	sleep( $opts{'RetryAfter'} );
	my $jobconf = $affa->get($jobname);
	$jobconf->set_prop( 'chattyOnSuccess', 1 ) if not $job{'chattyOnSuccess'};
	$affa = esmith::DB::db->open("/home/e-smith/db/affa");
	}
$allow_retry=1;
checkConnection(); # and exit on error

# mount root dir
if( $job{'AutomountDevice'} and $job{'AutomountPoint'} )
	{
	mount( $job{'AutomountDevice'},  $job{'AutomountPoint'}, $job{'AutomountOptions'} );
	}
File::Path::mkpath( "$job{'RootDir'}/$jobname", 0, 0700 ) unless -d "$job{'RootDir'}/$jobname";
esmith::templates::processTemplate({TEMPLATE_PATH=>"/etc/smb.conf"});
system("/usr/local/bin/svc -t /service/smbd");

# run daily, weekly, monthly or yearly if not already done;
if( $Command eq "scheduled" and -f "$job{'RootDir'}/$jobname/scheduled.0/.AFFA-REPORT" )
	{
	$0 =~ /(.*)/; # untaint
	my @cmd=($1, '--run', $jobname, 'yearly' );
	ExecCmd(  @cmd, 1 ) if( $job{'doneYearly'} ne $thisYear and $job{'yearlyKeep'}>0 );
	$cmd[3]='monthly';
	ExecCmd(  @cmd, 1 ) if( $job{'doneMonthly'} ne $thisMonth and $job{'monthlyKeep'}>0 );
	$cmd[3]='weekly';
	ExecCmd(  @cmd, 1 ) if( $job{'doneWeekly'} ne $thisWeek and $job{'weeklyKeep'}>0 );
	$cmd[3]='daily';
	ExecCmd(  @cmd, 1 ) if( $job{'doneDaily'} ne $thisDay and $job{'dailyKeep'}>0 );
	}

### hier geht's wirklich los ###
installWatchdog($opts{'watchdog'}||0);
execPreJobCommand();
my $linkDest='';
if( $Command eq 'scheduled' )
	{
	my $exclude = getExcludedString();
	my $include = getIncludedString();
	$linkDest = getLinkdest(0);
	dbg( "Using link destination $linkDest" ) if $linkDest;
	my @cmd;
	my $status=0;;
	my $rsyncOutput='';
	if( $linkDest && !$ESX && -f "$job{'RootDir'}/$jobname/$linkDest/.AFFA-CHUNK-FLAG" )
		{
		lg( "Link destination contains chunked files. Unchunking now.");
		unchunkfiles( $linkDest, 0);
		lg( "Unchunking done.");
		}

	if( $rsyncd ) # Windows Server with rsyncd installed
		{
		my @SourceDirs=getSourceDirs();
		my $source='';
		foreach my $src (@SourceDirs)
			{
			$src = "/$src" if not $src =~ /^\//;
			$src =~ s/'/'\\''/g; # escape single quotes
			$source .= ($job{'rsyncdUser'} ? $job{'rsyncdUser'}.'@' : '') . $job{'remoteHostName'} . "::'" . $job{'rsyncdModule'} . $src . "' ";
			}
		@cmd=(
			$rsyncLocal,
			"--archive",
			"--stats",
			"--delete-during", "--ignore-errors", 
			"--delete-excluded",
			"--relative",
			"--partial",
			$job{'BandwidthLimit'} ? "--bwlimit=$job{'BandwidthLimit'}" : '',
			$job{'rsync--modify-window'} > 0 ? "--modify-window=$job{'rsync--modify-window'}" : '',
			$job{'rsync--inplace'} ne 'no' ? "--inplace" : "",
			$job{'rsyncTimeout'} ? "--timeout=$job{'rsyncTimeout'}" : "",
			$job{'rsyncCompress'} eq 'yes' ? "--compress" : "",
			"--numeric-ids",
			( $linkDest ? "--link-dest='$job{'RootDir'}/$jobname/$linkDest'" : '' ),
			$include, 
			$exclude, 
			$rsyncOptions,
			$source,
			"$job{'RootDir'}/$jobname/scheduled.running/" 
			);
		$status=ExecCmd(  @cmd, 0 );
		$rsyncOutput=$Affa::lib::ExecCmdOutout;
		}
	elsif( $ESX ) # VMWare ESXi Server
		{
		require Affa::esxi;
		$rsyncOutput=Affa::esxi::syncESXi( $linkDest );
		}
	else # Standard Linux or SME
		{
		signalPreBackupEvent() if !$ESX and $job{'SMEServer'} ne 'no';
		installedRPMsList(0) if !$ESX and $job{'SMEServer'} ne 'no' and $job{'remoteHostName'} ne 'localhost';
		my $SourceDirs = getSourceDirsString()||'/';
		$SourceDirs =~ s/'/'\\''/g if $job{'remoteHostName'} ne 'localhost'; # escape single quotes
		@cmd=(
			$rsyncLocal,
			"--archive",
			"--stats",
			"--delete-during", "--ignore-errors", 
			"--delete-excluded",
			"--relative",
			"--partial",
			$job{'BandwidthLimit'} ? "--bwlimit=$job{'BandwidthLimit'}" : '',
			$job{'rsync--modify-window'} > 0 ? "--modify-window=$job{'rsync--modify-window'}" : '',
			$job{'rsync--inplace'} ne 'no' ? "--inplace" : "",
			$job{'rsyncTimeout'} ? "--timeout=$job{'rsyncTimeout'}" : "",
			$job{'rsyncCompress'} eq 'yes' ? "--compress" : "",
			"--numeric-ids",
			"--rsync-path='$rsyncRemote'",
			$job{'remoteHostName'} eq 'localhost' ? '' : "--rsh='/usr/bin/ssh -o HostKeyAlias=$jobname -p $job{'sshPort'}'",
			( $linkDest ? "--link-dest='$job{'RootDir'}/$jobname/$linkDest'" : '' ),
			$include, 
			$exclude, 
			$rsyncOptions,
			$job{'remoteHostName'} eq 'localhost' ? "$SourceDirs" : $job{'remoteHostName'}.":'$SourceDirs'", 
			"$job{'RootDir'}/$jobname/scheduled.running/" 
			);
		lg( "Running rsync..." );
		$status=ExecCmd(  @cmd, 0 );
		$rsyncOutput=$Affa::lib::ExecCmdOutout;
		signalPostBackupEvent() if $job{'SMEServer'} ne 'no';
		}
	# if nothing was transferred, scheduled.running does not exist
	if( not -d "$job{'RootDir'}/$jobname/scheduled.running" )
		{
		lg( "Error: No data transferred. Check include/exclude settings." );
		$status+=1000;
		}

	if( $status eq '0' or $status  eq  '23' or $status  eq  '24' )
		{
		lg( "writing $job{'RootDir'}/$jobname/scheduled.running/.AFFA-REPORT");
		my $as=open( AS, ">$job{'RootDir'}/$jobname/scheduled.running/.AFFA-REPORT" ) or lg( "Error: Couldn't write .AFFA-REPORT file" );
		if( $as )
			{
			(my $used, my $avail) = df( $job{'RootDir'} );
			print AS "Date: " . Date::Format::time2str("%Y%m%d%H%M",time()) ."\n";
			print AS "ExecutionTime: " . (time()-$StartTime) . "\n";
			print AS "$rsyncOutput";
			print AS "Exit status: $status\n";
			# Versions <= 0.4.0 printed kbytes values but bytes unit
			printf( AS "RootDirFilesystemUsed: %d kbytes\n", $used );
			printf( AS "RootDirFilesystemAvail: %d kbytes\n", $avail );
			# Versions <= 0.4.2 did wrong calculation of RootDirFilesystemUsage
			printf( AS "RootDirFilesystemUsage: %.1f %%\n", $used/($avail+$used)*100 );
            
			close( AS ) or lg( "Error: Couldn't close .AFFA-REPORT file" );
			}
		my $js=open( JS, ">$job{'RootDir'}/$jobname/scheduled.running/$jobname-setup.pl" ) or lg( "Error: Couldn't write $jobname-setup.pl file" );
		if( $js )
			{
			chmod( 0700, "$job{'RootDir'}/$jobname/scheduled.running/$jobname-setup.pl" );
			print JS "#!/usr/bin/perl -w\n";
			print JS "# Created on " .Date::Format::ctime(time()) . "\n";
			print JS "my \$jobname='$jobname';\n";
			print JS "my \%job=(\n";
			foreach my $k (sort keys %job)
				{
				next if not $job{$k} or $k=~/(doneDaily)|(doneWeekly)|(doneMonthly)|(doneYearly)|(chattyOnSucces)/;
				print JS "  '$k'=>'$job{$k}',\n";
				}
			print JS ");\n\n";
			my $in = open( IN, "/usr/lib/affa/jobconfig-sample.pl" );
			if( $in )
				{
				local $/;
				my $if=<IN>;
				$if =~ s/^.*(### nothing to edit below this line ####.*)$/$1/ms;
				print JS $if;
				close(IN);
				}
			close( JS ) or lg( "Error: Couldn't close  $jobname-setup.pl file" );
			}
		compareRPMLists("$job{'RootDir'}/$jobname/scheduled.running");
		}
	else
		{
		affaErrorExit( "rsync failed with status $status.");
		}
	}
chunkfiles($linkDest,0) if $linkDest ne 'scheduled.0' or $job{'scheduledKeep'}>1;
shiftArchives();
putDoneDates( $jobname );
updateReportDB();
execPostJobCommand(0);
DiskSpaceWarn();
sendSuccessMesssage();
affaExit( "Completed job '$jobname $Command ($job{'remoteHostName'})'" );

exit 0; ############################################################################




# get default configuration
sub getDefaultConfig()
	{
	%job=(
		'remoteHostName'=>'localhost',
		'TimeSchedule'=>'2230',
		'RetryAfter'=>600,
		'RetryAttempts'=>3,
		'RetryNotification'=>'yes',
		'Description'=>'',
		'scheduledKeep'=>2,
		'dailyKeep'=>7,
		'weeklyKeep'=>4,
		'monthlyKeep'=>12,
		'yearlyKeep'=>2,
		'SMEServer'=>'yes',
		'RPMCheck'=>'no',
		'DiskSpaceWarn'=>'strict', # strict | normal | risky | none
		'localNice'=>0,
		'remoteNice'=>0,
		'Watchdog'=>'yes',
		'sshPort'=>22,
		'ConnectionCheckTimeout'=>120,
		'rsyncTimeout'=>900,
		'rsyncCompress'=>'no',
		'BandwidthLimit'=>0,
		'EmailAddresses'=>'admin',
		'RootDir'=>'/var/affa',
		'AutomountDevice'=>'',
		'AutomountPoint'=>'',
		'AutomountOptions'=>'',
		'AutoUnmount'=>'yes',
		'postJobCommand'=>'',
		'preJobCommand'=>'',
		'doneDaily'=>-1,
		'doneMonthly'=>-1,
		'doneWeekly'=>-1,
		'doneYearly'=>-1,
		'Debug'=>'no',
		'status'=>'enabled',
		'rsync--inplace'=>'yes', # yes | no : rsync on source supports '--inplace' option
		'rsync--modify-window'=>0, #  seconds : rsync compares mod-times with reduced accuracy
		# rsyncd stuff (Cygwin)
		'rsyncdMode'=>'no',
		'rsyncdModule'=>'AFFA',
		'rsyncdUser'=>'affa',
		'rsyncdPassword'=>'',
		'remoteOS'=>'centos',
		);
	if( my $defaults = $affa->get('DefaultAffaConfig') )
		{
		my %props = $defaults->props;
		foreach my $k ( sort keys %props ) 
			{
			$job{$k} = $defaults->prop($k);
			$job{$k} =~ /(.*)/; $job{$k} = $1; # untaint
			}
		}
	$job{'EmailAddresses'}=$defaultEmail if not $job{'EmailAddresses'};
	$job{'Debug'}='yes' if $opts{'debug'};
	$defaultEmail=$job{'EmailAddresses'};
	}


# get the job configuration
sub getJobConfig( $ )
	{
	my $jobname = shift||'';
	getDefaultConfig();
	if( my $jobconf = $affa->get($jobname) )
		{
		my %props = $jobconf->props;
		foreach my $k ( sort keys %props ) 
			{
			$job{$k} = $jobconf->prop($k);
			$job{$k} =~ /(.*)/; $job{$k} = $1; # untaint
			}
		}
	else
		{
		affaErrorExit( "Job '$jobname' undefined.");
		}
	$job{'sshPort'}=22 if not $job{'sshPort'};
	$job{'EmailAddresses'}=$defaultEmail if not $job{'EmailAddresses'};
	$job{'BandwidthLimit'}=0 if not $job{'BandwidthLimit'};
	$job{'scheduledKeep'}=1 if( $job{'scheduledKeep'}<1 );
	$job{'dailyKeep'}=0 if( $job{'dailyKeep'}<0 );
	$job{'weeklyKeep'}=0 if( $job{'weeklyKeep'}<0 );
	$job{'monthlyKeep'}=0 if( $job{'monthlyKeep'}<0 );
	$job{'yearlyKeep'}=0 if( $job{'yearlyKeep'}<0 );

	# check for remoteHostName==localhost using DNS
	my $res   = Net::DNS::Resolver->new;
	$res->udp_timeout(10);
	if( $job{'remoteHostName'} ne 'localhost' )
		{
		my $query = $res->search($job{'remoteHostName'});
		if ($query) 
			{
        	foreach my $rr ($query->answer) 
				{
            	next unless $rr->type eq "A";
				$job{'remoteHostName'}='localhost' if( $rr->address eq $LocalIP );
        		}
			} 
		else
			{
			dbg("Warning: DNS resolver timeout for $job{'remoteHostName'} ($jobname)");
			}
		}
	# check for remoteHostName==localhost using e-smith DB
	if( $job{'remoteHostName'} =~ /^(127.0.0.1|$LocalIP|$SystemName|$SystemName\.$DomainName)$/ )
		{
		$job{'remoteHostName'}='localhost' 
		}
	$rsyncLocal = ( $job{'localNice'} ? '/bin/nice -' .  $job{'localNice'} . ' ' : '' ) . '/usr/bin/rsync';
	$rsyncRemote = ( $job{'remoteNice'} ? '/bin/nice -' .  $job{'remoteNice'} . ' ' : '' ) . '/usr/bin/rsync';
	$rsyncOptions = $job{'rsyncOptions'}||'';
	$rsyncd = $job{'rsyncdMode'} && $job{'rsyncdMode'} eq 'yes' ? 1 : 0;
	$remoteOS = $opts{'remoteOS'} ? $opts{'remoteOS'} : $job{'remoteOS'}; # option overrides the db value
	$ENV{'RSYNC_PASSWORD'}=$job{'rsyncdPassword'};
	$job{'rsyncdPassword'}='<not shown>';
	$job{'Debug'}='yes' if $opts{'debug'};
	$sshQuiet = $job{'Debug'} eq 'yes' ? '' : '-q';
	$ESX = $job{'ESXi'} && $job{'ESXi'} eq 'yes' ? 1 : 0;
	$job{'ChunkSize'}=900*1024 if( not defined $job{'ChunkSize'} );
	$job{'ChunkThresholdSize'}=2 if( not defined $job{'ChunkThresholdSize'} );
	$ChunkThresholdSize=$job{'ChunkThresholdSize'}*$job{'ChunkSize'};
	$job{'chunkFiles'} .= "/*.vmdk" if $ESX;

	dbg( "Job configuration:" );
	foreach my $k ( sort keys %job )
		{
		dbg( "  $k=$job{$k}" ) if $job{$k};
		}
	$lockfile = "$lockdir/$jobname";

	# avoid calling not suitable functions in ESXi mode
	if( $ESX and ( $opts{'full-restore'} or $opts{'rise'} or $opts{'undo-rise'} or $opts{'create-backup-file'} ) )
		{
		print "Error: The options --full-restore, --rise, --undo-rise and --create-backup-file are not supported with ESXi jobs.\n";
		exit -1;
		}
	}

sub putDoneDates( $ )
	{
	my $jobname = shift;
	my $jobconf = $affa->get($jobname);
	$jobconf->set_prop( 'doneYearly', $thisYear ) if( $Command eq 'yearly' ) ;
	$jobconf->set_prop( 'doneMonthly', $thisMonth ) if( $Command eq 'monthly' ) ;
	$jobconf->set_prop( 'doneWeekly', $thisWeek ) if( $Command eq 'weekly' ) ;
	$jobconf->set_prop( 'doneDaily', $thisDay ) if( $Command eq 'daily' ) ;
	$affa = esmith::DB::db->open("/home/e-smith/db/affa");
	}

sub setLock()
	{
	File::Path::mkpath( $lockdir, 0, 0700 ) if not -d $lockdir;
	open( LOCK, ">$lockfile" ) or die "Error: Couldn't create lockfile $lockfile\n";
	print LOCK "$process_id\n";
	close( LOCK ) or warn "Error: Couldn't close lockfile $lockfile\n";;
	$LockisSet=1;
	}

sub removeLock()
	{
	unlink( $lockfile ) if( $LockisSet );
	}

sub getLock($)
	{
	my $lockfile = shift(@_);
	my $lockpid=0;
	if( open( LOCK, "<$lockfile" ) )
		{
		$lockpid=<LOCK>;
		chomp( $lockpid );
		close( LOCK );
		}
	if( $lockpid )
		{
		if( -f "/proc/$lockpid/stat" )
			{
			if( open( STAT, "</proc/$lockpid/stat" ) )
				{
				my $stat=<STAT>;
				chomp( $stat );
				close( STAT );
				if( not ($stat =~ /^$lockpid \(affa\)/) ) 
					{
					$lockpid=0;
					unlink $lockfile;
					lg( "Orphaned lock found and removed." );
					}
				}
			}
		else 
			{ 
			$lockpid=0; 
			unlink $lockfile;
			lg( "Orphaned lock found and removed." );
			}
		}
	return $lockpid;
	}


sub checkConnection()
	{
	return checkConnection_silent($jobname,0,0);
	}

sub checkConnection_silent($$$)
	{
	return 0 if( $job{'remoteHostName'} eq 'localhost' );
	my ($jobname,$viapi,$silent)=@_;
	my $status=0;
	my @cmd;
	if( $rsyncd )
		{
		lg( "Checking rsyncd connection to " . $job{'remoteHostName'} );
		@cmd=($rsyncLocal, '-dq', ($job{'rsyncdUser'} ? $job{'rsyncdUser'}.'@' : '') . $job{'remoteHostName'} . "::'" . $job{'rsyncdModule'} . "'");
		not ExecCmd( @cmd, 0 ) or affaErrorExit( "Rsyncd connection to ". $job{'remoteHostName'}. " failed. Did you set the rsyncdUser, rsyncdPassword and rsyncdModule properties correctly?" );
		}
	else
		{
		lg( "Checking SSH connection to " . $job{'remoteHostName'} );
		@cmd=('/usr/bin/ssh', '-p', $job{'sshPort'}, '-o', 'StrictHostKeyChecking=yes', '-o', "HostKeyAlias=$jobname", '-o', "ConnectTimeout=$job{'ConnectionCheckTimeout'}",'-o','PasswordAuthentication=no', $sshQuiet, $job{'remoteHostName'},'echo OK');
		ExecCmd( @cmd,0); chomp($Affa::lib::ExecCmdOutout);
		if( $Affa::lib::ExecCmdOutout ne "OK" )
			{
			$status=-1;
			if( !$silent )
				{
				affaErrorExit( "SSH connection to ". $job{'remoteHostName'}. " failed. Did you send the public key?" );
				}
			}
		if( $ESX && $viapi )
			{
			lg( "Checking VI API connection to ESXi host " . $job{'remoteHostName'} );
			require Affa::esxi;
			Affa::esxi::checkAPIConnection( $job{'ESXiUsername'}, $job{'ESXiPassword'} ) or affaErrorExit( "VI API connection failed." );
			}
		}
	return $status;
	}

sub signalPreBackupEvent()
	{
	# requires bash shell on remote host
	my @cmd;
	if( $job{'remoteHostName'} eq 'localhost' )
		{
		lg( "signaling pre-backup event on localhost" );
		@cmd=('/sbin/e-smith/signal-event',"pre-backup", "desktop");
		}
	else
		{
		lg( "signaling pre-backup event on ". $job{'remoteHostName'} );
		@cmd=('/usr/bin/ssh', '-o', "HostKeyAlias=$jobname", '-p', $job{'sshPort'}, $sshQuiet, $job{'remoteHostName'},"/sbin/e-smith/signal-event pre-backup desktop");
		}
	not ExecCmd( @cmd, 0 ) or affaErrorExit( "signaling pre-backup event failed." );
	}

sub signalPostBackupEvent()
	{
	# requires bash shell on remote host
	my @cmd;
	if( $job{'remoteHostName'} eq 'localhost' )
		{
		lg( "signaling post-backup event on localhost" );
		@cmd=('/sbin/e-smith/signal-event',"post-backup");
		}
	else
		{
		lg( "signaling post-backup event on ". $job{'remoteHostName'} );
		@cmd=('/usr/bin/ssh', '-o', "HostKeyAlias=$jobname", '-p', $job{'sshPort'}, $sshQuiet, $job{'remoteHostName'},"/sbin/e-smith/signal-event post-backup");
		}
	not ExecCmd( @cmd, 0 ) or lg( "Error: signaling post-backup event failed." );
	}

sub installedRPMsList($)
	{
	return if $job{'SMEServer'} eq 'no' or $job{'RPMCheck'} ne 'yes';
	my $forceLocal=shift(@_);
	# requires bash shell on remote host
	my @cmd;
	if( $job{'remoteHostName'} eq 'localhost' or $forceLocal )
		{
		lg( "writing list of installed RPMs on localhost ($rpmlist)" );
		@cmd=( "/sbin/e-smith/affa-rpmlist.sh", ">$rpmlist" );
		}
	else
		{
		lg( "writing list of installed RPMs on ". $job{'remoteHostName'} . " ($rpmlist)");
		remoteCopy( "/sbin/e-smith/affa-rpmlist.sh", "/sbin/e-smith/affa-rpmlist.sh");
		@cmd=(
			'/usr/bin/ssh',
			'-o', "HostKeyAlias=$jobname",
			'-p', $job{'sshPort'},
			$sshQuiet, 
			$job{'remoteHostName'},
			"'/sbin/e-smith/affa-rpmlist.sh>$rpmlist'"
			);
		}
	not ExecCmd( @cmd, 0 ) or affaErrorExit( "writing list of installed RPMs failed." );
	}


sub compareRPMLists($)
	{
	return if $job{'SMEServer'} eq 'no' or $job{'remoteHostName'} eq 'localhost' or $job{'RPMCheck'} ne 'yes';
	my $localRPMList=shift;
	lg( "Comparing installed RPMs on backup und remote host" );
	my $RPMPath="home/e-smith/db/affa-rpmlist";
	my %remoteRPM;
	dbg( "Reading remote RPM list" );
	open( RP, "$localRPMList/$RPMPath" ) or affaErrorExit( "Couldn't open $localRPMList/$RPMPath" );
	while( <RP> )
		{
		my @z = split( " ", $_);
		$remoteRPM{$z[0]}=$z[1];
		}
	close( RP );
	installedRPMsList(1);
	my %localRPM;
	dbg( "Reading local RPM list" );
	open( RP, "</$RPMPath" ) or lg( "Error: Couldn't open /$RPMPath" );
	while( <RP> )
		{
		my @z = split( " ", $_);
		$localRPM{$z[0]}=$z[1];
		}
	close( RP );
	# vergleichen
	my (@missing, @VersionMismatch);
	my $md5 = Digest::MD5->new;
	foreach my $p ( keys %remoteRPM ) 
		{
		if( not $localRPM{$p} )
			{
			push( @missing, "$p-".$remoteRPM{$p});
			$md5->add("$p-".$remoteRPM{$p});
			}
		elsif( $localRPM{$p} ne $remoteRPM{$p} )
			{
			push( @VersionMismatch, "$p-".$remoteRPM{$p});
			$md5->add("$p-".$remoteRPM{$p});
			}
		}
	my $RPMFilename="$job{'RootDir'}/$jobname/rpms-missing.txt";
	my $MD5Filename="$job{'RootDir'}/$jobname/.md5-rpms-missing-".$md5->hexdigest;
	dbg("RPMFilename=$RPMFilename");
	dbg("MD5Filename=$MD5Filename");
	if( not -f $MD5Filename )
		{
		open( RP, ">$RPMFilename" ) or affaErrorExit( "Couldn't open $RPMFilename for writing.");

		my $out='';
		foreach my $k (@missing)
			{
			$out .= "$k\n";
			}
		if( $out )
			{
			print RP "* \n";
			print RP "* The following packages are installed on ".$job{'remoteHostName'}.",\n";
			print RP "* but they are missing on this backup host:\n";
			print RP "* \n";
			print RP $out;
			}

		$out='';
		foreach my $k (@VersionMismatch)
			{
			$out .= "$k\n";
			}
		if( $out )
			{
			print RP "\n* \n";
			print RP "* The following packages are installed on both,\n";
			print RP "* the source ".$job{'remoteHostName'}." and on this backup host,\n";
			print RP "* but the version does not match:\n";
			print RP "* \n";
			print RP $out;
			}

		close( RP );

		if( $job{'RPMCheck'} eq 'yes' and not -f $MD5Filename and $job{'EmailAddresses'} )
			{
			my $msg = new Mail::Send;
			$msg->subject("Missing RPMs on $SystemName.$DomainName ($LocalIP) compared with ". $job{'remoteHostName'});
			$msg->to($job{'EmailAddresses'});
			$msg->set("From", "\"Affa Backup Server\" <noreply\@$SystemName.$DomainName>");
			my $fh = $msg->open;
			open( RP, "<$RPMFilename" ) or affaErrorExit( "Couldn't open $RPMFilename.");
			while( <RP> )
				{
				print $fh $_;
				}
			close( RP );
			$fh->close; 
			lg( "RPM check message sent to " . $job{'EmailAddresses'} );
			}

		unlink( glob( "$job{'RootDir'}/$jobname/.md5-rpms-missing-*" ) );
		open( RP, ">$MD5Filename" ) or affaErrorExit( "Couldn't open $MD5Filename for writing.");
		print RP "md5sum of content of file $RPMFilename\n";
		close( RP );
		}
	}


# get directories and files to backup from db
# Add standard dirs if SME.
sub getSourceDirs()
	{
	my @SourceDirs=();
	if( $job{'SMEServer'} ne 'no' )
		{
		my $b = new esmith::Backup or die "Error: Couldn't create Backup object\n";
		foreach my $k ($b->restore_list)
			{
			$k = "/$k" if not $k =~ /^\//;
			push( @SourceDirs, $k );
			}
		}
	foreach my $k ( sort keys %job ) 
		{ 
		push( @SourceDirs, $job{$k} ) if $k =~ /^Include/;
		}
	my @result=();
	foreach my $k (@SourceDirs)
		{
		trim( $k );
		next if not $k =~ /^\//;
		$k =~ s/"/\\"/g;
		push(@result, $k) if $k;
		}
	return @result;
	}

sub getSourceDirsString()
	{
	my @SourceDirs=getSourceDirs();
	my $result='';
	foreach my $k (@SourceDirs)
		{
		next if not $k =~ /^\//;
		$result .= '"' . $k . '" ';
		}
	return trim($result);
	}

# get files to include 
sub getIncludedString()
	{
	my @include=();
	foreach my $k ( sort keys %job ) 
		{ 
		push( @include, $job{$k} ) if $k =~ /^Include/ && not $job{$k} =~ /^\//;
		}
	my $result='';
	foreach my $k (@include)
		{
		$k =~ s/"/\\"/g;
		$result .= '--include="'.$k.'" ' if $k;
		}
	chomp( $result );
	return trim($result);
	}

# get directories and files to exclude 
sub getExcludedString()
	{
	my @exclude=();
	foreach my $k ( sort keys %job ) 
		{ 
		$exclude[@exclude] = $job{$k} if $k =~ /^Exclude/;
		}
	my $result='';
	foreach my $k (@exclude)
		{
		$k =~ s/^\///;
		$k =~ s/\/$//;
		$k =~ s/"/\\"/g;
		$result .= '--exclude="'.$k.'" ' if $k;
		}
	chomp( $result );
	return trim($result);
	}

sub getLinkdest($)
	{
	my $prev=shift;
	my $dir = opendir( DIR, "$job{'RootDir'}/$jobname" );
	my $ar;
	my $ard=0;
	my $res='';
	my $prev_res='';
	while( defined ($ar=readdir(DIR)) )
		{
		next if not $ar =~ /^(scheduled|daily|weekly|monthly|yearly)\.[0-9]+$/;
		if( -f "$job{'RootDir'}/$jobname/$ar/.AFFA-REPORT" )
			{
			open( AS, "<$job{'RootDir'}/$jobname/$ar/.AFFA-REPORT" );
			my $d=<AS>; chomp($d); $d=~s/Date: //;
			close( AS );
			if( $d=~/^\d{12}$/ and $d>$ard )
				{
				$ard=$d;
				$prev_res=$res;
				$res=$ar;
				}
			}
		}
	return $prev ? $prev_res : $res;
	}

sub shiftArchives()
	{
	lg( "Shifting backup archives...");
	my $nothingDone = "Nothing to be done.";
	my $JobDir = "$job{'RootDir'}/$jobname";
	my $basename = "$JobDir/$Command";
	if( -d "$basename.0" and ( $Command ne 'scheduled' or -f "$JobDir/scheduled.running/.AFFA-REPORT" ) )
		{
		my $keep = $job{$Command.'Keep'}-1;
		if( -d "$basename.$keep" )
			{
			$nothingDone='';
			moveFileorDir( "$basename.$keep", "$basename.$keep.deleted" );
			removeDir( "$basename.$keep.deleted" );
			}
		for( my $i=$keep; $i>0; $i-- )
			{
			if( -d "$basename." . ($i-1) )
				{
				$nothingDone='';
				moveFileorDir( "$basename." . ($i-1), "$basename.$i");
				}
			}
		}
	if( $Command eq 'scheduled' and not -d "$JobDir/scheduled.0" and -f "$JobDir/scheduled.running/.AFFA-REPORT" )
		{
		moveFileorDir( "$JobDir/scheduled.running", "$JobDir/scheduled.0" );
		$nothingDone='';
		}
	if( $Command eq 'yearly' )
		{
		my $src = "$JobDir/monthly.".($job{'monthlyKeep'}-1);
		$src = "$JobDir/weekly.".($job{'weeklyKeep'}-1) if (not -d $src) && !$job{'monthlyKeep'};
		$src = "$JobDir/daily.".($job{'dailyKeep'}-1) if (not -d $src) && !$job{'monthlyKeep'} && !$job{'weeklyKeep'};
		$src = "$JobDir/scheduled.".($job{'scheduledKeep'}-1) if (not -d $src) && !$job{'monthlyKeep'} && !$job{'weeklyKeep'} && !$job{'dailyKeep'};
		if( -d $src )
			{
			moveFileorDir( $src, "$basename.0" );
			$nothingDone='';
			}
		}
	elsif( $Command eq 'monthly' )
		{
		my $src = "$JobDir/weekly.".($job{'weeklyKeep'}-1);
		$src = "$JobDir/daily.".($job{'dailyKeep'}-1) if (not -d $src) && !$job{'weeklyKeep'};
		$src = "$JobDir/scheduled.".($job{'scheduledKeep'}-1) if (not -d $src) && !$job{'weeklyKeep'} && !$job{'scheduledKeep'};
		if( -d $src )
			{
			moveFileorDir( $src, "$basename.0" );
			$nothingDone='';
			}
		}
	elsif( $Command eq 'weekly' )
		{
		my $src = "$JobDir/daily.".($job{'dailyKeep'}-1);
		$src = "$JobDir/scheduled.".($job{'scheduledKeep'}-1) if (not -d $src) && !$job{'dailyKeep'};
		if( -d $src )
			{
			moveFileorDir( $src, "$basename.0" );
			$nothingDone='';
			}
		}
	elsif( $Command eq 'daily' and -d "$JobDir/scheduled.".($job{'scheduledKeep'}-1) )
		{
		moveFileorDir( "$JobDir/scheduled.".($job{'scheduledKeep'}-1), "$basename.0" );
		$nothingDone='';
		}
	lg( $nothingDone ) if $nothingDone;
	}

sub installWatchdog($)
	{
	return if $ESX or $job{'Watchdog'} ne 'yes' or $job{'SMEServer'} ne 'yes' or $job{'remoteHostName'} eq 'localhost';
	my $t=shift;
	my $nextScheduled=($t||'86400')+600;
	lg( "Installing watchdog on ".$job{'remoteHostName'} . " with dt=$nextScheduled seconds" );

	my $scheduled=Date::Format::ctime(time()); chomp($scheduled);
	my $WDName = "affa-watchdog-$jobname-$LocalIP";
	my $trigger = Date::Format::time2str("%Y%m%d%H%M",time()+$nextScheduled);
	open( WD, "</usr/lib/affa/watchdog.template" ) or warn "Error: Couldn't open /usr/lib/affa/watchdog.template\n";
	open( WDS, ">/tmp/$$.$WDName" ) or warn "Error: Couldn't open /tmp/$$.$WDName for writing\n";
	dbg( "Watchdog parameters:" );
	while( <WD> )
		{
		$_ =~ s/^use constant _TRIGGER=>.*$/use constant _TRIGGER=>$trigger;/;
		$_ =~ s/^use constant _JOBNAME=>.*$/use constant _JOBNAME=>'$jobname';/;
		$_ =~ s/^use constant _EMAIL=>.*$/use constant _EMAIL=>\'$job{'EmailAddresses'}\';/;
		$_ =~ s/^use constant _BACKUPHOST=>.*$/use constant _BACKUPHOST=>'$SystemName.$DomainName ($LocalIP)';/;
		$_ =~ s/^use constant _SCHEDULED=>.*$/use constant _SCHEDULED=>'$scheduled';/;
		$_ =~ s/^use constant _WDSCRIPT=>.*$/use constant _WDSCRIPT=>'$WDName';/;
		print WDS $_;
		if( $_ =~ /^use constant/ )
			{
			(my $p = $_) =~ s/^use constant(.*);/$1/;
			chomp($p);
			dbg( "  " . trim($p) );
			}
		}
	close( WDS );
	close( WD );
	chmod( 0700, "/tmp/$$.$WDName" );
	my @cmd=('/usr/bin/ssh', '-o', "HostKeyAlias=$jobname", '-p', $job{'sshPort'}, $job{'remoteHostName'}, "/bin/rm", "-f", "/etc/cron.hourly/$WDName-reminder" ); 
	not ExecCmd( @cmd, 0 ) or affaErrorExit( "Couldn't delete /etc/cron.hourly/$WDName-reminder on remote host." );
	remoteCopy("/tmp/$$.$WDName","/etc/cron.hourly/$WDName" );
	unlink("/tmp/$$.$WDName");
	}

sub updateReportDB()
	{
	return if not $jobname or ($jobname eq $ServerBasename) or not $affa->get($jobname)||'';
	if( $job{'AutomountDevice'} and $job{'AutomountPoint'} )
		{
		mount( $job{'AutomountDevice'},  $job{'AutomountPoint'}, $job{'AutomountOptions'} );
		}
	my $dir = opendir( DIR, "$job{'RootDir'}/$jobname" );
	if( not $dir )
		{
		lg( "Couldn't open directory $job{'RootDir'}/$jobname. Cannot update report database. Continuing with the existing database." );
		return;
		}
	File::Path::mkpath( "/home/e-smith/db/affa-report", 0, 0700 ) if not -d "/home/e-smith/db/affa-report";
	unlink( "/home/e-smith/db/affa-report/$jobname" );
	my $affareport = esmith::DB::db->create("/home/e-smith/db/affa-report/$jobname") or die "Error: Couldn't create /home/e-smith/db/affa-report/$jobname db file.";
	my $archive;
	while( defined ($archive=readdir(DIR)) )
		{
		next if not $archive =~ /^(scheduled|daily|weekly|monthly|yearly)\.[0-9]+$/;
		my $report = $affareport->new_record($archive);
		$report->set_prop('type','affabackup');
		$report->set_prop('RootDir',"$job{'RootDir'}/$jobname/$archive/");
		my %pv;
		if( -f "$job{'RootDir'}/$jobname/$archive/.AFFA-REPORT" )
			{
			open( AS, "<$job{'RootDir'}/$jobname/$archive/.AFFA-REPORT" );
			while( <AS> )
				{
				next if /^[ \t]*#/ or not /: /;
				chomp($_);
				$_=~/(.*?):(.*)/;
				my $prop=trim($1);
				my $val=trim($2);
				next if not $val =~ /^[0-9]/ and not $prop =~ /^Date/;
				$val=~/([0-9.]+)(.*)/;
				$val = $1;
				my $unit=$2;
				my @ps = split( " ", $prop );
				$prop='';
				foreach my $k (@ps)
					{
					$prop .= ucfirst($k);
					}
				$val += $pv{$prop} if $pv{$prop};
				$pv{$prop} = $val;
				$report->set_prop($prop,"$val$unit");
				}
			close(AS);
			$report->set_prop('valid','yes');
			}
		else
			{
			$report->set_prop('valid','no');
			}
		}
	}

sub execPreJobCommand()
	{
	return if not $jobname or $Command ne "scheduled" or not $job{'preJobCommand'};
	lg( "Executing preJobCommand $job{'preJobCommand'}");
	affaErrorExit( "Couldn't execute preJobCommand $job{'preJobCommand'}") if not -x $job{'preJobCommand'};
	my @cmd = ( $job{'preJobCommand'}, $job{'remoteHostName'}, $jobname, $Command );
	not ExecCmd( @cmd, 0 ) or affaErrorExit( "Executing preJobCommand $job{'preJobCommand'} failed." );
	}

sub execPostJobCommand($)
	{
	return if not $jobname or $Command ne "scheduled" or not $job{'postJobCommand'};
	lg( "Executing postJobCommand $job{'postJobCommand'}");
	affaErrorExit( "Couldn't execute postJobCommand $job{'postJobCommand'}") if not -x $job{'postJobCommand'};
	my @cmd = ( $job{'postJobCommand'}, $job{'remoteHostName'}, $jobname, $Command, shift(@_) );
	$job{'postJobCommand'}=''; # don't execute again in affaErrorExit()
	not ExecCmd( @cmd, 0 ) or affaErrorExit( "Executing postJobCommand $job{'postJobCommand'} failed." );
	}

sub listJobs()
	{
	$interactive=1;
	lg( "listJobs");
	my %all = $affa->as_hash();
	foreach my $job( sort keys %all )
		{
		next if $job eq $ServerBasename;
		my $v=$all{$job};
		next if $v->{'type'} ne 'job';
		print "$job\n"
		}
	}

sub listArchives()
	{
	$interactive=1;
	if( not $ARGV[0] )
		{
		my %all = $affa->as_hash();
		foreach my $job( sort keys %all )
			{
			next if $job eq $ServerBasename;
			my $v=$all{$job};
			next if $v->{'type'} ne 'job';
			push( @ARGV, $job );
			}
		}

	my $out='';
	foreach my $arg (@ARGV )
		{
		my @csv = listArchivesRaw($arg);
		if( $opts{'csv'} )
			{
			$out = join( "\n", @csv ) .  "\n";
			}
		else
			{
			$out .= $out ? "\n" : "$affaTitle\n";
			shift(@csv);
			my $h;
			($h = sprintf "+-%076s-+\n", '-') =~ s/0/-/g;
			$out .= $h;
			$out .= sprintf( "| Job: %-71s |\n", $jobname );
			$out .= sprintf( "| Description: %-63s |\n", $job{'Description'} ) if $job{'Description'}; 
			$out .= sprintf( "| Directory: %-65s |\n", $job{'RootDir'}."/$jobname/" );
			$out .= sprintf( "| Hostname: %-66s |\n", $job{'remoteHostName'} );
			if( $job{'AutomountDevice'} and $job{'AutomountPoint'} )
				{
				$out .= sprintf( "| AutomountDevice: %-59s |\n", $job{'AutomountDevice'} );
				$out .= sprintf( "| AutomountPoint: %-60s |\n", $job{'AutomountPoint'} );
				$out .= sprintf( "| AutoUnmount: %-63s |\n", $job{'AutoUnmount'} );
				}
			my @em=split( /,/, $job{'EmailAddresses'} );
			my $etxt="Email:";
			foreach my $s (@em)
				{
				$out .= sprintf( "| %6s %-69s |\n", $etxt, trim($s) );
				$etxt='';
				}
			($h = sprintf "+-%05s-+-%021s-+-%09s-+-%014s-+-%06s-+-%06s-+\n", '-','-','-','-','-','-') =~ s/0/-/g;
			my $out2='';
			my $lastArchive='';
			my $tag=0;
			foreach my $k (@csv)
				{
				my @c=split(";", $k );
				my $valid = $c[7] ne "yes" ? 0 : 1;
				my $date = ($valid and $c[2] ne '197001010000' ) ? formatHTime($c[2]) : "Incomplete!";
				my $NumberOfFiles = ($valid and $c[3]) ? countUnit($c[3]) : '-';
				my $TotalFileSize = ($valid and $c[4]) ? sizeUnit($c[4]) : '-';
				my $TotalBytesReceived = ($valid and $c[8]>=0) ? sizeUnit($c[8]) : '-';
				my $DiskUsage = ($valid and $c[5] and $c[6]) ? sizeUnit($c[6]*1024) . "/" . int($c[6]/($c[5]+$c[6])*100) . "%" : '-';
				my $idx = sprintf '%s%2d', uc(substr($c[0],0,1)),$c[1];
				my $ExecTime = $c[9] eq '-' ? '-' : timeUnit($c[9]);
				if( $c[1]>=$job{$c[0]."Keep"} )
					{
					$idx .= " *";
					$tag++;
					}
				if( $lastArchive ne $c[0] )
					{
					$lastArchive=$c[0];
					$out2 .= $h;
					}
				$out2 .= sprintf( "| %-5s | %-21s | %9s | %14s | %6s | %6s |\n", $idx,$date,$ExecTime,$NumberOfFiles,$TotalFileSize,$TotalBytesReceived );
				}
			if( $lastArchive )
				{
				$out .= sprintf "| %-76s |\n", "Archives with an index greater than the Keep value are tagged with '*'" if $tag;
				$out .= $h;
				$out .= sprintf "| %-5s | %-21s | %9s | %14s | %6s | %6s |\n", "Run","Completion date", "Exec Time", "Files", "Size", "Recvd";
				}
			else
				{
				($h = sprintf "+-%076s-+\n", '-') =~ s/0/-/g;
				}
			$out2 .= $h;
			$out = $out.$out2;
			}
		}
	return $out;
	}

sub listArchivesRaw($)
	{
	$jobname=shift @_ ||'';
	if( not $affa->get($jobname)||'' )
		{
		my $txt= "Error: Job '$jobname' undefined."; print("$txt\n");
		affaErrorExit( "$txt" );
		}
	getJobConfig( $jobname );
	updateReportDB();
	my $affastatus = esmith::DB::db->open("/home/e-smith/db/affa-report/$jobname");
	my @allstats = $affastatus->get_all if $affastatus;
	my @out = ("Archive:Count;Date;Files;Size;RootDirFilesystemAvail;RootDirFilesystemUsed;valid;TotalBytesReceived;ExecutionTime");
	my %js;
	foreach my $record ( sort @allstats )
		{
		my $k=$record->key;
		$k =~ s/scheduled/A/; $k =~ s/daily/B/; $k =~ s/weekly/C/; $k =~ s/monthly/D/; $k =~ s/yearly/E/;
		(my $a, my $b) = split(/\./,$k);
		$js{$a.sprintf("%05d",$b)}=$record;
		}
	foreach my $k ( reverse sort keys %js )
		{
		my $v=$js{$k};
		(my $a, my $b) = split(/\./,$v->key);
		(my $TotalFileSize=$v->prop('TotalFileSize')||0) =~ s/.*?(\d*).*/$1/;
		(my $TotalBytesReceived=$v->prop('TotalBytesReceived')||0) =~ s/.*?(\d*).*/$1/;
		(my $RootDirFilesystemAvail=$v->prop('RootDirFilesystemAvail')||0) =~ s/.*?(\d*).*/$1/;
		(my $RootDirFilesystemUsed=$v->prop('RootDirFilesystemUsed')||0) =~ s/.*?(\d*).*/$1/;
		push( @out,  "$a;$b" 
			. ";". ($v->prop('Date')||'197001010000')
			. ";". ($v->prop('NumberOfFiles')||0)
			. ";". $TotalFileSize
			. ";". $RootDirFilesystemAvail
			. ";". $RootDirFilesystemUsed
			. ";". $v->prop('valid') 
			. ";". $TotalBytesReceived
			. ";". ($v->prop('ExecutionTime')||'-') 
			);
		}
	return @out;
	}


sub getStatus()
	{
	$interactive=1;
	my @csv = getStatusRaw();
	my $out = "$affaTitle\n";
	if( $opts{'csv'} )
		{
		$out = join( "\n", @csv ) .  "\n";
		}
	else
		{
		shift(@csv);
		my $jobcolw=2;
		foreach my $k (@csv)
			{
			my @c=split(";", $k );
			$jobcolw = length($c[0]) if $jobcolw<length($c[0]);
			}
		$jobcolw=17 if( $jobcolw>17 );
		(my $h = sprintf "+-%0".$jobcolw."s-+-%03s-+-%05s-+-%09s-+-%05s-+-%05s-+-%014s-+\n", '-', '-', '-', '-','-','-','-') =~ s/0/-/g;
		$out .= $h;
		$out .= sprintf "| %-".$jobcolw."s | %3s | %5s | %9s | %5s | %5s | %14s |\n", "Job", "ENA", "Last", "Exec Time", "Next", "Size", "N of S,D,W,M,Y";
		$out .= $h;
		my $eo='';
		my $do='';
		foreach my $k (sort @csv)
			{
			(my $thisjob,my $status,my $lastrun, my $netxrun, my $TotalFileSize,my $avail, my $used, my $nof, my $lock, my $TotalExecTime)=split(";", $k );
			$thisjob = length($thisjob)>$jobcolw ? substr($thisjob,0,$jobcolw-2).".." : $thisjob;
			$TotalFileSize=$TotalFileSize ? sizeUnit($TotalFileSize) : '-';
			my $DiskUsage = '-';
			my $line='';
			$TotalExecTime = timeUnit($TotalExecTime) if $TotalExecTime ne '-';
			if( not $lock )
				{
				$line = sprintf "| %-".$jobcolw."s | %3s | %5s | %9s | %5s | %5s | %14s |\n", 
				$thisjob, $status, $lastrun, $TotalExecTime, $netxrun, $TotalFileSize, $nof;
				}
			else
				{
				$line = sprintf "| %-".$jobcolw."s | %3s | %-33s | %14s |\n", 
				$thisjob, $status,  "running (pid $lock)", $nof;
				}
			if( $status eq "no" )
				{
				$do.=$line;
				}
			else
				{
				$eo.=$line;
				}
			}
		$do = $h.$do if $do && $eo;
		$out .= $eo.$do.$h;
		}
	return $out;
	}

sub getStatusRaw()
	{
	getDefaultConfig(); # defaults into %job
	my $txt;
	my $lock;
	my $lockpid=0;
	my $allDisabled=0;
	my %all = $affa->as_hash();
	if( ($all{"AffaGlobalDisable"}->{'type'}||'no') eq 'yes' )
		{
		$allDisabled=1;
		}
	my @out="Job;Enabled;Last;Next;Size;RootDirFilesystemAvail;RootDirFilesystemUsed;NumberOfArchives;lockpid;TotalExecTime";
	foreach my $cj( reverse sort keys %all )
		{
		next if $cj eq $ServerBasename;
		my $v=$all{$cj};
		next if $v->{'type'} ne 'job';
		my $affastatus = esmith::DB::db->open("/home/e-smith/db/affa-report/$cj");

		# lockpid
		my $lock = getLock("$lockdir/$cj");

		# status
		my $status=($v->{'status'}||$job{'status'}) eq "enabled" ? "yes" : "no";


		# Number of archives
		my @allstats = $affastatus->get_all if $affastatus;
		my %acnt=('scheduled'=>0,'daily'=>0,'weekly'=>0,'monthly'=>0,'yearly'=>0);
		my $total=0;
		foreach my $record ( sort @allstats )
			{
			(my $k=$record->key)=~s/\.[0-9]$//;
			$acnt{$k}++;
			$total++;
			}

		# Last and next run
		my $nowTime = Date::Format::time2str("%H%M",time());
		my @u = split(",",$v->{'TimeSchedule'}||$job{'TimeSchedule'}||'');
		my @s; 
		foreach my $z (sort @u)
			{
			$z=trim($z);
			push( @s, $z ) if( length( $z) == 4  and $z == sprintf( "%04d", int($z) ) );
			}
		@u = sort { $a <=> $b } @s;
		push( @u, $u[0] );
		(my $netxrun=$u[0]) =~ s/(..)(..)/$1:$2/;
		for( my $i=0; $i<@u; $i++ )
			{
			if( $nowTime < $u[$i] )
				{
				($netxrun = $u[$i]) =~ s/(\d\d)(\d\d)/$1:$2/;
				last;
				}
			}
		my $sched = $affastatus->get('scheduled.0') if $affastatus;
		my %props;
		my $lastrun= $total ? "ERROR" : 'never';
		if( $sched )
			{
			%props = $sched->props;
			$lastrun = $props{'Date'};
			if( not $lastrun or $curtime-hTime2Timestamp($lastrun) > 86400 ) 
				{
				$lastrun = $status eq "yes" ? "ERROR" : '-';
				}
			else
				{
				$lastrun =~ s/\d{8}(\d\d)(\d\d)/$1:$2/;
				}
			}

		# Size
		(my $TotalFileSize=$props{'TotalFileSize'}||0) =~ s/.*?(\d*).*/$1/;
		$TotalFileSize = int($TotalFileSize);

		# Disk usage
		(my $used=$props{'RootDirFilesystemUsed'}||'-') =~ s/ .*//;
		(my $avail=$props{'RootDirFilesystemAvail'}||'-') =~ s/ .*//;

		# Total execution time
		my $TotalExecTime=$props{'ExecutionTime'}||'-';

		if( not $allDisabled or $lock )
			{
			my $nof = sprintf "%2d,%2d,%2d,%2d,%2d", $acnt{'scheduled'}, $acnt{'daily'}, $acnt{'weekly'}, $acnt{'monthly'}, $acnt{'yearly'};
			push( @out, "$cj;$status;$lastrun;$netxrun;$TotalFileSize;$avail;$used;$nof;$lock;$TotalExecTime");
			}
		}
	getJobConfig( $jobname ) if $jobname; # restore config
	return @out;
	}

sub df($)
	{
	my $dir=shift;
	my $df = new Filesys::DiskFree;
	$df->df();
	return (int($df->used($dir)/1024),int($df->avail($dir)/1024));
	}

sub DiskUsage()
	{
	my @csv=DiskUsageRaw();
	my $out = "$affaTitle\n";
	if( $opts{'csv'} )
		{
		$out = join( "\n", @csv ) .  "\n";
		}
	else
		{
		(my $h = sprintf "+-%04s-+-%06s-+-%06s-+-%050s-+\n", '-','-','-','-') =~ s/0/-/g;
		$out .= $h;
		$out .= sprintf "| %4s | %6s | %6s | %-50s |\n", "Use%", "Used", "Avail", "Root Dir";
		$out .= $h;
		shift(@csv);
		foreach my $k (@csv)
			{
			my @c=split(";", $k );
			$c[0]=~s/"//g;
			$c[0] =~ s/.*(.{47})$/...$1/ if length($c[0])>50;
			if( $c[1] eq '-' or $c[2] eq '-' )
				{
				$out .= sprintf "| %4s | %6s | %6s | %-50s |\n",'-','-','-',$c[0];;
				}
			else
				{
				$out .= sprintf "| %4s | %6s | %6s | %-50s |\n",
					int($c[2]/($c[1]+$c[2])*100)."%", # Use%
					sizeUnit($c[2]*1024), # Used
					sizeUnit($c[1]*1024), # Avail
					$c[0];
				}
			}
		$out .= $h;
		}
	return $out;
	}

sub DiskUsageRaw()
	{
	getDefaultConfig(); # defaults into %job
	my %all = $affa->as_hash();
	my @out=('RootDir;RootDirFilesystemAvail;RootDirFilesystemUsed');
	my %done;
	foreach my $cj( reverse sort keys %all )
		{
		next if $cj eq $ServerBasename;
		my $v=$all{$cj};
		next if $v->{'type'} ne 'job';
		my $RootDir = $v->{'RootDir'}||$job{'RootDir'};
		next if $done{$RootDir};
		$done{$RootDir}=1;
		my $used = '-';
		my $avail = '-';
		my $dev = $v->{'AutomountDevice'}||$job{'AutomountDevice'};
		my $mountPoint = $v->{'AutomountPoint'}||$job{'AutomountPoint'};
		my $mountOptions = $v->{'AutomountOptions'}||$job{'AutomountOptions'};
		if( $RootDir && $mountPoint && $RootDir =~ /$mountPoint/ )
			{
			mount($dev,$mountPoint, $mountOptions) if $dev and $mountPoint;
			($used, $avail) = df( $RootDir) if isMounted($dev,$mountPoint);
			unmount($dev,$mountPoint) if $dev and $mountPoint;
			}
		elsif( $RootDir && -x $RootDir )
			{
			($used, $avail) = df( $RootDir);
			}
		push( @out,"\"$RootDir\";$avail;$used");
		}
	getJobConfig( $jobname ) if $jobname; # restore config
	return @out;
	}


sub isMounted($$)
	{
	(my $dev, my $AutomountPoint) = @_;
	$AutomountPoint =~ s/\/$//;
	my $txt="Check mounted: $dev $AutomountPoint";
	my $df = new Filesys::DiskFree;
	my $result=0;
	$df->df();
	if( $df->device($AutomountPoint) =~ "^/dev/mapper/" && not ($dev =~ "^/dev/mapper") )
		{
		# convert /dev/mapper/VG-LV to /dev/VG/LV
		(my $d=$df->device($AutomountPoint)) =~ s;^/dev/mapper/(.*)-(.*)$;/dev/$1/$2;;
		$result=1 if( $d eq $dev );
		}
	$result |= $df->device($AutomountPoint) eq $dev;
	dbg( "$txt. Result: " . ($result?'yes':'no ') );
	return $result;
	}

sub mount($$$)
	{
	(my $dev, my $AutomountPoint, my $options) = @_;
	$AutomountPoint =~ s/\/$//;
	return if isMounted( $dev, $AutomountPoint );
	File::Path::mkpath( $AutomountPoint, 0, 0700 ) if not -d $AutomountPoint;
	lg( "Mounting $dev to $AutomountPoint");
	my @cmd=('/bin/mount', $options, $dev, $AutomountPoint );
	if( ExecCmd( @cmd, 0 ) )
		{
		my $s="Couldn't mount $dev $AutomountPoint";
		if( $Command )
			{
			affaErrorExit( $s );
			}
		else
			{
			lg($s);
			}
		}
	$autoMounted{$AutomountPoint}=$dev if $job{'AutoUnmount'} eq 'yes';
	}

sub unmount($$)
	{
	(my $dev, my $AutomountPoint) = @_;
	$AutomountPoint =~ s/\/$//;
	return if not $autoMounted{$AutomountPoint} or $autoMounted{$AutomountPoint} ne $dev or not isMounted( $dev, $AutomountPoint );
	my @cmd=('/bin/umount', '-l', $AutomountPoint );
	lg( "Unmounting $AutomountPoint");
	not ExecCmd( @cmd, 0 ) or lg("Couldn't unmount $AutomountPoint");
	}

sub unmountAll()
	{
	while( (my $AutomountPoint, my $dev) = each( %autoMounted ) )
		{
		unmount( $dev, $AutomountPoint);
		}
	}

sub checkCrossFS($$)
	{
	(my $fs1, my $fs2) = @_;
	my $fn=".affa.checkCrossFS.$$";
	unlink( "$fs1/$fn" ) if -e "$fs1/$fn";
	unlink( "$fs2/$fn" ) if -e "$fs2/$fn";
	open( OUT, ">$fs1/$fn"); print OUT "test"; close(OUT);
	link( "$fs1/$fn", "$fs2/$fn" );
	my $res = -e "$fs2/$fn" ? 0 : 1;
	unlink( "$fs1/$fn" ) if -e "$fs1/$fn";
	unlink( "$fs2/$fn" ) if -e "$fs2/$fn";
	return $res;
	}

sub DiskSpaceWarn()
	{
	return if $Command ne 'scheduled' or $job{'DiskSpaceWarn'} eq 'none';
	lg( "Checking disk space." );
	(my $used, my $avail) = df( $job{'RootDir'} );
	my $affastatus = esmith::DB::db->open("/home/e-smith/db/affa-report/$jobname") or die "Error: Couldn't open /home/e-smith/db/affa-report/$jobname db file.";
	my $sched = $affastatus->get('scheduled.0');
	return unless $sched;
	my %props = $sched->props;
	return unless $props{'TotalFileSize'};
	(my $needed = $props{'TotalFileSize'}) =~ s/.*?([0-9]+).*/$1/ ;
	$needed = int($needed/1024);
	$needed=int($needed*0.5) if $job{'DiskSpaceWarn'} eq 'normal';
	$needed=int($needed*0.1) if $job{'DiskSpaceWarn'} eq 'risky';
	if( $avail<$needed )
		{
		my $msg = new Mail::Send;
		$msg->subject("Warning: Affa server $SystemName.$DomainName ($LocalIP) running out of disk space!");
		$msg->to($job{'EmailAddresses'});
		$msg->set("From", "\"Affa Backup Server\" <noreply\@$SystemName.$DomainName>");
		my $s;
		my $fh = $msg->open;
		print $fh "This message was sent by job '$jobname'.\n";
		$s = "Configured threshold type: $job{'DiskSpaceWarn'}\n"; print $fh $s; lg($s);
		$s = "Disk space left: " . sizeUnit(int($avail*1024)) . "\n"; print $fh $s; lg($s);
		$s = "Used disk space: " . sizeUnit(int($used*1024)) . "\n"; print $fh $s; lg($s);
		$s = "Disk size: " . sizeUnit(int(($avail+$used)*1024)) . "\n"; print $fh $s; lg($s);
		close( $fh );
		lg( "Running out of disk space message sent to " . $job{'EmailAddresses'} );
		}
	}

sub checkArchiveExists($$$)
	{
	(my $rootDir, $jobname, my $archive)=@_;
	if( not -f "$rootDir/$jobname/$archive/.AFFA-REPORT" )
		{
		$interactive=0;
		my $dir = opendir( DIR, "$rootDir/$jobname" );
		my $ar;
		my %va;
		while( defined ($ar=readdir(DIR)) )
			{
			next if not $ar =~ /^(scheduled|daily|weekly|monthly|yearly)\.[0-9]+$/;
			if( -f "$rootDir/$jobname/$ar/.AFFA-REPORT" )
				{
				open( AS, "<$rootDir/$jobname/$ar/.AFFA-REPORT" );
				my $d=<AS>; chomp($d); $d=~s/Date: //;
				close( AS );
				$va{$ar}=$d;
				}
			}

		my $txt="Error: Archive $archive not found."; print "$txt\n"; lg($txt);
		$txt = "Run 'affa --list-archives $jobname' to view available archives.\n";
		print "$txt\n";
		affaErrorExit( "." );
		}
	}


# remove archives which have indices greater than the Keep value
sub	cleanup()
	{
	my $jobname=$ARGV[0]||'';
	my $txt;
	my @cmd;
	if( not $affa->get($jobname)||'' )
		{
		$txt= "Error: Job '$jobname' undefined."; print("$txt\n");
		affaErrorExit( "$txt" );
		}
	getJobConfig( $jobname );
	my $dir = opendir( DIR, "$job{'RootDir'}/$jobname" );
	my %archives;
	my $cnt=0;
	if( $dir )
		{
		my $af;
		while( defined ($af=readdir(DIR)) )
			{
			next if not $af =~ /^(scheduled|daily|weekly|monthly|yearly)\.[0-9]+$/;
			(my $k, my $b)=split(/\./, $af);
			next if( $b<$job{$k."Keep"} );
			$k =~ s/scheduled/A/; $k =~ s/daily/B/; $k =~ s/weekly/C/; $k =~ s/monthly/D/; $k =~ s/yearly/E/;
			$archives{"$k".sprintf("%05d",$b)}=$af;
			$cnt++;
			}
		}
	if( $cnt )
		{
		print "\nWARNING: The following $cnt archives will be deleted!\n";
		foreach my $k ( reverse sort keys %archives )
			{
			print "$archives{$k}\n";
			}
		my $input='';
		print "Type 'proceed' to continue or <ENTER> to cancel: ";
		$input = lc(<STDIN>);
		chomp( $input );
		if( $input ne 'proceed' )
			{
			affaErrorExit( "Terminated by user" ) if $input ne 'proceed';
			}
		foreach my $k ( reverse sort keys %archives )
			{
			print "deleting archive $archives{$k} ... "; $|++;
			removeDir("$job{'RootDir'}/$jobname/$archives{$k}");
			print " Done.\n";
			}
		}
	}

sub moveArchive()
	{
	$jobname=$ARGV[0]||'';
	my $txt;
	my @cmd;
	if( not $affa->get($jobname)||'' )
		{
		$txt= "Error: Job '$jobname' undefined."; print("$txt\n");
		affaErrorExit( "$txt" );
		}
	getJobConfig( $jobname );
	(my $newdir=$ARGV[1])=~s/\/$//;
	if( not $newdir )
		{
		$txt= "Error: New RootDir not given."; print("$txt\n");
		affaErrorExit( "$txt" );
		}
	if( not $newdir=~/^\// or $newdir=~/\.\./ )
		{
		$txt= "Error: Full path required for NEWROOTDIR."; print("$txt\n");
		affaErrorExit( "$txt" );
		}
	if( not -d $newdir )
		{
		my $input='';
		while( not $input =~ /^(yes|no)$/ )
			{
			print "Directory $newdir does not exist. Create? (yes|no) ";
			$input = lc(<STDIN>);
			chomp( $input );
			}
		if( $input ne 'yes' )
			{
			$interactive=0;
			affaErrorExit( "Terminated by user" ) if $input ne 'proceed';
			}
		File::Path::mkpath( $newdir, 0, 0700 );
		}
	if( $job{'RootDir'}."/$jobname" ne "$newdir/$jobname" )
		{
		my $err=0;
		if( checkCrossFS( $job{'RootDir'}."/$jobname", $newdir ) )
			{
			$txt= "Warning: Cannot move across filesystems. Using copy and delete."; 
			print("$txt\n"); lg($txt);
			$txt= "Please wait..."; print("$txt\n"); lg($txt);
			my @cmd=(
				"/bin/tar", "--remove-files", "-C", $job{'RootDir'}, "-cf", '-', $jobname, 
				"|", "/bin/tar", "-C", $newdir, "-xf", '-' );
			$err=ExecCmd( @cmd, 0 );
			if( $err )
				{
				$txt= "Error: Copying failed."; print("$txt\n"); lg($txt);
				}
			else
				{
				removeDir( $job{'RootDir'}."/$jobname" );
				}
			}
		else
			{
			moveFileorDir( $job{'RootDir'}."/$jobname", "$newdir/$jobname" );
			}
		if( not $err )
			{
			my $jobconf = $affa->get($jobname);
			$jobconf->set_prop( 'RootDir', $newdir );
			$affa = esmith::DB::db->open("/home/e-smith/db/affa");
			}
		}
	}

sub renameJob()
	{
	$jobname=$ARGV[0]||'';
	my $txt;
	my @cmd;
	if( not $affa->get($jobname)||'' )
		{
		$txt= "Error: Job '$jobname' undefined."; print("$txt\n");
		affaErrorExit( "$txt" );
		}
	getJobConfig( $jobname );
	my $newname=$ARGV[1]||'';
	$newname=~s/\//_/g;
	if( not $newname )
		{
		$txt= "Error: No new jobname given."; print("$txt\n");
		affaErrorExit( "$txt" );
		}
	if( $affa->get($newname) )
		{
		$txt= "Error: A job 'mars' already exists."; print("$txt\n");
		affaErrorExit( "$txt" );
		}
	if( -f "/home/e-smith/db/affa-report/$jobname" )
		{
		moveFileorDir( "/home/e-smith/db/affa-report/$jobname", "/home/e-smith/db/affa-report/$newname");
		}
	if( -d $job{'RootDir'}."/$jobname" )
		{
		moveFileorDir( $job{'RootDir'}."/$jobname", $job{'RootDir'}."/$newname" );
		}

	# rename ssh HostKeyAlias
	if( open( KH, "</root/.ssh/known_hosts" ) )
		{
		open( KH_TMP, ">/root/.ssh/known_hosts.$$" );
		while( <KH> )
			{
			my @e = split( /\ /, $_);
			$e[0] = $newname if $e[0] eq $jobname;
			print KH_TMP join( " ", @e );
			}
		close KH_TMP;
		close KH;
		moveFileorDir( "/root/.ssh/known_hosts.$$", "/root/.ssh/known_hosts" );
		}
		
	my $new_record=$affa->new_record($newname);
	$new_record->set_prop('type', 'job');
	my $jobconf = $affa->get($jobname);
	my %props = $jobconf->props;
	foreach my $k ( sort keys %props ) 
			{
			$new_record->set_prop($k, $props{$k} );
			}
	$affa->get($jobname)->delete;
	$jobname=$newname;
	cronSetup();
	}

# entirely remove a job
sub	deleteJob()
	{
	my $jobname=$ARGV[0]||'';
	my $txt;
	my @cmd;
	if( not $affa->get($jobname)||'' )
		{
		$txt= "Error: Job '$jobname' undefined."; print("$txt\n");
		affaErrorExit( "$txt" );
		}
	getJobConfig( $jobname );
	my $dir = opendir( DIR, "$job{'RootDir'}/$jobname" );
	my %archives;
	my $cnt=0;
	if( $dir )
		{
		my $af;
		while( defined ($af=readdir(DIR)) )
			{
			next if not $af =~ /^(scheduled|hourly|daily|weekly|monthly|yearly])\.[0-9]+$/;
			(my $k, my $b)=split(/\./, $af);
			$k =~ s/scheduled/A/; $k =~ s/daily/B/; $k =~ s/weekly/C/; $k =~ s/monthly/D/; $k =~ s/yearly/E/;
			$archives{"$k".sprintf("%05d",$b)}=$af;
			$cnt++;
			}
		}
	print "\nWARNING: All ".($cnt?"$cnt archives and ":"")."configuration data of job '$jobname' will be deleted!\n";
	my $input='';
	print "Type 'proceed' to continue or <ENTER> to cancel: ";
	$input = lc(<STDIN>);
	chomp( $input );
	if( $input ne 'proceed' )
		{
		affaErrorExit( "Terminated by user" ) if $input ne 'proceed';
		}
	revokeKeys($jobname) if $opts{'revoke-keys'};
	if( $dir )
		{
		foreach my $k ( sort keys %archives )
			{
			print "deleting archive $archives{$k} ... "; $|++;
			removeDir("$job{'RootDir'}/$jobname/$archives{$k}");
			print " Done.\n";
			}
		print "deleting $job{'RootDir'}/$jobname ..."; $|++;
		removeDir("$job{'RootDir'}/$jobname");
		print " Done.\n";
		}
	print "deleting report database '$jobname' ... "; $|++;
	unlink("/home/e-smith/db/affa-report/$jobname");
	print " Done.\n";
	print "deleting logfile '/var/log/affa/$jobname.log' ... "; $|++;
	unlink("/var/log/affa/$jobname.log");
	print " Done.\n";
	print "deleting affa record '$jobname'..."; $|++;
	$affa->get($jobname)->delete;
	cronSetup();
	print " Done.\n";
	}


sub	fullRestore()
	{
	$interactive=0;
	$SIG{'INT'} = sub{};
	my $jobname=$ARGV[0]||'';
	my $archive=$ARGV[1]||'scheduled.0';
	my $txt;
	my @cmd;

	if( not $affa->get($jobname)||'' )
		{
		$txt= "Error: Job '$jobname' undefined."; print("$txt\n");
		affaErrorExit( "$txt" );
		}
	getJobConfig( $jobname );

	if( $job{'AutomountDevice'} and $job{'AutomountPoint'} )
		{
		mount( $job{'AutomountDevice'},  $job{'AutomountPoint'}, $job{'AutomountOptions'} );
		}

	# check if a job is running
	if( getLock("$lockdir/$jobname") )
		{
		print "Job '$jobname' is running. Wait for completion. Then run affa --full-restore again.\n";
		affaErrorExit( "affa job 'jobname'  is running." );
		}

	# check if archive exists
	checkArchiveExists($job{'RootDir'},$jobname,$archive);

	$interactive=1;
	checkConnection();

	print "WARNING: After the restore is done, the server $job{'remoteHostName'} will reboot!\n";

	my $input='';
	while( not $input =~ /^(proceed|quit|exit)$/ )
		{
		print "Type 'proceed' to continue or 'quit' to exit: ";
		$input = lc(<STDIN>);
		chomp( $input );
		}
	if( $input ne 'proceed' )
		{
		$interactive=0;
		affaErrorExit( "Terminated by user" ) if $input ne 'proceed';
		}

	$txt="Signaling pre-restore event on $job{'remoteHostName'}"; lg( $txt ); print "$txt\n";
	if( $job{'remoteHostName'} eq 'localhost' )
		{
		@cmd=("/sbin/e-smith/signal-event", "pre-restore");
		}
	else
		{
		@cmd=("/usr/bin/ssh", '-o', "HostKeyAlias=$jobname", '-o', "HostKeyAlias=$jobname", '-p', $job{'sshPort'}, $job{'remoteHostName'}, "/sbin/e-smith/signal-event", "pre-restore");
		}
	ExecCmd( @cmd, 0 );

	my @SourceDirs =  getSourceDirs();
	foreach my $k (@SourceDirs)
		{
		my $src = "$job{'RootDir'}/$jobname/$archive/$k";
		next if not -e $src;
		$txt="Restoring $job{'remoteHostName'}:/$k ..."; lg( $txt ); print "$txt\n";
		@cmd=(
			"/usr/bin/rsync",
			"--archive",
			"--stats",
			"--delete-during", "--ignore-errors", 
			"--partial",
			$job{'rsync--inplace'} ne 'no' ? "--inplace" : "",
			"--numeric-ids",
			$job{'remoteHostName'} eq 'localhost' ? '' : "--rsh='/usr/bin/ssh -o HostKeyAlias=$jobname -p $job{'sshPort'}'",
			(-d $src ? "$src/" : "$src"),
			$job{'remoteHostName'} eq 'localhost' ? "/$k" : $job{'remoteHostName'}.":/$k"
			);
		ExecCmd( @cmd, 0 );
		}

	if( $job{'remoteHostName'} eq 'localhost' )
		{
		imapIndexFilesDelete();
		}
	else
		{
		$txt="Deleting Dovecot's index files on $job{'remoteHostName'}"; lg( $txt ); print "$txt\n";
		@cmd=("/usr/bin/ssh", '-o', "HostKeyAlias=$jobname", '-p', $job{'sshPort'}, $job{'remoteHostName'}, "'" . imapIndexFilesDeleteCommand() . "'" );
		ExecCmd( @cmd, 0 );
		}

	$txt="Signaling post-upgrade event on $job{'remoteHostName'}"; lg( $txt ); print "$txt\n";
	if( $job{'remoteHostName'} eq 'localhost' )
		{
		@cmd=("/sbin/e-smith/signal-event", "post-upgrade");
		}
	else
		{
		@cmd=("/usr/bin/ssh", '-o', "HostKeyAlias=$jobname", '-p', $job{'sshPort'}, $job{'remoteHostName'}, "/sbin/e-smith/signal-event", "post-upgrade");
		}
	ExecCmd( @cmd, 0 );

	$txt="Signaling reboot event on $job{'remoteHostName'}"; lg( $txt ); print "$txt\n";
	if( $job{'remoteHostName'} eq 'localhost' )
		{
		@cmd=("/sbin/e-smith/signal-event", "reboot");
		}
	else
		{
		@cmd=("/usr/bin/ssh", '-o', "HostKeyAlias=$jobname", '-p', $job{'sshPort'}, $job{'remoteHostName'}, "/sbin/e-smith/signal-event", "reboot");
		}
	ExecCmd( @cmd, 0 );
	}

sub riseFromDeath()
	{
	$SIG{'INT'} = sub{};
	$interactive=1;
	my @cmd;
	my $bootstrap=$config->get_prop("bootstrap-console","Run");
	my $jobname=$ARGV[0]||'';
	my $archive=$ARGV[1]||'scheduled.0';
	getJobConfig( $jobname );

	if( $job{'remoteHostName'} eq 'localhost' )
		{
		$interactive=0;
		my $txt;
		$txt="Error: A rise cannot be done for this server from it's own backup.";
		lg( $txt ); print "$txt\n";
		$txt="Try option --full-restore instead.";
		lg( $txt ); print "$txt\n";
		affaErrorExit( "Cannot rise from my own backup." );
		}


	if( $job{'AutomountDevice'} and $job{'AutomountPoint'} )
		{
		mount( $job{'AutomountDevice'},  $job{'AutomountPoint'}, $job{'AutomountOptions'} );
		}

	# check if archive exists
	checkArchiveExists($job{'RootDir'},$jobname,$archive);

	# check if other affa jobs are running
	my %all = $affa->as_hash();
	my $havelocks=0;
	foreach my $job( reverse sort keys %all )
		{
		next if $job eq $ServerBasename;
		my $v=$all{$job};
		next if $v->{'type'} ne 'job';
		my $lockpid=getLock("$lockdir/$job");
		if( $lockpid )
			{
			my $txt = "Job '$job' is running (pid $lockpid)";
			print "$txt\n"; lg($txt);
			$havelocks++;
			$interactive=0;
			}
		}
	if( not $interactive )
		{
		print "Wait for completion of the running jobs or kill them. Then run affa --rise again.\n";
		affaErrorExit( "affa jobs are running." );
		}

	stopServices();

	if( $bootstrap ne 'no' )
		{
		print "*************************************************************\n";
		print "* WARNING:                                                  *\n";
		print "* Bootstrap-console Run flag is set.                        *\n";
		print "* It appears as if affa --rise has already run.             *\n";
		print "* Skipping backup run of affa server base (this server)     *\n";
		print "*************************************************************\n";
		}
	else
		{
		saveMySoul();
		}
	if( checkCrossFS( $job{'RootDir'}, "/usr/lib/affa" ) )
		{
		print "*************************************************************\n";
		print "* WARNING:                                                  *\n";
		print "* The archive is not on the local filesystem, therefore     *\n";
		print "* hardlinks cannot be used und all data need to be copied.  *\n";
		print "* Depending on the size of the archive this can take a long *\n";
		print "* time.                                                     *\n";
		print "*************************************************************\n";
		}
	my $input='';
	while( not $input =~ /^(proceed|quit|exit)$/ )
		{
		print "Type 'proceed' to continue or 'quit' to exit: ";
		$input = lc(<STDIN>);
		chomp( $input );
		}
	if( $input ne 'proceed' )
		{
		$interactive=0;
		affaErrorExit( "Terminated by user" ) if $input ne 'proceed';
		}

	print "Signaling pre-restore event\n";
	@cmd=("/sbin/e-smith/signal-event", "pre-restore");
	ExecCmd( @cmd, 0 );

	$config = esmith::ConfigDB->close;

	runRiseRsync("$job{'RootDir'}/$jobname/$archive/", "/");

	$config = esmith::ConfigDB->open or die "Error: Couldn't open config db.";

	imapIndexFilesDelete();

	print "Signaling post-upgrade event\n";
	@cmd=("/sbin/e-smith/signal-event", "post-upgrade");
	ExecCmd( @cmd, 0 );

	# preserve ethernet driver configuration for this hardware.
	# This allows us to run the --rise option remotely and connect to the restored server
	my $srcconfigPath="/var/affa/$ServerBasename/scheduled.0/home/e-smith/db/configuration";
	my $srcconfig = esmith::ConfigDB->open_ro($srcconfigPath)
		or affaErrorExit( "Couldn't open source config db $srcconfigPath");
	$config->set_prop("EthernetDriver1","type", $srcconfig->get("EthernetDriver1")->value);
	$config->set_prop("EthernetDriver2","type", $srcconfig->get("EthernetDriver2")->value);
	$config->set_prop("InternalInterface","Driver", $srcconfig->get_prop("InternalInterface","Driver"));
	$config->set_prop("InternalInterface","Name", $srcconfig->get_prop("InternalInterface","Name"));
	$config->set_prop("InternalInterface","NICBondingOptions", $srcconfig->get_prop("InternalInterface","NICBondingOptions"));
	$config->set_prop("InternalInterface","NICBonding", $srcconfig->get_prop("InternalInterface","NICBonding")||'disabled');

	print "Please make sure that the server '".$job{'remoteHostName'}."' is not connected to your network.\n";
	print "Otherwise you will not be able to connect to this server after the reboot.\n";
	print "Please reboot this server now.\n"
	}

sub undoRise()
	{
	my @cmd;
	$interactive=1;
	# search server base backup
	my $dir = opendir( DIR, "/var/affa/" );
	affaErrorExit( "Couldn't open directory /var/affa/" ) if not $dir;
	my $archive;
	my $a='';
	while( defined ($archive=readdir(DIR)) )
		{
		next if not $archive =~ /AFFA\.[a-z][a-z0-9\-]*\..*-\d*\.\d*\.\d*\.\d*/;
		$a=$archive;
		}
	affaErrorExit( "No server base backup found." ) if not $a and not -f "$a/scheduled.0";
	stopServices();
	print "\nWARNING: You will loose all data of your current server installation!\n";
	my $input='';
	while( not $input =~ /^(proceed|quit|exit)$/ )
		{
		print "Type 'proceed' to continue or 'quit' to exit: ";
		$input = lc(<STDIN>);
		chomp( $input );
		}
	if( $input ne 'proceed' )
		{
		$interactive=0;
		affaErrorExit( "Terminated by user" ) if $input ne 'proceed';
		}

	print "Signaling pre-restore event\n";
	@cmd=("/sbin/e-smith/signal-event", "pre-restore");
	ExecCmd( @cmd, 0 );

	runRiseRsync("/var/affa/$a/scheduled.0/", "/");

	imapIndexFilesDelete();

	print "Signaling post-upgrade event\n";
	@cmd=("/sbin/e-smith/signal-event", "post-upgrade");
	ExecCmd( @cmd, 0 );

	print "affa server base restored. Please reboot now.\n"
	}

sub runRiseRsync($$)
	{
	(my $archive,my $dest)=@_;
	my $b = new esmith::Backup or die "Error: Couldn't create Backup object\n";
	my @SourceDirs = $opts{'all'} ? getSourceDirs() : $b->restore_list;
	my $txt='Running rsync...'; lg($txt); print "$txt\n";
	foreach my $src (@SourceDirs)
		{
		$src =~ s/"/\\"/g;
		chomp( $src );
		$src = "$src/" if -d "$archive$src";
		my @cmd=(
			"/usr/bin/rsync",
			"--archive",
			"--stats",
			"--delete-during", "--ignore-errors", 
			"--delete-excluded",
			"--partial",
			"--inplace",
			"--numeric-ids",
			"--link-dest=$archive$src",
			"$archive$src",	
			"$dest$src",
			);
		print "/$src: ";
		my $status=ExecCmd(  @cmd, 0 );
		if( $status==0 or $status == 23 or $status == 24 )
			{
			$Affa::lib::ExecCmdOutout =~ /(Total file size: [0-9]* bytes)/gm;
			print "OK. $1\n";
			}
		else
			{
			print "Failed.\n";
			}
		}
	}


sub stopServices()
	{
	lg( "Stopping services...");
	my @cmd;
	foreach my $svc (@services)
		{
		@cmd=('/sbin/e-smith/service', $svc, 'stop');
		ExecCmd( @cmd, 0 );
		print $Affa::lib::ExecCmdOutout;
		}
	$ServicesStopped=1;
	}

sub startServices()
	{
	lg( "Starting services...");
	my @cmd;
	foreach my $svc (@services)
		{
		@cmd=('/sbin/e-smith/service', $svc, 'start');
		ExecCmd( @cmd, 0 );
		print $Affa::lib::ExecCmdOutout;
		}
	$ServicesStopped=0;
	}

sub saveMySoul()
	{
	my $txt = 'Backing up the Affa server base (this server).';
	print "$txt\n"; lg($txt);
	my $asb = $affa->get($ServerBasename);
	$asb->delete() if $asb;
	my $me = $affa->new_record($ServerBasename);
	$me->set_prop('type','job');
	my %props=(
		'remoteHostName'=>'localhost',
		'RootDir'=>'/var/affa',
		'TimeSchedule'=>'',
		'Description'=>'for internal use only',
		'scheduledKeep'=>1,
		'dailyKeep'=>1,
		'weeklyKeep'=>1,
		'monthlyKeep'=>1,
		'yearlyKeep'=>1,
		'SMEServer'=>'yes',
		'RPMCheck'=>'no',
		'DiskSpaceWarn'=>'none',
		'localNice'=>'+19',
		'remoteNice'=>0,
		'Watchdog'=>'no',
		'ConnectionCheckTimeout'=>120,
		'rsyncTimeout'=>900,
		'rsyncCompress'=>'yes',
		'EmailAddresses'=>'admin',
		'postJobCommand'=>'',
		'preJobCommand'=>'',
		'doneDaily'=>$thisDay,
		'doneMonthly'=>$thisMonth,
		'doneWeekly'=>$thisWeek,
		'doneYearly'=>$thisYear,
		'status'=>'disabled',
		'Debug'=>'no',
		);
	while( (my $p, my $v) = each %props )
		{
		 $me->set_prop($p,$v);
		}
	my @cmd=('/sbin/e-smith/affa','--run', $ServerBasename);
	not ExecCmd( @cmd, 0 ) or affaErrorExit( "Couldn't backup myself" );
	print "Done.\n";
	}

sub sendKeys()
	{
	$interactive=1;
	if( not $ARGV[0] and not $opts{'keys-host'} )
		{
		my %all = $affa->as_hash();
		foreach my $job( sort keys %all )
			{
			next if $job eq $ServerBasename;
			my $v=$all{$job};
			next if $v->{'type'} ne 'job';
			push( @ARGV, $job );
			}
		}
	elsif( $opts{'keys-host'} )
		{
		push( @ARGV, "" );
		}

	foreach my $jobname (@ARGV )
		{
		my $kf="/root/.ssh/id_dsa.pub";
		my $s;
		my @cmd;
		my $port;
		my $remotehost;
		my $HostKeyAliasOption='';
		if( $opts{'keys-host'} )
			{
			$remotehost=$opts{'keys-host'};
			$port = $opts{'keys-port'} ? $opts{'keys-port'} : 22;
			}
		else
			{
			if( not $affa->get($jobname)||'' )
				{
				my $txt= "Error: Job '$jobname' undefined."; print("$txt\n");
				affaErrorExit( "$txt" );
				}
			$HostKeyAliasOption="-o HostKeyAlias=$jobname";
			getJobConfig($jobname);
			print "Job $jobname: " if( $jobname );
			$remotehost=$job{'remoteHostName'};
			$port = $job{'sshPort'} ? $job{'sshPort'} : 22;
			}
    
		$remotehost =~ /(.*)/; $remotehost=$1;
		if( not -f $kf or not -f "/root/.ssh/id_dsa" )
			{
			$s="Generating DSA keys...";
			print "$s\n"; lg($s);
			@cmd=("/usr/bin/ssh-keygen",$sshQuiet,"-t","dsa","-N ''","-f", "/root/.ssh/id_dsa" );
			not ExecCmd( @cmd, 0 ) or affaErrorExit( "Couldn't generate DSA keys" );
			$s="Successfully created DSA key pair.";
			print "$s\n"; lg($s);
			}
		open( PUBK, $kf ) or affaErrorExit( "Could not open $kf" );
		my $pubk=trim(<PUBK>);
		close( PUBK );
		my $cmd;
		if( $remoteOS =~ /^cygwin$/i )
			{
			my $ah="/home/Administrator";
			my $akp="$ah/.ssh";
			my $ak="$akp/authorized_keys2";
			$cmd="/bin/cat $kf | /usr/bin/ssh $sshQuiet $HostKeyAliasOption -o StrictHostKeyChecking=no -p $port Administrator\@$remotehost '/bin/cat - > $ah/id_dsa.pub.$LocalIP.\$\$ && /bin/mkdir -p $akp && /bin/touch $ak && /bin/grep -v \"$pubk\" < $ak >> $ah/id_dsa.pub.$LocalIP.\$\$ ; /bin/mv -f $ah/id_dsa.pub.$LocalIP.\$\$ $ak'"; 
			}
		else
			{
			my $ak =$ESX ? "/root/.ssh/authorized_keys" : "/root/.ssh/authorized_keys2";
			$cmd="/bin/cat $kf | /usr/bin/ssh $sshQuiet $HostKeyAliasOption -o StrictHostKeyChecking=no -p $port $remotehost 'cat - > $ak.$LocalIP.\$\$ && touch $ak && grep -v \"$pubk\" < $ak >> $ak.$LocalIP.\$\$ ; mv -f $ak.$LocalIP.\$\$ $ak'"; 
			}
		dbg( "Exec Cmd: $cmd" );
		my $err=system($cmd);
		if ( $ESX )
			{
			$err=checkConnection_silent($jobname,0,1);
			}
		$s = $err ? "Sending public key to $remotehost failed." : "Public key sent to $remotehost";
		print "$s\n"; lg($s);
		}
	}

sub revokeKeys($)
	{
	my $jobname=shift;
	my $kf="/root/.ssh/id_dsa.pub";
	return if not -f $kf;
	my $s;
	my @cmd;
	my $port;
	my $remotehost;
	my $HostKeyAliasOption='';
	if( $opts{'keys-host'} )
		{
		$remotehost=$opts{'keys-host'};
		$port = $opts{'keys-port'} ? $opts{'keys-port'} : 22;
		}
	else
		{
		if( not $affa->get($jobname)||'' )
			{
			my $txt= "Error: Job '$jobname' undefined."; print("$txt\n");
			affaErrorExit( "$txt" );
			}
		$HostKeyAliasOption="-o HostKeyAlias=$jobname";
		getJobConfig($jobname);
		$remotehost=$job{'remoteHostName'};
		$port = $job{'sshPort'} ? $job{'sshPort'} : 22;
		}

	open( PUBK, $kf ) or affaErrorExit( "Could not open $kf" );
	my $pubk=trim(<PUBK>);
	close( PUBK );

	$remotehost =~ /(.*)/; $remotehost=$1;
	my $cmd;
	if( $remoteOS =~ /^cygwin$/i )
		{
		my $ah="/home/Administrator";
		my $akp="$ah/.ssh";
		my $ak="$akp/authorized_keys2";
		$cmd="/usr/bin/ssh $sshQuiet $HostKeyAliasOption -o PasswordAuthentication=no -o StrictHostKeyChecking=yes -p $port Administrator\@$remotehost '/bin/mkdir -p $akp && /bin/touch $ak && /bin/grep -v \"$pubk\" < $ak > $ak.$LocalIP.\$\$ ; /bin/mv -f $ak.$LocalIP.\$\$ $ak'";
		}
	else
		{
		my $ak =$ESX ? "/root/.ssh/authorized_keys" : "/root/.ssh/authorized_keys2";
		$cmd="/usr/bin/ssh $sshQuiet $HostKeyAliasOption -o PasswordAuthentication=no -o StrictHostKeyChecking=yes -p $port $remotehost 'touch $ak && grep -v \"$pubk\" < $ak > $ak.$LocalIP.\$\$ ; mv -f $ak.$LocalIP.\$\$ $ak'";
		}
	dbg( "Exec Cmd: $cmd" );
	my $err= $ESX ? checkConnection_silent($jobname,0,1) : 0; # workraound for Dropbear < 0.51
	$err=system($cmd) if not $err;
	$err=0 if $ESX and ( $err==255 or (65535-int($err))==255 );
	if( $err )
		{
		$s="Deleting public key on $remotehost failed.";
		print "$s\n";
		affaErrorExit( $s );
		}
	$s="Public key deleted on $remotehost";
	print "$s\n"; lg($s);
	}

sub checkConnectionsAll()
	{
	my %all = $affa->as_hash();
	foreach my $j( reverse sort keys %all )
		{
		next if $j eq $ServerBasename;
		my $v=$all{$j};
		next if $v->{'type'} ne 'job';
		getJobConfig($j);
		my @cmd;
		printf "%-16s : ", $j;
		if( $rsyncd )
			{
			print "Rsyncd connection ";
			@cmd=($rsyncLocal, '-dq', ($job{'rsyncdUser'} ? $job{'rsyncdUser'}.'@' : '') . $job{'remoteHostName'} . "::'" . $job{'rsyncdModule'} . "'");
			print( ((ExecCmd( @cmd, 0 )==0) ? "ok" : "FAILED") . ". " );
			}
		if( $ESX )
			{
			require Affa::esxi;
			print "VI API connection ";
			print( (Affa::esxi::checkAPIConnection( $job{'ESXiUsername'}, $job{'ESXiPassword'} ) ? "ok" : "FAILED") . ". " );
			}
		print "SSH connection ";
		if( $remoteOS =~ /^cygwin$/i )
			{
			@cmd=('/usr/bin/ssh', '-p', $job{'sshPort'}, '-o', "HostKeyAlias=$j", '-o', 'StrictHostKeyChecking=yes', '-o', "ConnectTimeout=$job{'ConnectionCheckTimeout'}",'-o','PasswordAuthentication=no', $sshQuiet, "Administrator\@$job{'remoteHostName'}",'echo OK');
			}
		else
			{
			@cmd=('/usr/bin/ssh', '-p', $job{'sshPort'}, '-o', "HostKeyAlias=$j", '-o', 'StrictHostKeyChecking=yes', '-o', "ConnectTimeout=$job{'ConnectionCheckTimeout'}",'-o','PasswordAuthentication=no', $sshQuiet, $job{'remoteHostName'},'echo OK');
			}
		ExecCmd( @cmd,0); chomp($Affa::lib::ExecCmdOutout);
		print ($Affa::lib::ExecCmdOutout eq "OK" ? "ok\n" : "FAILED\n");
		}
	}


sub createBackupFile()
	{
	my $jobname=$ARGV[0]||'';
	my $archive=$ARGV[1]||'scheduled.0';
	my $txt;

	if( not $affa->get($jobname)||'' )
		{
		$txt= "Job '$jobname' undefined."; print("Error: $txt\n");
		affaErrorExit( "$txt" );
		}
	getJobConfig( $jobname );

	if( $job{'AutomountDevice'} and $job{'AutomountPoint'} )
		{
		mount( $job{'AutomountDevice'},  $job{'AutomountPoint'}, $job{'AutomountOptions'} );
		}

	# check if a job is running
	if( getLock("$lockdir/$jobname") )
		{
		print "Job '$jobname' is running. Wait for completion. Then run affa --create-backup-file again.\n";
		affaErrorExit( "affa job 'jobname'  is running." );
		}
	setLock();

	# check if archive exists
	checkArchiveExists($job{'RootDir'},$jobname,$archive);

	my $outfile=$opts{'outfile'};
	$outfile=$cwd if not $outfile;
	$outfile .= '/smeserver.tgz' if -d $outfile;

	if( -f $outfile )
		{
		$txt= "File $outfile already exists."; print("Error: $txt\n");
		affaErrorExit( "$txt" );
		}

	unless ( open( OUT, ">$outfile" ) )
		{
		$txt= "Could not open $outfile for writing."; print("Error: $txt\n");
		affaErrorExit( "$txt" );
		}
	close( OUT );
	unlink( $outfile );

	my $affastatus = esmith::DB::db->open("/home/e-smith/db/affa-report/$jobname");
	my $size=$affastatus->get_prop( $archive, 'TotalFileSize' ) || 0;
	if( not $size )
		{
		updateReportDB();
		$affastatus = esmith::DB::db->open("/home/e-smith/db/affa-report/$jobname");
		$size=$affastatus->get_prop( $archive, 'TotalFileSize' ) || 0;
		}
	$size =~ s/(\d*).*/$1/;
	my $f;
	my $restore_list='';
	my $dir = opendir( DIR, "$job{'RootDir'}/$jobname/$archive" );
	while( defined ($f=readdir(DIR)) )
		{
		next if $f =~ /^(.AFFA-REPORT|$jobname-setup.pl|\.|\.\.)$/;
		$restore_list.="$f ";
		}
	close(DIR);
	my $status=system("/usr/lib/affa/create-backup-file.sh $job{'RootDir'} $jobname $archive  '$restore_list' $outfile $size");
	if( $status!=0 )
		{
		unlink( $outfile );
		$txt= "Could not create $outfile."; print("Error: $txt\n");
		affaErrorExit( "$txt" );
		}
	$txt="Backup file '$outfile' created."; print("$txt\n"); lg( $txt);
	}

sub showSchedule()
	{
	my $res=$opts{15} ? 15 : 30;
	my $vt=240/30*$res;
	my %out;
	my $maxlen=0;
	my $disabled=0;
	my %all = $affa->as_hash();
	foreach my $job( sort keys %all )
		{
		next if $job eq $ServerBasename;
		my $v=$all{$job};
		next if $v->{'type'} ne 'job';

		getJobConfig($job);
		if( $job{'status'} ne "enabled" && not $opts{'all'} )
			{
			$disabled++;
			next;
			}

		my $affastatus = esmith::DB::db->open("/home/e-smith/db/affa-report/$job");
		my $ExecTime = $affastatus ? $affastatus->get_prop("scheduled.0", "ExecutionTime")||0 : 0;
		$ExecTime=int($ExecTime/60);
		my $end = $affastatus ? $affastatus->get_prop("scheduled.0", "Date")||'0000' : '0000';
		$end =~ /.*(..)(..)$/;
		$end=$1*60+$2;
		my $start = $end-$ExecTime;
		while( $start<0 ) {$start+=1440};
		my ($s1, $e1, $s2, $e2);
		if( $start>$end )
			{
			$s1=0; $e1=$end; $s2=$start; $e2=1440;
			}
		else
			{
			$s1=$s2=$start; $e1=$e2=$end;
			}

		my $jn = substr( $job, 0, 26);
		$maxlen=length($jn) if length($jn) > $maxlen;
		my @ts = split(/,/, $job{'TimeSchedule'});
		for( my $i=0; $i<@ts; $i++ )
			{
			$ts[$i] =~ /(..)(..)/;
			$ts[$i] = $1*60+$2;
			}
		my $t=shift @ts if @ts;
		for( my $i=0; $i<24*60; $i+=$res )
			{
			$out{$jn}.=" " if $i%$vt == 0;
			if( $t >= $i && $t < $i+$res )
				{
				$out{$jn}.="X";
				while( @ts && $t < $i+$res )
					{
					$t=shift @ts;
					}
				}
			elsif( $i >= $s1 && $i< $e1 or $i >= $s2 && $i< $e2 )
				{
				$out{$jn}.="=";
				}
			else
				{
				$out{$jn}.="-";
				}
			}
		$out{$jn}.=sprintf "\n";
		}
	print "$affaTitle\n";
	if( %out )
		{
		printf "%" . $maxlen . "s", "TIME";
		for( my $i=0; $i<24*60; $i+=$res )
			{
			my $h = int($i/60);
			my $m = sprintf "%02d", $i-$h*60;
			printf " %-8s", "$h:$m" if $i%$vt == 0;
			}
		print "\n";
		foreach my $k (sort {$out{$b} cmp $out{$a} } keys %out )
			{
			printf "%" . $maxlen . "s%s",  $k, $out{$k};
			}
		}
	printf "%d disabled jobs not listed. Use --all to display.\n", $disabled if $disabled;
	}


sub sendStatus()
	{
	if( $job{'sendStatus'}||'' =~ /^(daily|weekly|monthly)$/ )
		{
		$job{'sendStatus'} = ucfirst( $job{'sendStatus'} );
		my $msg = new Mail::Send;
		$msg->subject("$job{'sendStatus'} status from $SystemName.$DomainName ($LocalIP)");
		$msg->to($job{'EmailAddresses'});
		$msg->set("From", "\"Affa Backup Server\" <noreply\@$SystemName.$DomainName>");
		my $fh = $msg->open;
		print $fh "Status:\n";
		print $fh getStatus();
		print $fh "\nDisk Usage:\n";
		print $fh DiskUsage();
		print $fh "\nArchive lists of all jobs:\n";
		print $fh listArchives();
		print $fh "\ngenerated on " .Date::Format::ctime(time());
		close( RP );
		$fh->close; 
		lg( "$job{'sendStatus'} status message sent to " . $job{'EmailAddresses'} );
		}
	}

sub mailTest()
	{
	$jobname=$ARGV[0]||'';
	if( not $affa->get($jobname)||'' )
		{
		my $txt= "Error: Job '$jobname' undefined."; print("$txt\n");
		affaErrorExit( "$txt" );
		}
	getJobConfig( $jobname );
	my $msg = new Mail::Send;
	$msg->subject("Testmail for job '$jobname' from $SystemName.$DomainName ($LocalIP)");
	$msg->to($job{'EmailAddresses'});
	$msg->set("From", "\"Affa Backup Server\" <noreply\@$SystemName.$DomainName>");
	my $fh = $msg->open;
	print $fh "It works!\n\n";
	print $fh "Status:\n";
	print $fh getStatus();
	print $fh "\nDisk Usage:\n";
	print $fh DiskUsage();
	print $fh "\nprinted on " .Date::Format::ctime(time());
	$fh->close; 
	my $txt="Testmail sent to " . $job{'EmailAddresses'};
	lg( $txt); print "$txt\n";
	if( $job{'Watchdog'} eq 'yes' and $job{'remoteHostName'} ne 'localhost' )	
		{
		mailTestWatchdogRemote();
		my $txt="Watchdog testmail from $job{'remoteHostName'} sent to " . $job{'EmailAddresses'};
		lg( $txt); print "$txt\n";
		}
	}

sub mailTestWatchdogRemote()
	{
	checkConnection();
	my $WDName = "affa-watchdog-mailtest-$jobname-$LocalIP";
	open( WD, "</usr/lib/affa/watchdog-mailtest.template" ) or warn "Error: Couldn't open /usr/lib/affa/watchdog.template\n";
	open( WDS, ">/usr/lib/affa/$WDName" ) or warn "Error: Couldn't open /usr/lib/affa/$WDName for writing\n";
	dbg( "Watchdog parameters:" );
	while( <WD> )
		{
		$_ =~ s/^use constant _JOBNAME=>.*$/use constant _JOBNAME=>'$jobname';/;
		$_ =~ s/^use constant _EMAIL=>.*$/use constant _EMAIL=>\'$job{'EmailAddresses'}\';/;
		$_ =~ s/^use constant _BACKUPHOST=>.*$/use constant _BACKUPHOST=>'$SystemName.$DomainName ($LocalIP)';/;
		$_ =~ s/^use constant _WDSCRIPT=>.*$/use constant _WDSCRIPT=>'$WDName';/;
		print WDS $_;
		}
	close( WDS );
	close( WD );
	chmod( 0700, "/usr/lib/affa/$WDName" );
	remoteCopy("/usr/lib/affa/$WDName", "/tmp/");
	my @cmd=('/usr/bin/ssh', '-p', $job{'sshPort'}, '-o', "HostKeyAlias=$jobname", $sshQuiet, $job{'remoteHostName'},"/tmp/$WDName");
	not ExecCmd( @cmd, 0 ) or affaErrorExit( "Couldn't run /usr/lib/affa/$WDName on remote host." );
	}

sub trim($)
	{
	my $s=shift;
	$s=~s/^\s+//;
	$s=~s/\s+$//;
	return $s;
	}

sub formatHTime($)
	{
	my $ts=shift(@_);
	my ($y,$m,$d,$H,$M)=$ts=~ /(\d\d\d\d)(\d\d)(\d\d)(\d\d)(\d\d)/ ;
	return Date::Format::time2str("%a %Y %b %d %H:%M",(timelocal(0,$M,$H,$d,$m-1,$y))) ;
	}

sub hTime2Timestamp($)
	{
	my $ts=shift(@_);
	my ($y,$m,$d,$H,$M)=$ts=~ /(\d\d\d\d)(\d\d)(\d\d)(\d\d)(\d\d)/ ;
	return timelocal(0,$M,$H,$d,$m-1,$y) ;
	}

sub countUnit($)
	{
	my $count = shift(@_);	
	my $unit = "";
	if( $count > 10*1000*1000*1000 )
		{
		$count = int($count/1000/1000/1000);
		$unit = " * 10E9";
		}
	elsif( $count > 10*1000*1000 )
		{
		$count = int($count/1000/1000);
		$unit = " * 10E6";
		}
	$count .= "$unit";
	return $count;
	}

sub timeUnit($)
	{
	my $res='';
	my $t = shift(@_);	
	my $days = int($t/86400); $t -= $days*86400;
	my $hours = int($t/3600); $t -= $hours*3600;
	my $minutes = int($t/60); 
	my $seconds = $t - $minutes*60;
	if( $days )
		{
		$res=sprintf("%dd%2dh%02dm", $days, $hours, $minutes);
		}
	else
		{
		$res=sprintf("%2dh%02dm%02ds", $hours, $minutes, $seconds);
		}
	return $res;
	}
sub sizeUnit($)
	{
	my $size = shift(@_);	
	my $unit = "B";
	if( $size > 1*1024.0*1024.0*1024.0*1024.0 )
		{
		$size = ($size/1024.0/1024.0/1024.0/1024.0);
		$unit = "TB";
		}
	elsif( $size > 1*1024.0*1024.0*1024.0 )
		{
		$size = ($size/1024.0/1024.0/1024.0);
		$unit = "GB";
		}
	elsif( $size > 1*1024.0*1024.0 )
		{
		$size = ($size/1024.0/1024.0);
		$unit = "MB";
		}
	elsif( $size > 1*1024.0 )
		{
		$size = ($size/1024.0);
		$unit = "kB";
		}
	if(  length(sprintf( "%2.1f",$size))<=3)
		{
		$size = sprintf( "%2.1f", $size);
		}
	else
		{
		$size = sprintf( "%3.0f", $size);
		}
	$size .= $unit;
	return $size;
	}

sub imapIndexFilesDeleteCommand()
	{
	return 'USERS=`/usr/bin/find /home/e-smith/files/users -type d -maxdepth 1`;USERS="/home/e-smith/ $USERS"; for u in $USERS ;  do ! /usr/bin/test -d $u/Maildir && continue; /usr/bin/find $u/Maildir -maxdepth 2 -type f -name ".imap.index*" -exec /bin/rm -f \'{}\' \; ; /usr/bin/find $u/Maildir -maxdepth 2 -type f -name "dovecot.index*" -exec /bin/rm -f \'{}\' \; ; done';
	}

sub imapIndexFilesDelete()
	{
	# Sometimes Dovecot index files are corrupted after a restore
	# It is save to delete them all. Dovecot will rebuild them when the mailbox is accessed.
	print "Deleting Dovecot's index files\n";
	my @cmd=( imapIndexFilesDeleteCommand() );
	ExecCmd( @cmd, 0 );
	}

sub sendErrorMesssage()
	{
	return if not $jobname or not $Command or not $job{'EmailAddresses'};
	my $msg = new Mail::Send;
	$msg->subject("Error on $SystemName.$DomainName ($LocalIP): Job '$jobname' failed.");
	$msg->to($job{'EmailAddresses'});
	$msg->set("From", "\"Affa Backup Server\" <noreply\@$SystemName.$DomainName>");
	my $fh = $msg->open;
	print $fh "Excerpt from log file $Affa::lib::logfile:\n";
	foreach my $k (@Affa::lib::Messages)
		{
		chomp($k);
		$k =~ s/ /_/g;
		print $fh "$k\n";
		}
	$fh->close; 
	lg( "Email sent to " . $job{'EmailAddresses'} );
	}

sub sendSuccessMesssage()
	{
	return if not $jobname or $Command ne "scheduled" or not $job{'EmailAddresses'} or ($job{'chattyOnSuccess'}||0)<=0;
	my $jobconf = $affa->get($jobname);
	$jobconf->set_prop( 'chattyOnSuccess', $job{'chattyOnSuccess'}-1 ) if( $job{'chattyOnSuccess'} > 0 ) ;
	$affa = esmith::DB::db->open("/home/e-smith/db/affa");
	my $msg = new Mail::Send;
	$msg->subject("Success on $SystemName.$DomainName ($LocalIP): Job '$jobname $Command' completed.");
	$msg->to($job{'EmailAddresses'});
	$msg->set("From", "\"Affa Backup Server\" <noreply\@$SystemName.$DomainName>");
	my $fh = $msg->open;
	print $fh listArchives() . "\n";
	print $fh "\nDisk Usage:\n";
	print $fh DiskUsage();
	print $fh "\nYou will receive " . ($job{'chattyOnSuccess'}?$job{'chattyOnSuccess'}:'no') . " further success notifications.\n";
	$fh->close; 
	lg( "Email sent to " . $job{'EmailAddresses'} );
	}

sub cronSetup()
	{
	lg( 'expanding template /etc/cron.d/affa' );
    processTemplate( {
	TEMPLATE_PATH=>"/etc/cron.d/affa",
		} );
	lg( 'expanding template /etc/cron.d/affa-status' );
    processTemplate( {
	TEMPLATE_PATH=>"/etc/cron.d/affa-status",
		} );
	}

sub moveFileorDir($$)
	{
	(my $src, my $dst)=@_;
	return if not -e $src;
	dbg( "Moving $src to $dst" );
	rename( $src, $dst) or affaErrorExit("Moving $src to $dst failed");
	}

sub removeDir($)
	{
	my $dir = shift(@_);
	return if( not -d $dir );
	dbg( "Deleting $dir" );
	# after the first rm run, do a chmod 777 and run rm again. This delete files with wrong permissions.
	# This is an issue on mounted CIFS filesystems.
	my @cmd=('/bin/rm', '-rf', $dir, ';', '/bin/chmod', '-R', '777', $dir, '&>', '/dev/null', ';', '/bin/rm', '-rf', $dir );
	ExecCmd( @cmd, 0 );
	}

sub remoteCopy($$)
	{
	(my $src, my $dst) = @_;
	my @cmd=(
		$rsyncLocal, 
		"--archive",
		$job{'BandwidthLimit'} ? "--bwlimit=$job{'BandwidthLimit'}" : '',
		$job{'rsync--modify-window'} > 0 ? "--modify-window=$job{'rsync--modify-window'}" : '',
		$job{'rsyncTimeout'} ? "--timeout=$job{'rsyncTimeout'}" : "",
		$job{'rsyncCompress'} eq 'yes' ? "--compress" : "",
		"--rsync-path='$rsyncRemote'",
		"--rsh='/usr/bin/ssh -o HostKeyAlias=$jobname -p $job{'sshPort'}'",
		$src, $job{'remoteHostName'}.":$dst" );
	not ExecCmd( @cmd, 0 ) or affaErrorExit( "Couldn't copy $src to remote host." );
	}

sub showVersion()
	{
	print "$affaTitle\n";
	}

sub showHelp($)
	{
	my $short = shift;
	if( not $short )
		{
		print "$affaTitle\n";
		print "Copyright (C) 2004-2009 by Michael Weinberger, neddix Stuttgart, Germany\n";
		print "\n";
		print "Affa comes with ABSOLUTELY NO WARRANTY. This is free software, and\n";
		print "you are welcome to redistribute it under certain conditions.\n";
		print "See the GNU General Public Licence for details.\n";
		print "\n";
		print "Affa is a rsync based backup program for the SME Server Version 7 and above.\n";
		print "It remotely backups SME servers or other systems, which have either the rsync\n";
		print "program and the sshd service or the rsyncd service installed.\n";
		print "\n";
		print "In addition, Affa supports backups of ESXi virtual machine snapshots.\n";
		print "For ESXi VM backups the VI Perl Toolkit is required.\n";
		print "\n";
		print "Please see http://wiki.contribs.org/Affa and\n";
		print "http://wiki.contribs.org/Backup_of_ESXi_Virtual_Machines_using_Affa\n";
		print "for full documentation.\n";
		print "\n";
		}
	print "Usage: affa --run JOB\n";
	print "  or   affa --make-cronjobs\n";
	print "  or   affa --send-key JOB\n";
	print "  or   affa --send-key --host=TARGETHOST [--port=PORT] [--remoteOS=cygwin]\n";
	print "  or   affa --full-restore JOB [ARCHIVE]\n";
	print "  or   affa --rise [--all] JOB [ARCHIVE]\n";
	print "  or   affa --undo-rise\n";
	print "  or   affa --list-archives [--csv] [JOB JOB ...]\n";
	print "  or   affa --status [--csv]\n";
	print "  or   affa --disk-usage [--csv]\n";
	print "  or   affa --show-schedule [-15]\n";
	print "  or   affa --send-status\n";
	print "  or   affa --mailtest JOB\n";
	print "  or   affa --cleanup JOB\n";
	print "  or   affa --rename-job JOB NEWNAME\n";
	print "  or   affa --move-archive JOB NEWROOTDIR\n";
	print "  or   affa --delete-job [--revoke-key] JOB\n";
	print "  or   affa --chunk-archive JOB ARCHIVE\n";
	print "  or   affa --unchunk-archive JOB ARCHIVE\n";
	print "  or   affa --revoke-key JOB\n";
	print "  or   affa --revoke-key --host=TARGETHOST [--port=PORT] [--remoteOS=cygwin]\n";
	print "  or   affa --check-connections\n";
	print "  or   affa --create-backup-file JOB [ARCHIVE] [--outfile=FILE]\n";
	print "  or   affa --kill JOB\n";
	print "  or   affa --version\n";
	print "  or   affa --help\n";
	}

sub SignalHandler()
	{
	my $sig=shift;
	killProcessGroup(0,$sig);
	}

sub killJob()
	{
	$interactive=1;
	$allow_retry=0;
	$jobname=$ARGV[0]||'';
	if( not $affa->get($jobname)||'' )
		{
		my $txt= "Error: Job '$jobname' undefined."; print("$txt\n");
		affaErrorExit( "$txt" );
		}
	my $pid=getLock("$lockdir/$jobname");
	if( $pid )
		{
		killProcessGroup($pid, 'TERM');
		print "Job $jobname killed (pid=$pid)\n";
		}
	else
		{
		print "Job $jobname not running\n";
		}
	exit 0;
	}

sub killProcessGroup($$)
	{
	(my $pid, my $sig) = @_;
	$allow_retry=0;
	$SIG{'TERM'} = sub{};
	my $pgrp=getpgrp($pid);
	kill 'TERM', -$pgrp;
	if( $pid == 0 ) # current process
		{
		lg("$Command run killed");
		if( $Command eq "scheduled" )
			{
			affaErrorExit( "Caught interrupt signal $sig");
			}
		else
			{
			exit -1;
			}
		}
	}

sub affaErrorExit($)
	{
	(my $msg) = @_;
	my $package=(caller)[0];
	my $err=(caller)[2];
	my $sub=(caller(1))[3]||'';
	lg( "Error $err in '$package': $msg" );
	print "Error $err in '$package': $msg\n" if $interactive==1;
	execPostJobCommand($err) if $sub ne 'main::execPostJobCommand';
	unmountAll();
	my $retry = ( defined $opts{'RetryAttempts'} ? $opts{'RetryAttempts'} : $job{'RetryAttempts'}) - 1;
	my $retryCmd='';
	if( $allow_retry && $retry>=0 && ($Command eq 'scheduled') )
		{
		my $sleep=$opts{'RetryAfter'}||($job{'RetryAfter'}-3)||0;
		$sleep = 0 if $sleep<0;
		$retry = int(86400/$sleep)-1 if $retry*$sleep>86400; 
		$retryCmd="affa --run $jobname --RetryAttempts=$retry --RetryAfter=$sleep &";
		lg( "Starting re-run " . ($job{'RetryAttempts'}-$retry) . " of $job{'RetryAttempts'} in the background.");
		}
	lg( "Total execution time: " . timeUnit(time()-$StartTime) ) if $StartTime;
	sendErrorMesssage() if not $retryCmd or ($job{'RetryNotification'}||'') eq 'yes';
	removeLock();
	startServices() if $ServicesStopped;
	lg( "Exiting." );
	lg( '.' );
	system("sleep 3 && $retryCmd") if $retryCmd;	
	exit -1;
	}

sub affaExit( $ )
	{
	my $msg = shift(@_);
	unmountAll();
	lg( $msg );
	removeLock();
	lg( "Total execution time: " . timeUnit(time()-$StartTime) ) if $StartTime;
	lg( "Exiting." );
	lg( '.' );
	exit 0;
	}

sub chunkArchive($)
	{
	my $unchunk=shift;
	$interactive=1;
	$jobname=$ARGV[0]||'';
	my $archive=$ARGV[1]||'';
	my $txt;
	my @cmd;

	if( not $affa->get($jobname)||'' )
		{
		$txt= "Error: Job '$jobname' undefined."; print("$txt\n");
		affaErrorExit( "$txt" );
		}
	getJobConfig( $jobname );
	if( (not $archive) || (not -d "$job{'RootDir'}/$jobname/$archive") )
		{
		$txt= "Error: Archive '$archive' not found."; print("$txt\n");
		affaErrorExit( "$txt" );
		}
	if( $unchunk )
		{
		unchunkfiles($archive,1);
		}
	else
		{
		chunkfiles($archive,1);
		}
	}

sub chunk( $$$$$ )
	{
	(my $cmd, my $path, my $extension, my $chunksize, my $copydest) = @_;

	$path =~ s/^\///;
	my $fullpath="$job{'RootDir'}/$jobname/$cmd/$path";
	my $buffer;
	my $count=0;
	my $subfullpath, my $subpath, my $subfile;

	return if not -f $fullpath;

	dbg( "Chunking $fullpath with blocksize=$chunksize bytes." );

	my $linkDest='';
	opendir( DIR, "$job{'RootDir'}/$jobname" );
	while( defined (my $archive=readdir(DIR)) )
		{
		next if not $archive =~ /^(scheduled|daily|weekly|monthly|yearly)\.[0-9]+$/ or $archive eq $cmd;
		my $ld = "$job{'RootDir'}/$jobname/$archive/$path.affa-chunked";
		$linkDest .= "--link-dest='$ld' " if -d $ld;
		}
	close DIR;

	main::removeDir("$fullpath$extension");
	my $total_out=0;
	open( IF, $fullpath ) or affaErrorExit("Could not open $fullpath");
	until( eof(IF) )
		{
		$subfullpath = sprintf( "%016lx", $count);
		$subfullpath =~ s/(..)/$1\//g;
		$subfullpath =~ /(.*)\/(.*)\//; $subpath=$1; $subfile=$2;
		File::Path::mkpath( "$fullpath$extension/$subpath", 0, 0700 ) unless -d "$fullpath$extension/$subpath";
		read( IF, $buffer, $chunksize );
    	my $bz = Compress::Bzip2->new();
		$bz->bzopen("$fullpath$extension/$subpath/$subfile.bz2", "w");
		if( not $bz->bzwrite($buffer) )
			{
			affaErrorExit( "bzip2 error; " .$bz->bzerror() );
			}
		$bz->bzclose();
		$total_out+=$bz->total_out();
		$count++;
		}
	dbg( "Created $count compressed chunks. Last chunk file is $fullpath$extension/$subpath/$subfile.bz2" );
	dbg( "Bytes written: " . sizeUnit($count*$chunksize) . " uncompressed " . sizeUnit($total_out) . " compressed. Compression ratio=" . sprintf("%.1f%%", $total_out/$count/$chunksize*100) );
	close(IF);

	dbg( "Linking against other archives." );
	removeDir("$fullpath$extension.tmp");
	my @cmd=(
		$rsyncLocal,
		'--archive',
		'--partial',
		'--stats',
		'--inplace',
		"--numeric-ids",
		"--modify-window=".(86400*365),
		"--remove-source-files",
		$linkDest,
		"$fullpath$extension/",
		"$fullpath$extension.tmp");
	ExecCmd( @cmd, 0);
	removeDir("$fullpath$extension");
	moveFileorDir( "$fullpath$extension.tmp", "$fullpath$extension" );
	if( $copydest )
		{
		moveFileorDir( $fullpath, $copydest);
		}
	unlink( $fullpath );
	open(CF, ">$job{'RootDir'}/$jobname/$cmd/.AFFA-CHUNK-FLAG"); close CF;
	}

sub chunkfiles($$)
	{
	return if( !$job{'chunkFiles'} );
	(my $cmd, my $verbose)=@_;
	return if not $cmd;
	my $arfull = "$job{'RootDir'}/$jobname/$cmd";
	return if not -d $arfull;

	my @nf = split( "/", $job{'chunkFiles'});
	my $nopt='';
	my $txt='';
	foreach my $patt (@nf)
		{
		$nopt .= ($nopt ? ' -o ' : '') . "-name '$patt'";
		$txt .= " '$patt'";
		}
	return if not $nopt;
	dbg( "Searching for files to be chunked with name(s) $txt" );	

	open FI, "cd $arfull && find . -type f \\( $nopt \\)|";
	while(<FI>)
		{
		chomp($_);
		(my $cf=$_) =~ s/\.\///;
		if( -s "$job{'RootDir'}/$jobname/$cmd/$cf" > $ChunkThresholdSize )
			{
			print "Chunking $cmd/$cf ..." if $verbose;
			chunk( $cmd, "$cf", ".affa-chunked", $job{'ChunkSize'}, '' );
			print " done.\n" if $verbose;
			}
		}
	close FI;
	}

sub unchunkfiles($$)
	{
	(my $cmd, my $verbose)=@_;
	return if not $cmd;
	my $arfull = "$job{'RootDir'}/$jobname/$cmd";
	return if not -d $arfull;

	my @linkdir;
	opendir( DIR, "$job{'RootDir'}/$jobname" );
	while( defined (my $archive=readdir(DIR)) )
		{
		next if not $archive =~ /^(scheduled|daily|weekly|monthly|yearly)\.[0-9]+$/ or $archive eq $cmd;
		push( @linkdir, $archive );
		}
	close DIR;
	open FI, "cd $arfull && find . -type d -name '*.affa-chunked'|";
	while(<FI>)
		{
		$_=~/\.\/(.*)\/(.*)\.affa-chunked/;
		my $path=$1; my $file=$2;
		next if not -f "$job{'RootDir'}/$jobname/$cmd/$path/$file.affa-chunked/00/00/00/00/00/00/00/00.bz2";

		print "Unchunking $cmd/$path/$file ..." if $verbose;
		dbg( "Unchunking $job{'RootDir'}/$jobname/$cmd/$path/$file" );

		removeDir("$job{'RootDir'}/$jobname/$cmd/$path/$file.tmp");
		my @cmd=("cd", "$arfull/$path/$file.affa-chunked", "&&", "find", ".", "-type f", "-name '*.bz2'", 
				"|", "sort", "|", "xargs bzcat", ">>$arfull/$path/$file.tmp");
		ExecCmd( @cmd, 0);
		my $linkDest='';
		foreach my $s (@linkdir)
			{
			if( -f "$job{'RootDir'}/$jobname/$s/$path/$file" )
				{
				$linkDest .= "--link-dest='$job{'RootDir'}/$jobname/$s/$path' ";
				}
			}
		if( $linkDest )
			{
			dbg( "Linking against other archives." );
			@cmd=(
				$rsyncLocal,
				'--archive',
				'--partial',
				'--stats',
				'--inplace',
				"--numeric-ids",
				"--modify-window=".(86400*365),
				"--remove-source-files",
				$linkDest,
				"$job{'RootDir'}/$jobname/$cmd/$path/$file.tmp",
				"$job{'RootDir'}/$jobname/$cmd/$path/$file");
			if( not ExecCmd( @cmd, 0) )
				{
				removeDir("$job{'RootDir'}/$jobname/$cmd/$path/$file.affa-chunked");
				}
			}
		else
			{
			moveFileorDir( "$job{'RootDir'}/$jobname/$cmd/$path/$file.tmp", "$job{'RootDir'}/$jobname/$cmd/$path/$file" );
			removeDir("$job{'RootDir'}/$jobname/$cmd/$path/$file.affa-chunked");
			}
		print " done.\n" if $verbose;
		}
	unlink("$job{'RootDir'}/$jobname/$cmd/.AFFA-CHUNK-FLAG") if -f "$job{'RootDir'}/$jobname/$cmd/.AFFA-CHUNK-FLAG";
	close FI;
	}


###
