#!/usr/bin/perl -w # Backups Blow by Brandon Low# # This script is designed to backup all mysql databases on a # host as well as any specified directories and dump them to # a configured FTP site. It uses incremental tar and does not # require a temporary location to store the backup files. # # There is only one commandline option available and that # specifies the location of a configuratoin file. The default # location is /etc/backupsblow.cfg # # I got various ideas for this script from: # ReoBack http://sourceforge.net/projects/reoback/ # http://www.cyberciti.biz/tips/how-to-backup-mysql-databases-web-server-files-to-a-ftp-server-automatically.html # # Copyright 2006-2007 Brandon Low # Released under the terms of the GPL v2 # # http://lostlogicx.com/backupsblow/ use strict; use Switch; use Log::Log4perl; use BackupsBlow::Config qw(:all); # This initializes logging, so load Config, then readConfig # then load other modules and get our logger readConfig($0,$ARGV[0]); use BackupsBlow::Programs qw(:all); use BackupsBlow::Util qw(:all); my $logger=Log::Log4perl->get_logger(); sub buildBackupSets($$) { my ($backupPath,$suffix)=@_; $logger->debug("Loading list of existing backups"); my @ls=listBackups($backupPath); # Build a hash of old backups my %oldBackups; foreach my $file(@ls) { if ($file =~ m/^(full|incremental)-[0-9]{14}\.tar\.$suffix$/i) { my @parts=split("[-.]",$file); $oldBackups{$parts[1]}=$file; } } # Build arrays of old full+incremental sets my @oldSets; my $oldSet=[]; foreach my $key(reverse sort keys(%oldBackups)) { my $file=$oldBackups{$key}; unshift @$oldSet,$file; if ($file =~ m/^full/) { unshift @oldSets,$oldSet; $oldSet=[]; } } # In case there were incrementals before the earliest full unshift @oldSets,$oldSet if (@$oldSet > 0); return @oldSets; } sub prepDataDir($) { my $dataDir=needConfig("DATA"); unless ( -d $dataDir ) { $logger->info("Creating data directory for first run"); `mkdir -p $dataDir`; unless ( $? == 0) { die("Cannot create data directory, do you have permission?"); } } my $incFile="$dataDir/backupsblow.inc.dat"; if ($_[0] eq "full") { # Remove the incremental file if this is a new full # backup in order to force tar to backup all files. unlink $incFile; } my $errFile="$dataDir/tar.stderr"; unlink $errFile; return ($incFile, $errFile); } # Perform the SQL Backup sub performSqlBackup($$) { my ($compression,$suffix)=@_; $logger->info("Performing SQL backup"); my $sqlHost=config("SQLHOST","localhost"); my $sqlDump=needProgram("mysqldump"); my $sqlFile=needConfig("SQLFILE"); $sqlFile.=".$suffix" unless ($sqlFile =~ m/\.$suffix$/); # Dump all databases to stdout my @sqlCommand; push @sqlCommand,$sqlDump; push @sqlCommand,"-u".needConfig("SQLUSER"); push @sqlCommand,"-p".needConfig("SQLPASS"); push @sqlCommand,"-h$sqlHost"; push @sqlCommand,"--all-databases"; push @sqlCommand,"--all"; push @sqlCommand,"--opt"; push @sqlCommand,"--allow-keywords"; push @sqlCommand,"--flush-logs"; push @sqlCommand,"--hex-blob"; push @sqlCommand,"--master-data"; push @sqlCommand,"--max_allowed_packet=16M"; push @sqlCommand,"--quote-names"; my $sqlCommand=join(" ", @sqlCommand); # Pipe the SQL to compression program @sqlCommand=(); push @sqlCommand,$sqlCommand; push @sqlCommand,$compression; $sqlCommand=join("|", @sqlCommand); # Write the compressed output @sqlCommand=(); push @sqlCommand,$sqlCommand; push @sqlCommand,$sqlFile; $sqlCommand=join(">",@sqlCommand); system($sqlCommand); unless ($? == 0) { $logger->error("Failed to backup MySQL databases: $sqlCommand: $?"); } $logger->info("SQL backup complete"); } # Main program function -- put in a function to improve scoping sub main() { $logger->info("Starting backup"); my $transport=config("TRANSPORT"); switch ($transport) { case /local/i { require BackupsBlow::Transport::Local; BackupsBlow::Transport::Local->import(qw(:all)); } case /s3/i { require BackupsBlow::Transport::S3; BackupsBlow::Transport::S3->import(qw(:all)); } case /ftp/i { require BackupsBlow::Transport::Ftp; BackupsBlow::Transport::Ftp->import(qw(:all)); } case /ssh/i { require BackupsBlow::Transport::Ssh; BackupsBlow::Transport::Ssh->import(qw(:all)); } else { die("Invalid transport $transport"); } } $logger->info("Using $transport transport"); chomp(my $day=`date +"%a"`); chomp(my $now=`date +"%Y%m%d%H%M%S"`); my $fullBackupDay=config("FULL_BACKUP_DAY","Sun"); my $backupPath=needConfig("BACKUP_DIR"); $backupPath=fixBackupPath($backupPath); my $compression=config("COMPRESSION_PROGRAM","gzip"); my $suffix=config("COMPRESSION_SUFFIX","gz"); performMounts(); if (configEnabled("SQL")) { performSqlBackup($compression,$suffix); } # Get a list of existing backup sets my @oldSets=buildBackupSets($backupPath,$suffix); my $type="full"; if (@oldSets) { # Get the first element of the last backup set my @lastSet=@{$oldSets[$#oldSets]}; my $full=$lastSet[0]; my $latest=$lastSet[$#lastSet]; $logger->debug("Most recent backup: $latest"); if ($full =~ m/^full-/) { # No need to print this if it's the same as above unless ($#lastSet == 0) { $logger->debug("Most recent full backup:", $full); } my @parts=split("[.-]",$full); my $lastFullStamp=$parts[1]; # If it's not full backup day or < 23:59 after the last full backup if ($day ne $fullBackupDay || $now-$lastFullStamp < 235900) { $type="incremental"; } } else { $logger->warn("No previous full backup found"); } } else { $logger->info("No existing backups found"); } $logger->info("Performing $type backup"); my $backupFile="$type-$now.tar.$suffix"; my ($incFile, $errFile)=prepDataDir($type); performBackup($backupPath, $backupFile, $incFile, $errFile); $logger->info("Backup stored successfully"); # After creating a new full backup, check for and remove stale backups if (configEnabled("RM_OLD") && $type eq "full") { $logger->info("Removing old backups"); my $backSets=configWhole("BACKUP_SETS",1); # Generate a command to remove the 'old' backup files. my @rmFiles; # Keep backSets sets around while ($#oldSets >= $backSets) { my $oldSet=shift @oldSets; foreach my $file(@$oldSet) { push @rmFiles,$file; } removeBackups($backupPath,\@rmFiles); @rmFiles=(); } } performUnmounts(); $logger->info("Backup process completed successfully, exiting"); } main();
"You give me Governor Ventura, myself and eight more of my fellow Navy SEALS -- and we could paralyze the entire country of the United States of America" --Richard Marcinko