Backups Blow - backupsblow.pl
#!/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
Google
 
© 2002-2008 Brandon Low 300 hits since April 21, 2007