#!/usr/bin/perl ################################################################################ rt - restic wrapper (mwx'2026) use File::Basename;use Term::ReadKey;use Getopt::Std;getopts('c:n:i:t:p:'); $VERSION='1.5.6'; $CONFIG='default';$CONFIG=$opt_c if ($opt_c!~/^\s*$/); # set deault options options chomp($host=`hostname`);$host=~s/^([^\.]+).*/$1/; $NAME=$host; $POLICY='--keep-daily 14 --keep-weekly 12 --keep-monthly 12 --keep-yearly 100'; for ('/db/etc/rt.conf',$ENV{"HOME"}.'/.rt.conf') { $CONF=$_ if (-f $_);} # find/load/check configuration die "Fatal: no config file found\n" if ($CONF=~/^\s*$/); require($CONF); $NAME = $opt_n if ($opt_n!~/^\s*$/); # overwrite options with commandline (name, id, tag, password) $ID = $opt_i if ($opt_i!~/^\s*$/); $TAG = $opt_t if ($opt_t!~/^\s*$/); $PASSWORD = $opt_p if ($opt_p!~/^\s*$/); for ('$NAME','$TARGET','$SRC','$RESTIC','$LOG') { die "Fatal: missing $_ in config\n" if (eval($_)=~/^\s*$/); } $ID='0' if ($ID=~/^\s*$/); # set repository $REPOSITORY="$TARGET/$NAME.$ID"; print "rt $VERSION, Name: $NAME, Repository: $REPOSITORY, Config: $CONFIG\n"; open(PW,"> $PWFILE");print PW "$PASSWORD\n";close(PW); $ENV{'RESTIC_REPOSITORY'} = $REPOSITORY; # set environment $ENV{'RESTIC_PASSWORD_FILE'} = $PWFILE; $r=`$RESTIC version`; # find restic version if ($r=~/restic\s+([\d\.]+)\s+compiled/) { $RVERSION=$1; @RV=split('\.',$1);$RVN=sprintf("%02d%02d%02d",$RV[0],$RV[1],$RV[2]); } $TMP="/tmp"; my(@z)=localtime(time());$z[4]+=1;$z[5]-=100;$z[6]+=1; $z=sprintf("%02d\.%02d\.%02d %02d\:%02d",$z[3],$z[4],$z[5],$z[2],$z[1]); if ($ARGV[0] eq 'cleanup') { # ----------------------------------------------------------- forget, prune, check $ts=time(); print "$RESTIC -r $REPOSITORY forget $POLICY --prune\n"; # forget & prune $rms=0; open(IN,"$RESTIC forget $POLICY --prune |"); while() { print; $rms=$1 if (/^(\d+)\s+snapshots\s+have\s+been\s+removed/) } print "$RESTIC -r $REPOSITORY check\n"; # check open(IN,"$RESTIC check |"); while() { print; chomp($lastline=$_); } $rt=time()-$ts; if ($HTTP_LOG!~/^\s*$/) { # http loging $tmp= "RTC,$VERSION/$RVERSION,$NAME,$ID,$CONFIG,$TAG,$REPOSITORY,$lastline,$rms,$rt"; use LWP::UserAgent; my $ua = new LWP::UserAgent(); $response = $ua->post("https://$HTTP_LOG", Content => $tmp); } if ($MY_HOST!~/^\s*$/) { # mysql loging use DBI; $dbh=DBI->connect("DBI:mysql:$MY_DB:$MY_HOST:$MY_PORT",$MY_USER,$MY_PASSWD,{RaiseError=>1,PrintError=>1}); $sql="INSERT INTO resticcleanup VALUES(now(),'$VERSION/$RVERSION', '$NAME','$ID','$CONFIG','$TAG','$REPOSITORY','$lastline',$rms,$rt)"; $dbh->do($sql); } } elsif ($ARGV[0] eq 'backup' || $ARGV[0] eq 'delaybackup') { # ----------------------------------- run backup $snap='';$added='';$total=''; if ($ARGV[0] eq 'delaybackup') { printf "$z --- delaybackup (%d sec) ---\n",&dfn($NAME); sleep(&dfn($NAME)) } else { printf "$z --- backup ---\n"; } print "$RESTIC -r $REPOSITORY -vv backup $EXCLUDE $SRC\n"; $ts=time(); open(IN,"$RESTIC -vv backup $EXCLUDE $SRC |"); while() { print if (!/^unchanged/); if (/Files:\s*(\d+)\s*new,\s*(\d+)\s*changed,\s*(\d+)\s*unmodified/) { $fn+=$1;$fc+=$2;$fu+=$3; } if (/Dirs:\s*(\d+)\s*new,\s*(\d+)\s*changed,\s*(\d+)\s*unmodified/) { $fn+=$1;$fc+=$2;$fu+=$3; } $snap=$1 if (/snapshot\s+([^\s]+)\s+saved/); $added=&deform($1) if (/Added\s+to\s+the\s+repo:\s+(.*)/); $added=&deform($1) if (/Added\s+to\s+the\s+repository:\s+([^\(]*)/); $total=&deform($1) if (/processed\s+\d+\s+files,\s+(.*)\s+in/); $files=$1 if (/processed\s+(\d+)\s+files,\s+.*\s+in/); } if ($snap!~/^\s*$/ && $added!~/^\s*$/ && $total!~/^\s*$/) { $rt=time()-$ts; open(LOG,">> $LOG"); # write logfile print LOG "$z [$NAME/$snap], Files: $files, New: $fn, Changed: $fc, ". "Unmodified: $fu, Added: $added, Total: $total, Runtime: $rt\n"; close(LOG); if ($HTTP_LOG!~/^\s*$/) { # http loging $tmp= "RT,$VERSION/$RVERSION,$NAME,$ID,$CONFIG,$TAG,$REPOSITORY,". "$snap,$files,$fn,$fc,$fu,$added,$total,$rt"; use LWP::UserAgent; my $ua = new LWP::UserAgent(); $response = $ua->post("https://$HTTP_LOG", Content => $tmp); } if ($MY_HOST!~/^\s*$/) { # mysql loging use DBI; $dbh=DBI->connect("DBI:mysql:$MY_DB:$MY_HOST:$MY_PORT",$MY_USER,$MY_PASSWD,{RaiseError=>1,PrintError=>1}); $sql="INSERT INTO restic VALUES(now(),'$VERSION/$RVERSION', '$NAME','$ID','$CONFIG','$TAG','$REPOSITORY','$snap',$files,$fn,$fc,$fu,$added,$total,$rt)"; $dbh->do($sql); } } } elsif ($ARGV[0] eq 'password') { # ----------------------------------------------- setup restic password file system("mkdir -p ".&dirname($PWFILE)) if (!-d &dirname($PWFILE)); print "new repository password: "; chomp($pwd = &ReadLine(0)); if ($pwd!~/^\s*$/) { open(OUT,"> $PWFILE");print OUT "$pwd\n";close(OUT); } } elsif ($ARGV[0] eq 'help' || $ARGV[0]=~/^\s*$/) { # ----------------------------------------------- show help &help(); &advhelp() if ($ARGV[0] eq 'help'); } else { # ------------------------------------------------------------------------------ other restic commands system("$RESTIC @ARGV"); if ($ARGV[0] eq 'init') { system("rm -f /var/log/restic.log /var/log/rt.log"); } } ####################################################################################################### SUPPORT sub deform() { # transform sizes my($s)=@_; return $1<<40 if ($s=~/([\d\.]+)\s*TiB/i); return $1<<30 if ($s=~/([\d\.]+)\s*GiB/i); return $1<<20 if ($s=~/([\d\.]+)\s*MiB/i); return $1<<10 if ($s=~/([\d\.]+)\s*KiB/i); return $1 if ($s=~/([\d\.]+)\s*B/i); } sub dfn() { # calculate delay from string my($s)=@_;my $v=0;my $n=1; $v+=ord($_)*$n++ for (split('',$s)); return ($v%24)*5*60; } sub help() { # help print "\nrt v$VERSION - restic wrapper (mwx'2022)\n"; print <<'ENDOFHELP'; USAGE: ------ rt [options] backup run restic backup rt [options] delaybackup run delayed restic backup rt [options] cleanup run forget, prune and check rt [options] password setup restic password file rt [options] init initialize restic repository rt help advanced help rt other commands will be forwarded directly to restic options: -c sets $CONFIG (default is 'default') -n overwrite $NAME -i overwrite $ID -t overwrite $TAG -p overwrite $PASSWORD ENDOFHELP } sub advhelp() { # advanced help print <<'ENDOFHELP'; CONFIG FILE (/db/etc/rt.conf) ----------------------------- $NAME = 'NAME'; # backup name $ID = '0'; # repository id (optional, default is 0) $TAG = 'TAG'; # custom tag for logging (optional) $TARGET = "/backup/restic"; # restic target = "s3:http://188.68.34.116:9234/restic"; = "b2:mwx002:restic/server"; $SRC = '/etc /db /var /root /home /usr/local /opt'; # source paths $EXCLUDE = '-e /var/lib/lxcfs -e /var/log/journal'; # exclude options $RESTIC = '/db/bin/restic'; # restic binary $LOG = '/var/log/restic.log'; # log file $PWFILE = '/db/etc/restic.pw'; # restic password file $HTTP_LOG = 'HOST'; # host for logging via https $MY_HOST = 'HOST'; # mysql host $MY_PORT = 'PORT'; # mysql port $MY_USER = 'USER'; # mysql user $MY_PASSWD = 'PASSWORD'; # mysql password $MY_DB = 'DATABASE'; # mysql database $ENV{'AWS_ACCESS_KEY_ID'} = '123456'; # S3 token example $ENV{'AWS_SECRET_ACCESS_KEY'} = 'abcdefg'; $ENV{'B2_ACCOUNT_ID'} = '123456'; # B2 token example $ENV{'B2_ACCOUNT_KEY'} = 'abcdefg'; MYSQL LOGGING TABLES: --------------------- CREATE TABLE `restic` ( `ts` timestamp NULL DEFAULT NULL, `version` varchar(255) DEFAULT NULL, `name` varchar(255) DEFAULT NULL, `id` int(11) DEFAULT NULL, `config` varchar(255) DEFAULT NULL, `tag` varchar(255) DEFAULT NULL, `repository` varchar(255) DEFAULT NULL, `snapshot` varchar(255) DEFAULT NULL, `files` bigint(11) DEFAULT NULL, `new` bigint(11) DEFAULT NULL, `changed` bigint(11) DEFAULT NULL, `unmodified` bigint(11) DEFAULT NULL, `added` bigint(11) DEFAULT NULL, `total` bigint(11) DEFAULT NULL, `runtime` int(11) DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=latin1; CREATE TABLE `resticcleanup` ( `ts` timestamp NULL DEFAULT NULL, `version` varchar(255) DEFAULT NULL, `name` varchar(255) DEFAULT NULL, `id` int(11) DEFAULT NULL, `config` varchar(255) DEFAULT NULL, `tag` varchar(255) DEFAULT NULL, `repository` varchar(255) DEFAULT NULL, `txt` varchar(255) DEFAULT NULL, `rmsnap` int(11) DEFAULT NULL, `runtime` int(11) DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=latin1; CRONTAB ENTRIES: --------------- 0 2 * * * /db/bin/rt delaybackup >> /var/log/rt.log 2>&1 0 7 * * 6 /db/bin/rt cleanup >> /var/log/rt.log 2>&1 ENDOFHELP } ########################################################################################################### END