#!/usr/bin/perl # SERIAL: 25 use strict; my $CONFPATH=set_confpath('/etc/backup', '/usr/local/scripts/etc/backup', '/usr/local/etc/backup'); my ($O, $OUT_FILE); quit( invalidate_argv(filter_args(@ARGV)) ); quit( no_conf_file(filter_args(@ARGV)) ); download_updates( 'http://mirror.positive-internet.com/rsync_backup/', 'http://main.cream.org/rsync_backup/' ); my %Interp; my $G_finally; foreach my $arg ( @ARGV ) { my @subargs=split(/[:=]/, $arg); my $type=$subargs[0]; undef %Interp; for (my $i=0; $i<@subargs; $i++) { $Interp{"[$i]"}=$subargs[$i] }; $Interp{'[name]'}=$type; my %conf=read_conf($type); my $g=$conf{GLOBAL}; showconf(\%conf), exit() if $g->{showconfonly}; $O=$g->{output}; undef $OUT_FILE; $OUT_FILE=1 if substr($O,0,1) eq '/'; my $date=localtime(time); open (OUT, ">$O/$type-".( split(' ', $date) )[0] ) if $OUT_FILE; out("++++++ $type backup beginning",1); $G_finally=$g->{finally} || undef; my $g_method=lc $g->{method} || 'rsync'; my $g_exclude=make_exclude($g->{exclude}); my $g_include=make_include($g->{include}); my ($g_preswap1, $g_preswap2)=split (' ', $g->{preswap}) if $g->{preswap}; my ($g_postswap1, $g_postswap2)=split (' ', $g->{postswap}) if $g->{postswap}; mount($g->{mount}, $g->{mail}) unless $g->{dummy}; halt_on_file($g->{halt}) if $g->{halt}; proceed_on_file($g->{proceed}) if $g->{proceed}; if ($g->{prerun}) { out("Pre-running: $g->{prerun}"); unless ($g->{dummy}) { system($g->{prerun}) && out("Prerun error: $!"); } } if ($g_preswap2) { out("Pre-swapping: $g_preswap1 to $g_preswap2"); unless ($g->{dummy}) { swap_dirs( first => $g_preswap1, second => $g_preswap2, hostname => $g->{hostname}, target => $g->{target} ); } } foreach ( @{$conf{LIST}} ) { my $l=$conf{$_}; my $l_method=$l->{method} || $g_method || 'rsync'; my $l_switches=$l->{switches}; my $l_switches=$g->{switches} if !$l_switches && $l_method eq $g_method; my $l_exclude=make_exclude($l->{exclude}) || $g_exclude; my $l_include=make_include($l->{include}) || $g_include; my $l_target=$l->{target} || $g->{target}; my $l_mail=$l->{mail} || $g->{mail}; my $l_hostname=$l->{hostname} || $g->{hostname}; my $l_username=$l->{username} || $g->{username}; my $l_password=$l->{password} || $g->{password}; my $l_halt =$l->{halt} || $g->{halt}; my $l_proceed =$l->{proceed} || $g->{proceed}; my $l_dummy =$l->{dummy}; my $l_real =$l->{real}; my $l_prerun =$l->{prerun}; my $l_postrun =$l->{postrun}; my $l_nobackup=$l->{nobackup} || $g->{nobackup}; my $l_dobackup=$l->{dobackup} || $g->{dobackup}; my $l_label=$l->{target} || "backup"; my $l_mount=$l->{mount}; my ($l_preswap1, $l_preswap2)=split (' ', $l->{preswap}) if $l->{preswap}; my ($l_postswap1, $l_postswap2)=split (' ', $l->{postswap}) if $l->{postswap}; $l_method='rsync' if !$l_method; out("\n*** Beginning $l->{label} ($_)"); mount($l_mount, $l_mail) unless $l_dummy || ($g->{dummy} && !$l_real); halt_on_file($l_halt) if $l_halt; proceed_on_file($l_proceed) if $l_proceed; if ($l_prerun) { out("Pre-running: $l->{prerun}"); unless ($l_dummy || ($g->{dummy} && !$l_real) ) { system($l_prerun) && out("Prerun error: $!"); } } if ($l_preswap2) { out("Pre-swapping: $l_preswap1 to $l_preswap2"); unless ($l_dummy || ($g->{dummy} && !$l_real) ) { swap_dirs( first => $l_preswap1, second => $l_preswap2, hostname => $l_hostname, target => $l_target ); } } unless ($l_nobackup && !$l_dobackup) { if (lc $l_method eq 'rsync') { rsync( switches => $l_switches, exclude => $l_exclude, include => $l_include, source => $_, target => $l_target, mail => $l_mail, hostname => $l_hostname, username => $l_username, password => $l_password, ) unless $l_dummy || ($g->{dummy} && !$l_real); }; } else { out("Not backing up, as per conditional"); } if ($l_postrun) { out("Post-running: $l_postrun"); unless ($l_dummy || ($g->{dummy} && !$l_real) ) { system($l_postrun) && out("Postrun error: $!"); } } if ($l_postswap2) { out("Post-swapping: $l_postswap1 to $l_postswap2"); unless ($l_dummy || ($g->{dummy} && !$l_real) ) { swap_dirs( first => $l_postswap1, second => $l_postswap2, hostname => $l_hostname, target => $l_target ); } } umount($l_mount, $l_mail) unless $l_dummy || ($g->{dummy} && !$l_real); out("*** Completed $l->{label} ($_)\n"); } if ($g->{postrun}) { out("Post-running: $g->{prerun}"); unless ($g->{dummy}) { system($g->{postrun}) && out("Postrun error: $!"); } } if ($g_postswap2) { out("Post-swapping: $g_postswap1 to $g_postswap2"); unless ($g->{dummy}) { swap_dirs( first => $g_postswap1, second => $g_postswap2, hostname => $g->{hostname}, target => $g->{target} ); } } umount($g->{mount}, $g->{mail}) unless $g->{dummy}; if ($G_finally) { my $return=system("$G_finally 0"); $return=0 if !$return; out("'Finally' script ran with exit status $return: $G_finally 0"); } out("++++++ $type backup concluding",1); close OUT if $OUT_FILE; } #--- sub swap_dirs { my %d=@_; my ($first, $second)=($d{first}, $d{second}); my $host=$d{hostname}; my $remote=1 if ($d{target}!~/\// || $d{target}=~/:$/) && $host; my $append=int(rand(1000000))+1; if (!$remote) { local_mv($first, "$first.$append"); local_mv($second, $first); local_mv("$first.$append", $second); } else { out("Note: Swapping remotely on $host"); remote_mv(hostname=>$host, source=>$first, target=>"$first.$append"); remote_mv(hostname=>$host, source=>$second, target=>$first); remote_mv(hostname=>$host, source=>"$first.$append", target=>$second); } } #--- sub remote_mv { my %d=@_; my $source=$d{source}; my $target=$d{target}; my $hostname=$d{hostname}; my $username=$d{username}; ssh( hostname => $hostname, username => $username, command => "/bin/mv $source $target" ); } #--- sub ssh { my %d=@_; my $username="$d{username}\@" if $d{username}; my $hostname=$d{hostname}; my $command=$d{command}; my $ignore=$d{ignore}; my $ssh="/usr/bin/ssh ${username}${hostname} $command"; system($ssh) && !$ignore && quit("Could not execute ssh command $ssh: $1"); } #--- sub local_mv { my ($host, $target, $ignore)=@_; return system("/bin/mv $host $target") && !$ignore && quit("Failed to move $host to $target: $!"); } #--- sub filter_args { my @arg=@_; my @filtered_arg; foreach (@arg) { my $new_arg=$_; if (/^([^:=]+)(.*)$/) { $new_arg=$1; } push (@filtered_arg, $new_arg); } return @filtered_arg; } #--- sub rsync { my %a=@_; out("Backing up $a{source} to $a{target}"); if ($a{source}=~/:$/ && $a{hostname}) { $ENV{RSYNC_PASSWORD}=$a{password}; $a{source}=~s/^(.*?)[:\/]+$/$1/; $a{source}="$a{hostname}".'::'."$a{source}/"; $a{source}=$a{username}.'@'.$a{source} if $a{username}; } if ( ($a{target}!~/\// || $a{target}=~/:$/) && $a{hostname}) { $ENV{RSYNC_PASSWORD}=$a{password}; $a{target}=~s/^(.*?)[:\/]*$/$1/; $a{target}="$a{hostname}"."::"."$a{target}/"; $a{target}=$a{username}.'@'.$a{target} if $a{username}; } my $cmd="$a{switches} $a{include} $a{exclude} $a{source} $a{target}"; $cmd="/usr/bin/rsync $cmd"; chomp $cmd; out("Command: $cmd"); my $error=system($cmd); if ($error && $error !=6144) { error_out(cmd => $cmd,mail=>$a{mail},source=>$a{source},error=>$error); } } #--- sub error_out { my %d=@_; my $cmd=$d{cmd}; my $error=$d{error} || $cmd; my $mail=$d{mail}; my $hostname=$d{hostname} || `hostname`; chomp $hostname; my $source=$d{source} || $hostname;; mail(to => $mail, from => "backup", subject => "Backup Failed on $hostname!", content => "Backup failed on $hostname:\n$cmd") if $mail; quit("Backup of $source aborted ($error)"); } #--- sub mail { my %data=@_; my $date=`date -R`; my $sendmail='/usr/sbin/sendmail'; $sendmail='/usr/lib/sendmail' if !-e $sendmail; open (MAIL, "|$sendmail -t"); print MAIL "To: $data{to}\r\n"; print MAIL "From: $data{from}\r\n"; print MAIL "Subject: $data{subject}\r\n"; print MAIL "Date: $date\r\n"; print MAIL "\r\n"; print MAIL "$data{content}\n"; close MAIL; } #--- sub make_exclude { my $exclude_string=shift || return; $exclude_string=~s/\s+/ --exclude=/g; $exclude_string="--exclude=$exclude_string"; return $exclude_string; } #--- sub make_include { my $include_string=shift || return; $include_string=~s/\s+/ --include=/g; $include_string="--include=$include_string"; return $include_string; } #--- sub mount { my ($mountpoint, $mail)=@_; return if !$mountpoint; system("umount $mountpoint 2> /dev/null"); out("Mounting $mountpoint"); my $error = system("mount $mountpoint"); error_out(cmd => "Mounting $mountpoint", mail=> $mail, error=>$error) if $error; } #--- sub umount { my ($mountpoint, $mail)=@_; return if !$mountpoint; out("Un-mounting $mountpoint"); my $error=system("umount $mountpoint"); error_out(cmd => "Mounting $mountpoint", mail=> $mail, error=>$error) if $error; } #--- sub read_conf { my $type=shift; my %conf; my $ra_conf=read_file("$CONFPATH/$type.conf"); my $in_section; my $dir; my $old_dir; my @local_settings =qw(dobackup nobackup postswap preswap read prerun postrun real dummy hostname username password mail switches label mount target exclude include halt proceed); my @global_settings=qw(dobackup nobackup postswap preswap read prerun postrun set showconfonly dummy hostname username password mail switches label mount target exclude include output halt proceed finally); my $i=-1; foreach (@$ra_conf) { chomp; $i++; next if /^\s*#/; s/^\s*(.*?)\s*/$1/; if ($in_section) { foreach my $set (@local_settings) { if (/^$set\s*[:=]\s*(.*?)\s*}?\s*$/i) { my $value=interpolate($1); if (lc $set eq 'read') { my $open=(substr($value,0,1) eq '/' ? $value : "$CONFPATH/$value"); my $ra_read=read_file($open); splice @$ra_conf, $i+1, 0, @$ra_read; } $conf{$dir}->{$set}=$value unless lc $set eq 'read'; } } undef $in_section, undef $dir if /\}\s*$/; } else { my $test_dir=interpolate($_); $test_dir=~/^(\/[^\s]*)/; $dir=$1 if $1; $test_dir=~/^(.*?):\s*$/; $dir="$1:" if $1 && !$dir && !is_in(\@global_settings, $1, 'u'); push(@{$conf{LIST}}, $dir) if $dir && $dir ne $old_dir; $old_dir=$dir; foreach my $set (@global_settings) { if (/^$set\s*[:=]\s*(.*?)\s*$/i) { if (lc $set eq 'set') { my ($key, $val)=split(/\s*\=\s*/, $1); $Interp{uc "[$key]"}=interpolate($val); } my $value=interpolate($1); if (lc $set eq 'read') { my $open=(substr($value,0,1) eq '/' ? $value : "$CONFPATH/$value"); my $ra_read=read_file($open); splice @$ra_conf, $i+1, 0, @$ra_read; } $conf{GLOBAL}->{$set}=$value unless lc $set eq 'read'; } } if (/{\s*$/) { $in_section=1; } } } return %conf; } #--- sub is_in { my ($ra_list, $check, $case)=@_; foreach (@$ra_list) { if (lc $case eq 'l') { return 1 if lc $_ eq $check; } elsif (lc $case eq 'u') { return 1 if uc $_ eq $check; } elsif (lc $case eq 'i') { return 1 if lc $_ eq lc $check; } else { return 1 if $_ eq $check; } } } #--- sub showconf { my $rh_conf=shift; foreach (keys %$rh_conf) { next if $_ eq 'LIST'; print "\n*** $_:\n"; my $rh_child=$rh_conf->{$_}; print "$_ = $rh_child->{$_}\n" foreach(keys %$rh_child); } } #--- sub read_text_record { my ($file, $key, $column)=@_; $column=1 if !$column; $file="$CONFPATH/$file" unless substr ($file, 0, 1) eq '/'; open (F, $file) || quit("Can't open $file: $!"); my $value; while (defined (my $line=) ) { next if $line=~/^\s*\#/; my @cols=split (/\s*:\s*/, $line); $value=$cols[$column] if $cols[0] eq $key; last if $value; } chomp $value; return $value; } #--- sub interpolate { my $value=shift; setup_interp() if !$Interp{'[dayname]'}; until ( $value!~/\[ [^\]]+ \]/x ) { $value=~s/(\[ \! [^\]\[]+ \]) /interpolate_file($1)/exg; $value=~s/(\[ \$ [^\]\[]+ \]) /interpolate_env($1)/exg; $value=~s/\[([^:\[\]]+):([^:\[\]]+):?([^:\[\]]*)\]/read_text_record($1, $2, $3)/exg; $value=~s/(\[ [a-zA-Z0-9\-_\.\/]+ \]) /$Interp{$1}/xg; } return $value; } #--- sub interpolate_env { my $env=shift; $env=~s/\[\s*\$\s*(.*)\s*\]\s*/$1/; return $ENV{$env}; } #--- sub interpolate_file { my $cmd=shift; my $allow_nonexistent=shift; $cmd=~s/\[\s*\!\s*(.*)\s*\]\s*/$1/; my $filename=(split(' ', $cmd) )[0]; quit("Can't see $filename: $!") unless -e $allow_nonexistent || $filename; my $output; if ( -d $filename ) { $output=oldest_dir($filename); } elsif ( -x $filename ) { $output=`$cmd`; chomp $output; my $return_code=($?==0 ? 1 : 0); $output=$return_code if !$output && $allow_nonexistent; } else { $output=read_file($filename, 1); chomp $output; } return $output; } #--- sub oldest_dir { my $dir=shift; $dir=~s[/$][]; my @ls; opendir (DIR, $dir) || quit("Couldn't open $dir: $!"); foreach (readdir DIR) { next if /^\./; my $file="$dir/$_"; my $age=-M $file || quit("Couldn't stat $file: $!"); push (@ls, "$age $file"); } return (split ' ', (reverse sort {$a <=> $b} @ls)[0])[1]; } #--- sub setup_interp { my $epoch=time(); my ($sec, $min, $hour, $mday, $mon, $year, $dow, $doy)=localtime(); $mon+=1; foreach ($sec, $min, $hour, $mday, $mon) { $_=sprintf('%02.0f', $_); } $year+=1900; my ($shortyear)=($year=~/^..(..)$/); $doy=sprintf('%03.0f', $doy); my $yest_dow=$dow-1; $yest_dow=7 if $yest_dow<0; my %day=(1 => ['Mon', 'Monday'], 2 => ['Tue', 'Tuesday'], 3 => ['Wed', 'Wednesday'], 4 => ['Thu', 'Thursday'], 5 => ['Fri', 'Friday'], 6 => ['Sat', 'Saturday'], 7 => ['Sun', 'Sunday'], 0 => ['Sun', 'Sunday'] ); my %month=('01' => ['Jan', 'January'], '02' => ['Feb', 'February'], '03' => ['Mar', 'March'], '04' => ['Apr', 'April'], '05' => ['May', 'May'], '06' => ['Jun', 'June'], '07' => ['Jul', 'July'], '08' => ['Aug', 'August'], '09' => ['Sep', 'September'], '10' => ['Oct', 'October'], '11' => ['Nov', 'November'], '12' => ['Dec', 'December'] ); %Interp=(%Interp, '[yyyy]' => $year, '[yy]' => $shortyear, '[mm]' => $mon, '[dd]' => $mday, '[epoch]' => $epoch, '[month]' => $month{$mon}->[0], '[longmonth]' => $month{$mon}->[1], '[yyyy-mm-dd]' => "$year-$mon-$mday", '[isodate]' => "$year-$mon-$mday", '[yy-mm-dd]' => "$shortyear-$mon-$mday", '[dd-mm-yyyy]' => "$mday-$mon-$year", '[dd-mm-yy]' => "$mday-$mon-$shortyear", '[mm-dd-yyyy]' => "$mon-$mday-$year", '[mm-dd-yy]' => "$mon-$mday-$shortyear", '[day]' => $day{$dow}->[0], '[yesterday]' => $day{$yest_dow}->[0], '[dayname]' => $day{$dow}->[0], '[yesterdayname]' => $day{$yest_dow}->[0], '[longday]' => $day{$dow}->[1], '[longyesterday]' => $day{$yest_dow}->[1], '[longdayname]' => $day{$dow}->[1], '[longyesterdayname]' => $day{$yest_dow}->[1], '[daynum]' => $dow, '[yesterdaynum]' => $yest_dow, '[time]' => "$hour.$min", '[longtime]' => "$hour.$min.$sec", ); } #--- sub read_file { my ($filename, $whole)=@_; my @file; my $data; return unless -e $filename; open (F, $filename) || quit("Can't open $filename: $!"); if ($whole) { undef local $/; $data=; } else { @file=; $data=\@file; } close F; return $data; } #--- sub invalidate_argv { my @argv=@_; my $error; $error="Please specify valid backup type" unless @argv; #my $illegal=invalidate_backup_types(\@argv); #$error="$illegal is not a valid backup type" if $illegal; return $error; } #--- sub no_conf_file { my @argv=@_; my $error; foreach (@argv) { my $filename="$CONFPATH/$_.conf"; open (F, $filename) || ($error="$CONFPATH/$_.conf file: $!"); close F; } return $error; } #--- sub invalidate_backup_types { my $ra_argv=shift; my ($valid, $illegal); foreach my $user_input (@$ra_argv) { undef $valid; foreach my $valid_type ('local', 'remote') { $valid++ if $user_input eq $valid_type; } $illegal=$user_input if $valid < @$ra_argv-1 || !$valid; } return $illegal; } #--- sub quit { my $text=shift || return; my $stamp=stamp(); if ($G_finally) { my $quote_esc_text=$text; $quote_esc_text=~s/\'/\\\'/g; my $final_command="$G_finally 1 '$quote_esc_text'"; my $return=system($final_command); $return=0 if !$return; out("'Finally' script ran with exit status $return: $final_command"); } out(" !!! EXITING: $text"); exit; } #--- sub out { return unless $O; my ($string, $fulltime)=@_; my $p; $p="\n" if ($string=~s/^\n//); my $time=stamp($fulltime); if ($OUT_FILE) { print OUT "${p}$time $string\n"; } else { print "${p}$time $string\n"; } } #--- sub stamp { my $fulltime=shift; my $time=localtime(time); $time=~s/^.*?(\d\d:\d\d:\d\d).*$/$1/ unless $fulltime; return $time; } #--- sub download_updates { my @sites=@_; my $cwd=cwd(); install_wget() unless wget_installed(); get_download_updates(@sites); system("/bin/rm $cwd/rsync_backup.*.txt &> /dev/null"); } #--- sub get_download_updates { my @sites=@_; my @files; my $cwd=cwd(); my $this_script_file="$cwd/rsync_backup.pl"; for (my $i=0; $i<@sites; $i++) { my $url=$sites[$i]; my $download_from="$url/rsync_backup.txt"; my $download_to="$cwd/rsync_backup.$i.txt"; http_get($download_from, $download_to); return unless diff($this_script_file, $download_to); for my $file(@files) {return if diff($file, $download_to)}; push(@files, $download_to); } my $live_serial=serial($this_script_file); my $download_serial=serial($files[0]); return if $live_serial>$download_serial && $live_serial<9999; ## IF IT'S REACHED HERE, A LEGITIMATE UPDATE HAS ARRIVED system("/bin/mv $files[0] $cwd/rsync_backup.pl &> /dev/null "); system("/bin/cp $files[0] $cwd/rsync_backup &> /dev/null "); system("chmod 755 $cwd/rsync_backup.pl &> /dev/null "); system("chmod 755 $cwd/rsync_backup &> /dev/null "); } #--- sub serial { my $filename=shift; my $serial; return unless -e $filename; open (F, $filename); for my $i (0..10) { my $line=; if ($line=~/^\s*\#\s*serial\s*:?\s*(\d+)/i) { $serial=$1; last; } } close F; return $serial; } #--- sub diff { my ($file1, $file2)=@_; my $different=system("/usr/bin/diff $file1 $file2 &> /dev/null"); return $different; } #--- sub http_get { my ($from, $to)=@_; my $wget='/usr/bin/wget'; system("$wget --timeout 4 -t 2 -O $to $from &> /dev/null"); } #--- sub wget_installed { return 1 if -e '/usr/bin/wget'; } #--- sub install_wget { my $apt='/usr/bin/apt-get'; system("$apt update &> /dev/null"); system("$apt install -y wget &> /dev/null"); } #--- sub cwd { (my $path)=($0=~m[(.+)/[^/]+$]); return $path; } #--- sub set_confpath { my @paths=@_; foreach (@paths) { return $_ if -d $_; } } #--- sub check_file { my $file=shift; return 1 if (-e $file && !-x $file) || interpolate_file($file, 1); } #--- sub halt_on_file { my ($file, $mail)=@_; if ( check_file($file) ) { error_out(mail=>$mail, error=>"Halted on $file condition"); } } #--- sub proceed_on_file { my ($file, $mail)=@_; unless ( check_file($file) ) { error_out(mail=>$mail, error=>"Could not proceed: no $file condition"); } }