#!/usr/bin/perl ############################################################################################# mbackup (mwx'2023) $VERSION='3.2.0'; use Getopt::Std;getopts('vVDWMYhsfc:');$opt_v=1 if ($opt_V); use Net::Ping; use Fcntl ':flock'; chomp($HOST=`hostname`); &help if($opt_h); $NOW=time(); my(@z)=localtime($NOW);$z[4]+=1;$z[5]-=100;$z[6]+=1; $z=sprintf("%02d.%02d-%02d:%02d",$z[3],$z[4],$z[5],$z[2],$z[1]); $MAX{'daily'}=7; # default values $MAX{'weekly'}=4; $MAX{'monthly'}=12; $MAX{'yearly'}=3; $FLAT=0; $RSYNCOPTS='--no-o --no-g --chmod=ugo=rwX'; $RSYNCRETRIES=3; $CONFIG='';$REQUESTCONFIG=$ARGV[0]; # load configuration for ($opt_c,"/db/etc/mbackup.conf",$ENV{'HOME'}."/etc/mbackup.conf",$ENV{'HOME'}."/.mbackup.conf") { if (-f) { $confile=$_; last; } } require $confile; for (keys(%GROUPS)) { # build group list if ($_ eq $REQUESTCONFIG) { push(@GROUP,@{$GROUPS{$_}}); $CONFIG=$REQUESTCONFIG; } } if ($CONFIG=~/^\s*$/) { # check for valid config &log("ERROR: config '$REQUESTCONFIG' not found",1); exit; } $REMOTE=0;$REMOTE=1 if ($TARGETDIR!~/^\//); if ($opt_s) { # show configuration if (@GROUP) { print "\n"; print "mode: group\n\n"; print "group sleep: $GROUPSLEEP\n\n" if ($GROUPSLEEP>0); for $job (@GROUP) { print "/db/bin/mbackup -c $confile $job\n"; } print "\n"; } else { print "\n"; if ($FLAT==1) { print "mode: flat\n"; } else { print "mode: $MAX{'daily'} daily, $MAX{'weekly'} weekly, "; print "$MAX{'monthly'} monthly, $MAX{'yearly'} yearly\n"; } if ($#EXCLUDE>-1) { print "\n"; $tmp="";$tmp.="$_, " for (@EXCLUDE);$tmp=~s/\,\s+$//; print "exclude: $tmp\n"; } print "\n"; $l=0; for (keys(%SRC)) { $l=length($_) if length($_)>$l; } printf "%-${l}s -> %s\/%s\n",$_,$TARGETDIR,$SRC{$_} for (keys(%SRC)); print "\n"; } exit; } if (@GROUP) { # run backup group for $job (@GROUP) { $cmd="/db/bin/mbackup -c $confile $job"; &log("{$CONFIG} $cmd",1); system($cmd); if ($GROUPSLEEP>0 && $job ne $GROUP[-1]) { &log("group loop sleep ($GROUPSLEEP seconds)",1); sleep($GROUPSLEEP); } } exit; } if ($MY_LOG) { # mysql loging use DBI; $dbh=DBI->connect("DBI:mysql:$MY_DB:$MY_HOST:",$MY_USER,$MY_PASSWD,{RaiseError=>1,PrintError=>1}); } $mode=''; # set mode (daily,weekly,monthly,yearly) my($sec,$min,$hour,$mday,$mon,$year,$wday,$yday)=localtime($NOW); $mon++;$year+=1900; $z=sprintf("%02d\.%02d\.%04d %02d\:%02d",$mday,$mon,$year,$hour,$min); $mode='daily' if ( $MAX{'daily'}>0); $mode='weekly' if ( $wday==0 && $MAX{'weekly'}>0); $mode='monthly' if ( $mday==1 && $MAX{'monthly'}>0); $mode='yearly' if ( $yday==0 && $MAX{'yearly'}>0); $mode='daily' if ($opt_D); # set mode manual $mode='weekly' if ($opt_W); $mode='monthly' if ($opt_M); $mode='yearly' if ($opt_Y); if (defined($PING)) { # check if host is reachable if (!Net::Ping->new("icmp", 15)->ping($PING)) { &log("ERROR: host $PING not reachable",1);exit; } } if (!-d "$TARGETDIR" && $REMOTE==0) { # check target dir if ($opt_f) { system("mkdir $TARGETDIR"); if (!-d "$TARGETDIR") { &log("ERROR: target dir creation failed (mkdir $TARGETDIR)",1); exit; } } else { &log("ERROR: can not access target dir (mkdir $TARGETDIR)",1); exit; } } for $srcdir (keys(%SRC)) { # check source dir if ($srcdir=~/^\// && !-d $srcdir) { &log("ERROR: mising source dir '$srcdir'",1); exit; } } undef(%keycount); # check for duplicate source keys for $srcdir (keys(%SRC)) { $keycount{$SRC{$srcdir}}++; if ($keycount{$SRC{$srcdir}}>1) { &log("ERROR: duplicate source key '$SRC{$srcdir}'",1); exit; } } if ($FLAT==1 || $REMOTE==1) { # flat mode $toterr=0;$totfiles=0;$tottfi=0;$totbytes=0;$tottby=0;$totrt=0; for $srcdir (keys(%SRC)) { $subname=$SRC{$srcdir}; $cmd="/usr/bin/rsync -av $RSYNCOPTS --stats --delete " . &exf() . " --numeric-ids $srcdir/. $TARGETDIR/$subname"; ($err,$fi,$tfi,$by,$tby,$rt,$try)=&rsync($cmd); &log("[$CONFIG] $srcdir,$TARGETDIR ($err,$tfi,$fi,$tby,$by,$rt)",1); if ($MY_LOG) { # mysql logging $sql="INSERT INTO mbackup VALUES(0,now(),'$HOST','$VERSION','$CONFIG',". "'$srcdir','$TARGETDIR',$fi,$tfi,$by,$tby,$rt,$err,$try)"; $dbh->do($sql); } if ($HTTP_LOG!~/^\s*$/) { # http logging $tmp= "MBACKUP,$HOST,$VERSION,$CONFIG,$srcdir,$TARGETDIR,$fi,$tfi,$by,$tby,$rt,$err,$try"; use LWP::UserAgent; my $ua = new LWP::UserAgent(); $response = $ua->post("http://$HTTP_LOG", Content => $tmp); } $toterr+=$err;$totfi+=$fi;$tottfi+=$tfi;$totby+=$by;$tottby+=$tby;$totrt+=$rt; &log("rsync error $err in $srcdir/. -> $TARGETDIR/$subname") if ($err); } if ($toterr==0) { &log("[$CONFIG] mbackup successful finished ($tottfi,$totfi,$tottby,$totby,$totrt)",1) } exit; } if ($mode eq '') { # check if mode is defiend otherwise exit &log("ERROR: undefined mode",1); exit; } $yweekly=0;$ymonthly=0;$yyearly=0; # search for missing snapshot opendir(DIR,$TARGETDIR); while ($file=readdir(DIR)) { my(@stat)=stat("$TARGETDIR/$file"); if ($file=~/^weekly\.\d+$/) { $yweekly=$stat[9] if ($stat[9]>$yweekly); } if ($file=~/^monthly\.\d+$/) { $ymonthly=$stat[9] if ($stat[9]>$ymonthly); } if ($file=~/^yearly\.\d+$/) { $yyearly=$stat[9] if ($stat[9]>$yyearly); } } closedir(DIR); if ($NOW-$yweekly>86400*10 && $yweekly!=0 && $MAX{'weekly'}>0) { &log("WARNING: no weekly snapshot for more than 10 days",1); } if ($NOW-$ymonthly>86400*40 && $ymonthly!=0 && $MAX{'monthly'}>0) { &log("WARNING: no monthly snapshot for more than 40 days",1); } if ($NOW-$yyearly>86400*400 && $yyearly!=0 && $MAX{'yearly'}>0) { &log("WARNING: no yearly snapshot for more than 400 days",1); } undef(%SNAPLIST); # read snapshot list for current mode opendir(DIR,$TARGETDIR); while ($file=readdir(DIR)) { my(@stat)=stat("$TARGETDIR/$file"); $SNAPLIST{$1}=$stat[9] if ($file=~/^$mode\.(\d+)/); } closedir(DIR); for $n (sort {$b<=>$a} keys(%SNAPLIST)) { # snapshot cycling $m=$n+1; if (-d "$TARGETDIR/$mode.$n" && !-e "$TARGETDIR/$mode.$m") { if ($m>=$MAX{$mode}) { &cmd("rm -rf $TARGETDIR/$mode.$n"); } else { &cmd("mv $TARGETDIR/$mode.$n $TARGETDIR/$mode.$m"); } } else { $tmp=""; $tmp="($TARGETDIR/$mode.$n not found)" if (!-d "$TARGETDIR/$mode.$n"); $tmp="($TARGETDIR/$mode.$m exists)" if (-e "$TARGETDIR/$mode.$m"); &log("ERROR: error while snapshot cycling $tmp",1);exit; } } $youngest='';$y=0; # search for youngest snapshot opendir(DIR,$TARGETDIR); while ($file=readdir(DIR)) { my(@stat)=stat("$TARGETDIR/$file"); if ($file=~/^((daily|weekly|monthly|yearly)\.\d+)/) { if ($stat[9]>$y) { $y=$stat[9]; $youngest=$file; } } } closedir(DIR); &cmd("mkdir -m 0755 -p $TARGETDIR/$mode.tmp/"); # create new $mode.0 &cmd("touch $TARGETDIR/$mode.tmp/"); if (-d "$TARGETDIR/$mode.tmp") { # build new snapshot $toterr=0;$totfi=0;$tottfi=0;$totbytes=0;$tottby=0; for $srcdir (keys(%SRC)) { $subname=$SRC{$srcdir}; $linkdest=""; if ($youngest!~/^\s*$/) { if (-d "$TARGETDIR/$youngest" && -d "$TARGETDIR/$youngest/$subname") { $linkdest="--link-dest=$TARGETDIR/$youngest/$subname" } } $cmd="/usr/bin/rsync -av $RSYNCOPTS --no-o --no-g --chmod=ugo=rwX --stats --delete " . &exf() . " --numeric-ids $linkdest $srcdir/. $TARGETDIR/$mode.tmp/$subname"; ($err,$fi,$tfi,$by,$tby,$rt,$try)=&rsync($cmd); &log("[$CONFIG] $srcdir,$TARGETDIR ($err,$tfi,$fi,$tby,$by,$rt)",1); if ($MY_LOG) { # mysql logging $sql="INSERT INTO mbackup VALUES(0,now(),'$HOST','$VERSION','$CONFIG',". "'$srcdir','$TARGETDIR',$fi,$tfi,$by,$tby,$rt,$err,$try)"; $dbh->do($sql); } if ($HTTP_LOG!~/^\s*$/) { # http logging $tmp= "MBACKUP,$HOST,$VERSION,$CONFIG,$srcdir,$TARGETDIR,$fi,$tfi,$by,$tby,$rt,$err,$try"; use LWP::UserAgent; my $ua = new LWP::UserAgent(); $response = $ua->post("http://$HTTP_LOG", Content => $tmp); } $toterr+=$err;$totfi+=$fi;$tottfi+=$tfi;$totby+=$by;$tottby+=$tby;$totrt+=$rt; } if ($toterr==0) { &log("[$CONFIG] mbackup successful finished ($tottfi,$totfi,$tby,$by,$totrt)",1); &cmd("mv $TARGETDIR/$mode.tmp $TARGETDIR/$mode.0"); utime $NOW,$NOW,"$TARGETDIR/$mode.0"; } } else { &log("ERROR: can not access $mode.tmp",1); } ######################################################################################################## SUPPORT sub cmd() { # run command my($cmd)=@_; &log($cmd); system($cmd); } sub log() { # write logs my($str,$forceprint)=@_; my(@z)=localtime(time());$z[4]+=1;$z[5]-=100;$z[6]+=1; $ts=sprintf("%02d\.%02d\.%02d %02d\:%02d",$z[3],$z[4],$z[5],$z[2],$z[1]); if ($LOGFILE!~/^\s+$/) { open(LOG,">> $LOGFILE"); flock(LOG, LOCK_EX); print LOG "$ts $$/$VERSION/$CONFIG $str\n"; close(LOG); } print "$str\n" if ($opt_v || $forceprint); } sub rsync() { # run rsync my($rsynccmd)=@_; my($files)=0;my($tfi)=0;my($bytes)=0;my($tby)=0;my($error)=0;my($try)=0; $runtime=time(); $retries=0; for $try (1..$RSYNCRETRIES) { $rsynclog=""; open(RSYNC,"$rsynccmd |");while() { print if ($opt_V); if (/Number of files:\s*([\d\,]+)/) {;$files=$1;$files=~s/,//g;} if (/Number of regular files transferred:\s*([\d\,]+)/) {;$tfi=$1;$tfi=~s/,//g;} if (/Total file size:\s*([\d\,]*)\s*bytes/) {;$bytes=$1;$bytes=~s/,//g;} if (/Total transferred file size:\s*([\d\,]*)\s*bytes/) {;$tby=$1;$tby=~s/,//g;} } close(RSYNC); $error=($? >> 8); $runtime=time()-$runtime; if ($error==0) { &log("$rsynccmd (successful)"); } else { &log("$rsynccmd (error $error in try $try)") } last if ($error==0); $retries++; } return ($error,$files,$tfi,$bytes,$tby,$runtime,$retries); } sub exf() { # exclude file $ex=''; if ($#EXCLUDE>-1) { $exfile="/tmp/mbackup.exclude.$CONFIG"; open(OUT,"> $exfile"); print OUT "$_\n" for (@EXCLUDE); close(OUT); $ex="--exclude-from $exfile --delete-excluded"; } return $ex; } sub buildsrc() { # build source list (used in config) my($base,@paths)=@_; for $path (@paths) { if ($path=~/\/([^\/]+)$/) { $SRC{"$base$path"}=$1; } } } sub sc() { # build simple configuration (used in config) my($uhd,$dirs,$exclude,$grp,$opt)=@_; $mode=0; if ($uhd=~/^([^\.]+)\.(.*)\:\:(.*)$/) {;$user=$3;$config=$1;$domain=$2;$mode=1;} if ($uhd=~/^(\w+)\@([^\.]+)\.(.*)$/) {;$user=$1;$config=$2;$domain=$3;$mode=2;} if ($mode>0) { %opt=%$opt; $config=$opt{name} if ($opt{name}); @grp=@$grp; for $g (@grp) { push(@{$GROUPS{$g}},$config) } if ($REQUESTCONFIG eq $config) { $RSYNCOPTS=$opt{rsync} if ($opt{rsync}); $PING=$opt{ping} if ($opt{ping}); $TARGETBASE=$opt{target} if ($opt{target}); if ($opt{versions}=~/(\d+):(\d+):(\d+):(\d+)/) { $MAX{'daily'}=$1; $MAX{'weekly'}=$2; $MAX{'monthly'}=$3; $MAX{'yearly'}=$4; $FLAT=0; } else { $FLAT=1; } @DIRS=@$dirs; @EXCLUDE=@$exclude; $TARGETDIR="$TARGETBASE/$config"; for $path (@DIRS) { $tpath=$path;$tpath=~s/^\s*\///;$tpath=~s/\//\./g; $SRC{"$uhd$path"}=$tpath if ($mode==1); $SRC{"$uhd:$path"}=$tpath if ($mode==2); } $CONFIG=$REQUESTCONFIG; } } } sub help() { # help print "mbackup v$VERSION\n"; print <<'ENDOFHELP'; USAGE: ------ mbackup [-v] [-V] [-DWMY] mbackup -h -D force daily mode -W force weekly mode -M force monthly mode -Y force yearly mode -v verbose output -V very verbose output -f force creation of target directory -h this help CONFIG FILE (/db/etc/mbackup.conf) GLOBAL OPTIONS: -------------------------------------------------- $LOGFILE = "/var/log/mbackup.log"; # logfile location $TARGETBASE = "/data/backup"; # targetbase for simple config $HTTP_LOG = 'log.mxwx.org'; # http logging server $MY_LOG = 1; # enable/disable mysql logging (deprecated) $MY_HOST = 'localhost'; $MY_USER = ''; $MY_PASSWD = ''; $MY_DB = ''; SIMPLE BACKUP JOB CONFIGURATION (/db/etc/mbackup.conf): ------------------------------------------------------- &sc(target,[paths],[exclude],[groups],{ option=>value, .. } example: &sc('root@host.domain',['/etc','/var','/home'],['/lib/lxcfs'],['all']); &sc('host.domain::root',['/etc','/var','/home'],[],['all'], { rsync=>"-e 'ssh -p 2234'" }); &sc('root@host.domain',['/etc','/var','/home'],[],[] { versions=>'7:4:12:3' }); options: name => 'name' # set configuration name rsync => 'rsync options' # set additional rsync options ping => 'ip address' # apply ping test before backup version => 'daily:weekly:monthly:yearly' # define backup versioning target => '/path/to/mbackup' # define backup destination BACKUP JOB CONFIGURATION (/db/etc/mbackup.conf): ------------------------------------------------ if ($REQUESTCONFIG eq 'NAME') { # basic backup job config $CONFIG=$REQUESTCONFIG; } if ($REQUESTCONFIG eq 'NAME') { # backup group definition @GROUP=('NAME1','NAME2',...); $CONFIG=$REQUESTCONFIG; } push(@{$GROUPS{'all'}},'NAME'); # add config to group BACKUP JOB OPTIONS: ------------------- # backup target definition (use only once) $TARGETDIR='/backup/NAME' # local traget directory $TARGETDIR='10.0.1.1::root/backup/NAME' # rsync protocol target $TARGETDIR='root@10.0.1.1:/backup/NAME' # ssh traget directory # backup source definition (multiple allowd, different keys required) $SRC{'/etc'} = 'etc1'; # local source $SRC{'10.0.1.1::root/etc'} = 'etc2'; # remote source (rsync protocol) $SRC{'root@10.0.1.1:/etc'} = 'etc3'; # remote source (ssh) # define eculsions (absolut path definitions from source path) @EXCLUDE=('/path1','/path2',...); # define number of backups to keep, at least one value must be greater than 0 $MAX{'daily'} = 7; # keep n daily backup (default=7) $MAX{'weekly'} = 4; # keep n weekly backup (default=4) $MAX{'monthly'} = 12; # keep n monthly backup (default=12) $MAX{'yearly'} = 3; # keep n yearly backup (default=3) $FLAT=1; # only keep one backup (default=0) $PING='10.0.1.1'; # perform ping test before backup run $RSYNCOPTS="-e 'ssh -p 2234'"; # add extra options for rsync $RSYNCRETRIES=3; # number of retries if rsync fails MYSQL LOGGING TABLE: -------------------- CREATE TABLE `mbackup` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `ts` timestamp NULL DEFAULT NULL, `host` varchar(255) DEFAULT NULL, `version` varchar(255) DEFAULT NULL, `config` varchar(255) DEFAULT NULL, `source` varchar(255) DEFAULT NULL, `target` varchar(255) DEFAULT NULL, `files` bigint(11) DEFAULT NULL, `transfiles` bigint(11) DEFAULT NULL, `bytes` bigint(11) DEFAULT NULL, `transbytes` bigint(11) DEFAULT NULL, `runtime` int(11) DEFAULT NULL, `error` int(11) DEFAULT NULL, `retries` int(11) DEFAULT '0', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=latin1; ENDOFHELP exit; } ############################################################################################################ END