#!/usr/bin/perl -w #---------------------------------------------------------------------- # copyright (C) 2006-2007 Jean-Paul Leclere # copyright (C) 2007 Charlie Brady # # 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # #---------------------------------------------------------------------- use strict; use Errno; use esmith::util; use esmith::templates; use File::Copy; use File::Path qw(make_path remove_tree); use File::Find; use POSIX qw(:sys_wait_h strftime); use File::Glob qw(bsd_glob); use esmith::ConfigDB; use esmith::Backup; use esmith::BlockDevices; sub ldie; sub start_dar_killer; sub run_backup; my $job = shift || 'DailyBackup'; my $confdb = esmith::ConfigDB->open; my $backupwk = $confdb->get('backupwk') or die "No backupwk db entry found\n"; my $bkname = strftime '%Y%m%d%H%M%S', localtime; my $dow = strftime '%w', localtime; my $ref = ""; my $id = $backupwk->prop('Id') || $confdb->get('SystemName')->value . "." . $confdb->get('DomainName')->value; my $internalinterface = $confdb->get('InternalInterface') or die "No internalinterface db entry found\n"; my $ether = $internalinterface->prop('Name'); my $smbhost = $backupwk->prop('SmbHost'); my $smbshare = $backupwk->prop('SmbShare'); my $smbv1 = $backupwk->prop('SmbV1') || 'disabled'; my $smbhostmac = $backupwk->prop('SmbHostMAC'); my $smbhostdelay = $backupwk->prop('SmbHostDelay') || 300; my $login = $backupwk->prop('Login'); my $password = $backupwk->prop('Password'); my $setsmax = $backupwk->prop('SetsMax') || 1; my $daysinset = $backupwk->prop('DaysInSet') || 1; my $setnum = $backupwk->prop('SetNum'); $setnum = $setsmax unless defined $setnum; my $incnum = $backupwk->prop('IncNum'); $incnum = ($daysinset-1) unless defined $incnum; my $timeout = (($backupwk->prop('Timeout') * 3600) - 30) || '88500'; my $inconly = $backupwk->prop('IncOnlyTimeout') || 'no'; my $VFSType = $backupwk->prop('VFSType') || 'cifs'; my $fullday = $backupwk->prop('FullDay'); $fullday = 7 unless defined $fullday; my $mail = $backupwk->prop('MailNotify') || 'yes'; my $frommail = $backupwk->prop('FromMail') || 'admin-backup'; my $tomail = $backupwk->prop('ToMail') || 'admin'; my $mntdir = $backupwk->prop('Mount') || '/mnt/smb'; $mntdir = "\/$smbshare" if ($VFSType eq 'usb'); # ToDo change to $backupwk->prop('Mount') my $deleteearly = $backupwk->prop('DeleteEarly') || 'false'; my @backup_excludes = esmith::Backup->excludes; @backup_excludes = map "\t/$_\n", sort @backup_excludes; my $report = "From: $frommail\n"; $report .= "To: $tomail\n"; $report .= "Subject: Daily Backup Report: $id\n\n"; $report .= "================================== \n"; $report .= "DAILY BACKUP TO WORKSTATION REPORT \n"; $report .= "================================== \n"; $report .= "Backup of $id started at " .localtime() . "\n"; $report .= "================================== \n"; if (@backup_excludes) { $report .= "Some parts are excluded of your backup: \n"; $report .= "@backup_excludes"; $report .= "================================== \n"; } # ping the SMB Host to see if it is awake $report .= wol ($ether,$smbhost,$smbhostmac,$smbhostdelay); # mount backup bmount($mntdir,$smbhost,$smbshare,$VFSType,$smbv1); # rotating backup indicators $incnum++; $incnum = 0 if ($dow == $fullday && $incnum > $daysinset-7) || ($fullday == 7 && $incnum >= $daysinset); if ($incnum == 0) { $setnum %= $setsmax; $setnum++; } # if no set directory, make it my $setname = "set$setnum"; my $setdirname = "$mntdir/$id/$setname"; createTree ($setdirname); $report .= "Destination //$smbhost/$smbshare/$id/$setname\n"; if ( $incnum == 0 ) { $bkname = "full-" . $bkname; } else { # if $incnum <> 0 backup should be incremental # we find correct reference backup for incremental my $file; opendir(DIR, $setdirname) or ldie("Can't open dir $setdirname $!"); while (defined($file = readdir(DIR))) { next if $file =~ /^\.\.?$/; if ($file =~ /dar$/) { $ref = $file; } } closedir (DIR); # if no reference do full backup if ($ref eq "") { $incnum = 0; $report .= "No existing reference backup, will make full backup \n"; $bkname = "full-" . $bkname; } else { # removing .dar extension $ref =~ s/\..*\.dar$//; $ref = "--ref|" . $setdirname . "/" . $ref; # | will be used to split this string in run_backup() $bkname = "inc-" . sprintf("%03d", $incnum) . "-". $bkname; } } unless ( ( $incnum != 0 ) || ( $fullday == 7 ) || ( $dow == $fullday ) ) { my $delay = ($fullday - $dow) % 7; ldie("Not a permitted day for full backup. Aborting...\nNext full backup in $delay days.\n"); } $report .= "Basename $bkname\n"; # calculate real timeout if we timeout incrementals only. # timeout of 88500 is a security for aborting backup within 24h if ( ($ref eq "") && ($inconly eq "yes")) { $timeout = 88500; } $report .= "Starting the backup with a timeout of ". int ($timeout/(60*60)). " hours\n"; # Expand backup configuration file template processTemplate ({TEMPLATE_PATH => "/etc/dar/$job.dcf"}); # If this is a new set and delete before backup is in use # then empty target directory of any .dar files if (($deleteearly eq 'true') && (($incnum == 0))) { foreach my $file (glob("$setdirname/*.dar")) { unlink $file or ldie("Error deleting old backup files in $setdirname : $!") } } my $destination = ($deleteearly ne 'true') ? "$mntdir/$id/$bkname" : "$setdirname/$bkname"; # Perform the actual backup my $rc = run_backup($destination); if ($rc != 0 && $rc != 11) { ldie("Error while running dar: $rc"); } if ($deleteearly ne 'true') # Not DeleteEarly so move backup to $setdirname { if ($incnum == 0) # If this is a new set then empty target directory of any .dar files { foreach my $file (glob("$setdirname/*.dar")) { unlink $file or ldie("Error deleting old backup files in $setdirname : $!") } } foreach (bsd_glob("$mntdir/$id/*.dar")) # Move the backup files to the set directory { ldie("Error while moving backup file $_ to $setdirname : $!") unless move($_, $setdirname); } } # update dar_manager catalog updateDarCatalog ("$mntdir/$id"); # Check free disk space my $df = qx(/bin/df -Ph \"$mntdir\"); if ($df =~ /(\S+)\s+(\S+)\s+(\S+)\s+(\d*%)/) { $report .= "Destination disk usage $2, $4 full, $3 available\n"; } else { $report .= "Destination disk space not available\n"; } # unmount shared folder system("/bin/umount", "-f", "$mntdir") unless ($VFSType eq 'mnt'); # time now to update backup configuration $backupwk->set_prop('SetNum', $setnum); $backupwk->set_prop('IncNum', $incnum); $report .= "Backup successfully terminated at ".localtime()."\n"; # Send the Workstation Backup report if ($mail eq 'yes') {sendReport ($report);} exit (0); sub ldie { my $errmsg = shift; $report =~ s/Report:/Failed:/; $report .= "*** No backup allowed or error during backup ***\n"; $report .= $errmsg; if (($mail eq 'yes') || ($mail eq 'error')) { sendReport ($report,$errmsg); } if (($VFSType ne 'mnt') && (!checkMount ($mntdir))) { system("/bin/umount", "-f", "$mntdir"); } die($errmsg); } sub start_dar_killer { my ($darpid, $gracetime) = @_; my $tick = $gracetime/10; my $killer = fork; return $killer if $killer; POSIX::setsid; chdir '/'; #fork && exit; # wait for timeout or backup termination while ($tick > 0) { sleep 10; $tick--; exit unless (kill(0, $darpid)); } if (kill(0, $darpid)) { while (kill('QUIT', $darpid) != 1) { warn "Failed to stop $darpid dar process\n"; } } warn "Partial backup stored on backup workstation.\n", "Session cleanly closed by timeout after $timeout seconds.\n", "Not an error, backup process will continue next night.\n"; exit; } sub run_backup { my $dest = shift; my $data = undef; my $pid = undef; my $killerpid = undef; eval { ($pid = open INPUT, "-|", "/usr/bin/dar", "-Q", "-asecu", "-az", "--create", "$dest", split(/\|/,$ref), "-B", "/etc/dar/$job.dcf") or ldie("cannot start : $!" ); if ($pid) { $killerpid = start_dar_killer($pid, $timeout); } $data = do { local($/); }; }; $report .= $data; if ($killerpid && kill(0, $killerpid)) { while (kill('TERM', $killerpid) != 1) { warn "Failed to kill $killerpid killer process\n"; } waitpid($killerpid, 0); } waitpid($pid, 0); my $code = WEXITSTATUS($?); close(INPUT); return $code; } # Copied from /etc/e-smith/web/functions/backup # TODO: Move to a shared module sub checkMount { # check if $mountdir is mounted my $mountdir = shift; $|=1; # Auto-flush my @res = qx( findmnt $mountdir ); return ( !@res ); } # Copied from /etc/e-smith/web/functions/backup # TODO: Move to a shared module sub dmount { # mount dar unit according to dar-workstation configuration # return nothing if mount successfull my ($host,$share,$mountdir,$login,$password,$VFSType,$smbv1) = @_; if ($VFSType eq 'cifs') { my $opt= ($smbv1 eq "enabled")? ",vers=1.0": ""; return ( qx(/bin/mount -t cifs "//$host/$share" $mountdir -o credentials=/etc/dar/CIFScredentials,nounix$opt 2>&1) ); } elsif ($VFSType eq 'nfs') { return ( qx(/bin/mount -t nfs -o nolock "$host:/$share" $mountdir 2>&1) ); } elsif ($VFSType eq 'usb') { my $device = ""; my $vollbl = ""; my $devices = esmith::BlockDevices->new ('allowmount' => 'disabled'); my ($valid, $invalid) = $devices->checkBackupDrives(0); if ( ${$valid}[0] ) { foreach ( @{$valid} ) { $vollbl = $devices->label($_); if ( $share eq "media/$vollbl" ) { $device = "/dev/$_"; } } } $devices->destroy; return ( qx (mount $device /$share 2>&1) ); } else { return ("Error while mounting $host/$share : $VFSType not supported.\n"); } } sub removeTree { my $tree = shift; if (-d "$tree") { eval {remove_tree("$tree")}; ldie("Error while deleting $tree : $@.\n") if $@; } return; } sub createTree { my $tree = shift; if (! -d "$tree") { eval {make_path("$tree")}; ldie("Error while creating $tree : $@. Maybe insufficient rights directory.\n") if $@; } return; } sub sendReport { my $text = shift; my $error = shift || ""; open (MAIL, "|/var/qmail/bin/qmail-inject") || die "Cannot start mail program: $! $error\n"; print MAIL $text; close(MAIL); return; } sub wol { my ($ether,$host,$mac,$delay) = @_; my $output=""; if (defined($mac) && (system("ping -c1 $host > /dev/null") != 0)) { $output .= "$host might be asleep, attempting to wake\n"; system("ether-wake -i $ether $mac"); $output .= "Waiting $delay seconds...\n"; sleep $delay; } return $output; } sub updateDarCatalog { # update dar_manager catalog my $mntbkdir = shift; my $catalog = "$mntbkdir/dar-catalog"; my %backupfiles = (); # hash of backup files found on the disk. unless ( -e $catalog) # Create an empty catalog if none found { system("/usr/bin/dar_manager", "-Q", "-C", "$catalog") == 0 or ldie("Unable to create dar_manager catalog.\n"); sleep 1; # sleep added to ensure the creation of a valid catalog } # find available backups for the server my $setbackuplist = sub {if($_ =~ /([\w|-]+)-(\d+)\..*\.dar/){$backupfiles{$2} = $File::Find::dir."/$1-$2";}}; find { wanted => \&$setbackuplist, untaint => 1, follow => 1 }, $mntbkdir ; my @deletions; my $pid = open (DAR_LIST, "-|", "/usr/bin/dar_manager", "-Q", "-B", "$catalog", "-l") or ldie ("Cannot start : $!"); my $catalogIndex; # Dar refers to each backup by an index my $oldkey=0; # The key for %backupfiles is the timestamp while () { next unless ($_ =~ /(\d+).*\s[\w|-]+-(\d+)/); ($catalogIndex, $oldkey) = ($1,$2); if (defined($backupfiles{$oldkey})) # found on disk and in catalog { delete $backupfiles{$oldkey}; # so that it will not be added in the next step. } else { push @deletions, $catalogIndex; # Will be deleted from the catalog } } waitpid ($pid, 0); my $err = WEXITSTATUS($?); close (DAR_LIST); ldie ("Catalog error : $err") if $err; # Delete backups from the catalog that were missing on disk in descending order while (my $catIndex = pop @deletions) { system("/usr/bin/dar_manager", "-Q", "-ai", "-B", "$catalog", "-D", "$catIndex") == 0 or ldie("Failed to delete set $catIndex from catalog. $!\n"); sleep 1; # sleep added to ensure the creation of a valid catalog } # Add backups that were missing from the catalog in chronological order foreach my $key (sort keys %backupfiles) { next unless ($key > $oldkey); # prevent adding old backups to the catalog system("/usr/bin/dar_manager", "-Q", "-ai", "-B", "$catalog", "-A", "$backupfiles{$key}") == 0 or ldie("Failed to add set $backupfiles{$key} to catalog. $!\n"); sleep 1; # sleep added to ensure the creation of a valid catalog } return; } sub bmount { my ($mntdir,$host,$share,$VFSType,$smbv1) = @_; # verify backup directory not already mounted if (!checkMount ($mntdir)) { return if ($VFSType eq 'mnt'); ldie("Seems backup directory is already mounted. " . "It should not happen and maybe there is a zombie process " . "you must kill, or another backup in progress. \n"); } else { if ($VFSType eq 'mnt') { ldie("Seems backup directory is not mounted. " . "The backup directory must be mounted when using type 'mnt'. \n"); } } # create the directory mount point if it does not exist createTree ($mntdir); # mount the backup directory my $err = dmount($host,$share,$mntdir,'','',$VFSType,$smbv1); ldie("Error while mounting \n" . $err) if $err; # verify $mntdir is mounted if (checkMount ($mntdir)) { # The mount should have suceeded, but sometimes it needs more time, # so sleep and then check again. sleep 5; if (checkMount ($mntdir)) { ldie("Seems backup directory is not really mounted. It should not happen. \ Verify availability of your backup volume. Stopping the backup now.\n"); } } return; }