Backup-EZ
view release on metacpan or search on metacpan
lib/Backup/EZ.pm view on Meta::CPAN
package Backup::EZ;
$Backup::EZ::VERSION = '0.46';
use strict;
use warnings;
use warnings FATAL => 'all';
use Config::General;
use Carp;
use Devel::Confess 'color';
use Time::localtime;
use Unix::Syslog qw(:macros :subs);
use Data::UUID;
use Sys::Hostname;
use File::Slurp qw(slurp read_dir);
use File::Spec;
use Backup::EZ::Dir;
use Data::Printer alias => 'pdump';
use Data::Dumper;
#
# CONSTANTS
#
use constant EXCLUDE_FILE => '/etc/ezbackup/ezbackup_exclude.rsync';
use constant CONF => '/etc/ezbackup/ezbackup.conf';
use constant COPIES => 30;
use constant DEST_HOSTNAME => 'localhost';
use constant DEST_APPEND_MACH_ID => 0;
use constant USE_SUDO => 0;
use constant IGNORE_VANISHED => 0;
use constant DEFAULT_ARCHIVE_OPTS => '-az';
use constant ARCHIVE_NO_RECURSE_OPTS => '-dlptgoDz';
=head1 NAME
Backup::EZ - Simple backups based on rsync
=head1 VERSION
version 0.46
=cut
=head1 SYNOPSIS
use Backup::EZ;
my $ez = Backup::EZ->new;
$ez->backup;
=head1 DESCRIPTION
Backup::EZ is backup software that is designed to be as easy to use
as possible, yet provide a robust solution
If you only want to run backups, see the included command line utility
"ezbackup". See the README for configuration instructions.
=head1 SUBROUTINES/METHODS
=head2 new
optional args:
conf => $config_file
dryrun => $bool,
exclude_file => $rsync_excl_file
=cut
sub new {
my $class = shift;
my %a = @_;
my $self = {};
# uncoverable branch true
if ( $ENV{VERBOSE} ) {
setlogmask( LOG_UPTO(LOG_DEBUG) );
$self->{syslog_option} = LOG_PID | LOG_PERROR;
}
else {
setlogmask( LOG_UPTO(LOG_INFO) );
$self->{syslog_option} = LOG_PID;
}
_read_conf( $self, @_ );
if ( $a{dryrun} ) {
$self->{dryrun} = 1;
}
if ( !defined $a{exclude_file} ) {
$self->{exclude_file} = EXCLUDE_FILE;
}
else {
$self->{exclude_file} = $a{exclude_file};
}
bless $self, $class;
return $self;
}
sub _debug {
my $self = shift;
my $msg = shift;
my $line = (caller)[2];
openlog "ezbackup", $self->{syslog_option}, LOG_SYSLOG;
syslog LOG_DEBUG, "($line) $msg";
closelog;
}
#sub _error {
# my $self = shift;
# my $msg = shift;
#
# openlog "ezbackup", $self->{syslog_option}, LOG_LOCAL7;
# syslog LOG_ERR, $msg;
# closelog;
#}
sub _info {
my $self = shift;
my $msg = shift;
openlog "ezbackup", $self->{syslog_option}, LOG_SYSLOG;
syslog LOG_INFO, $msg;
closelog;
}
sub _read_conf {
my $self = shift;
my %a = @_;
# uncoverable branch false
my $conf = $a{conf} ? $a{conf} : CONF;
my $config = Config::General->new(
-ConfigFile => $conf,
-ForceArray => 1,
-LowerCaseNames => 1,
-AutoTrue => 1,
);
my %conf = $config->getall;
_debug( $self, Dumper \%conf );
foreach my $key ( keys %conf ) {
if ( !defined $conf{backup_host} ) {
$conf{backup_host} = DEST_HOSTNAME;
}
if ( !defined $conf{copies} ) {
$conf{copies} = COPIES;
}
if ( !defined $conf{append_machine_id} ) {
$conf{append_machine_id} = DEST_APPEND_MACH_ID;
}
if ( !defined $conf{use_sudo} ) {
$conf{use_sudo} = USE_SUDO;
}
if ( !defined $conf{ignore_vanished} ) {
$conf{ignore_vanished} = IGNORE_VANISHED;
}
}
if ( ref( $conf{dir} ) ne 'ARRAY' ) {
$conf{dir} = [ $conf{dir} ];
}
$self->{conf} = \%conf;
}
sub _get_dirs {
my $self = shift;
my @dirs;
foreach my $dir ( @{ $self->{conf}->{dir} } ) {
push( @dirs, Backup::EZ::Dir->new($dir) );
}
$self->_debug( Dumper \@dirs );
return @dirs;
}
=head2 get_conf_dirs
Returns a list Backup::EZ::Dir objects as read from the conf file.
=cut
sub get_conf_dirs {
my $self = shift;
return $self->_get_dirs;
}
sub _ssh {
my $self = shift;
my $cmd = shift;
my $dryrun = shift;
my $sshcmd;
my $login = $self->_get_dest_login;
# uncoverable branch false
if ( $self->_is_unit_test ) {
# unit testing
$sshcmd = "$cmd";
}
else {
#
# this breaks stuff. commenting out for now.
#
# if ($cmd !~ /^ *sudo /) {
# $cmd = sprintf( "%s $cmd", $self->{conf}->{use_sudo} ? 'sudo' : '' );
# }
my $ssh_opts = [];
if ( $self->{conf}->{ssh_opts} ) {
push @$ssh_opts, $self->{conf}->{ssh_opts};
}
$sshcmd = sprintf( 'ssh %s %s %s', join( ' ', @{$ssh_opts}), $login, $cmd );
}
$self->_debug($sshcmd);
return undef if $dryrun;
my @out = `$sshcmd`;
# uncoverable branch true
confess if $?;
return @out;
}
sub _get_dest_username {
my $self = shift;
if ( $self->{conf}->{backup_user} ) {
return $self->{conf}->{backup_user};
}
if ( $ENV{USER} ) {
return $ENV{USER};
}
my $whoami = `whoami`;
chomp $whoami;
return $whoami;
}
sub _get_dest_hostname {
my $self = shift;
return $self->{conf}->{backup_host};
}
sub _get_dest_tmp_dir {
my $self = shift;
return sprintf( "%s/%s", $self->get_dest_dir, ".tmp" );
}
sub _get_dest_backup_dir {
my $self = shift;
return sprintf( "%s/%s", $self->get_dest_dir, $self->{datestamp} );
}
sub _is_unit_test {
my $self = shift;
# uncoverable branch false
if ( $0 =~ /\.t$/ ) {
return 1;
}
return 0;
}
sub _get_dest_login {
my $self = shift;
my $username = $self->_get_dest_username;
my $hostname = $self->_get_dest_hostname;
return sprintf( '%s@%s', $username, $hostname );
}
# Unused?
# Commenting out to make it clear these are unused
# Could consider removing
#sub _rsync_no_recursion {
# my $self = shift;
# my $dir = shift;
# my @extra_options = @_;
#
# my $rsync_opts = '-dlptgoD';
# my $cmd;
# my $login;
#
# if ( $self->{dryrun} ) {
# push( @extra_options, '--dry-run' );
# }
#
# $self->_mk_dest_dir( sprintf( "%s%s", $self->_get_dest_tmp_dir, $dir ) );
# $login = $self->_get_dest_login;
#
# # uncoverable branch false
# if ( $self->_is_unit_test ) {
# $cmd = sprintf(
# "rsync %s $rsync_opts %s/ %s%s",
# join( ' ', @extra_options ), $dir,
# $self->_get_dest_tmp_dir, $dir
# );
# }
# else {
# $cmd = sprintf(
# "rsync %s $rsync_opts -e ssh %s/ %s:%s%s",
# join( ' ', @extra_options ),
# $dir, $login, $self->_get_dest_tmp_dir, $dir
# );
# }
#
lib/Backup/EZ.pm view on Meta::CPAN
#
$self->_debug($cmd);
system($cmd);
# uncoverable branch true
# uncoverable branch true
if ($?) {
my $exit = $? >> 8;
if ($exit == 24 && $self->{conf}->{ignore_vanished}) {
$self->_debug("ignoring vanished files");
}
else {
confess;
}
}
}
# Unused?
# Commenting out to make it clear these are unused
# Could consider removing
#sub _rsync {
# my $self = shift;
# my $dir = shift;
# my @extra_options = @_;
#
# my $cmd;
# my $login;
#
# if ( $self->{dryrun} ) {
# push( @extra_options, '--dry-run' );
# }
#
# $self->_mk_dest_dir( sprintf( "%s%s", $self->_get_dest_tmp_dir, $dir ) );
# $login = $self->_get_dest_login;
#
# # uncoverable branch false
# if ( $self->_is_unit_test ) {
# $cmd = sprintf(
# "rsync %s -a %s/ %s%s",
# join( ' ', @extra_options ), $dir,
# $self->_get_dest_tmp_dir, $dir
# );
# }
# else {
# $cmd = sprintf(
# "rsync %s -aze ssh %s/ %s:%s%s",
# join( ' ', @extra_options ),
# $dir, $login, $self->_get_dest_tmp_dir, $dir
# );
# }
#
# $cmd .= " --exclude-from " . $self->{exclude_file};
#
# $self->_debug($cmd);
# system($cmd);
#
# # uncoverable branch true
# confess if $?;
#}
sub _full_backup_chunked {
my $self = shift;
my $dir = shift;
$self->_rsync2(
dir => $dir->dirname,
archive_opts => ARCHIVE_NO_RECURSE_OPTS,
extra_opts => $dir->excludes(),
);
my @entries = read_dir( $dir->dirname, prefix => 1 );
foreach my $entry (@entries) {
if ( -d $entry ) {
$self->_rsync2(
dir => $entry,
archive_opts => DEFAULT_ARCHIVE_OPTS,
extra_opts => $dir->excludes(),
);
}
}
}
sub _full_backup {
my $self = shift;
my $dir = shift;
if ( $dir->chunked ) {
$self->_full_backup_chunked($dir);
}
else {
$self->_rsync2(
dir => $dir->dirname,
archive_opts => DEFAULT_ARCHIVE_OPTS,
extra_opts => $dir->excludes(),
);
}
}
sub _inc_backup_chunked {
my $self = shift;
my $dir = shift;
my $last_backup_dir = shift;
my $link_dest = shift;
$self->_rsync2(
dir => $dir->dirname,
archive_opts => ARCHIVE_NO_RECURSE_OPTS,
extra_opts => $dir->excludes(),
link_dest => $link_dest,
);
my @entries = read_dir( $dir->dirname, prefix => 0 );
foreach my $entry (@entries) {
my $abs_entry = sprintf( '%s/%s', $dir->dirname, $entry );
if ( -d $abs_entry ) {
$self->_rsync2(
dir => $abs_entry,
archive_opts => DEFAULT_ARCHIVE_OPTS,
extra_opts => $dir->excludes(),
link_dest => sprintf( '%s/%s', $link_dest, $entry ),
);
}
}
}
sub _inc_backup {
my $self = shift;
my $dir = shift;
my $last_backup_dir = shift;
my $link_dest = sprintf(
"%s/%s/%s",
$self->get_dest_dir, #
$last_backup_dir, #
$dir->dirname, #
);
if ( $dir->chunked ) {
$self->_inc_backup_chunked( $dir, $last_backup_dir, $link_dest );
}
else {
$self->_rsync2(
dir => $dir->dirname,
archive_opts => DEFAULT_ARCHIVE_OPTS,
extra_opts => $dir->excludes(),
link_dest => $link_dest,
);
}
}
sub _mk_dest_dir {
my $self = shift;
my $dir = shift;
my $dryrun = shift;
my $cmd = sprintf( "mkdir -p %s", $dir );
$self->_ssh( $cmd, $dryrun );
}
sub _set_datestamp {
my $self = shift;
my $t = localtime;
$self->{datestamp} = sprintf(
"%04d-%02d-%02d_%02d:%02d:%02d",
$t->year + 1900,
$t->mon + 1,
$t->mday, $t->hour, $t->min, $t->sec
);
}
=head2 dump_conf
Does what it says.
=cut
sub dump_conf {
my $self = shift;
pdump $self->{conf};
}
=head2 backup
Invokes the backup process. Takes no args.
=cut
sub backup {
my $self = shift;
$self->_mk_dest_dir( $self->get_dest_dir );
my @backups = $self->get_list_of_backups;
$self->_set_datestamp;
$self->_mk_dest_dir( $self->_get_dest_tmp_dir, $self->{dryrun} );
foreach my $dir ( $self->_get_dirs ) {
my $dirname = $dir->dirname();
if ( -d $dirname ) {
$self->_info("backing up $dirname");
if ( !@backups ) {
# full
$self->_full_backup($dir);
}
else {
# incremental
$self->_inc_backup( $dir, $backups[$#backups] );
}
}
else {
$self->_info("skipping $dirname because it does not exist");
}
}
$self->_ssh(
sprintf( "mv %s %s",
$self->_get_dest_tmp_dir, $self->_get_dest_backup_dir ),
$self->{dryrun}
);
$self->expire();
return 1;
}
=head2 expire
Expire backups. Gets a list of current backups and removes old ones that are
beyond the cutoff (see "copies" in the conf file).
=cut
sub expire {
my $self = shift;
my @list = $self->get_list_of_backups;
while ( scalar(@list) > $self->{conf}->{copies} ) {
my $subdir = shift @list;
my $del_dir = sprintf( "%s/%s", $self->get_dest_dir, $subdir );
my $cmd = sprintf( "%s rm -rf $del_dir",
$self->{conf}->{use_sudo} ? 'sudo' : '' );
$self->_ssh($cmd);
}
}
=head2 get_backup_host
Returns the backup_host name.
=cut
sub get_backup_host {
my $self = shift;
return $self->{conf}->{backup_host};
}
=head2 get_dest_dir
Returns the dest_dir.
=cut
sub get_dest_dir {
my $self = shift;
my $hostname = hostname();
$hostname =~ s/\..+$//;
if ( $self->{conf}->{append_machine_id} ) {
# uncoverable branch true
if ( !-f '/etc/machine-id' ) {
# uncoverable statement count:2
my $data_uuid = Data::UUID->new;
my $uuid = $data_uuid->create_str();
# uncoverable statement count:3
open my $fh, ">/etc/machine-id"
or confess "failed to open /etc/machine-id: $!";
print $fh "$uuid\n";
close($fh);
}
my $uuid = slurp("/etc/machine-id");
chomp $uuid;
$hostname = "$hostname-$uuid";
}
return sprintf( "%s/%s", $self->{conf}->{dest_dir}, $hostname );
}
=head2 get_list_of_backups
Returns an array of backups. They are in the format of "YYYY-MM-DD_HH:MM:SS".
=cut
sub get_list_of_backups {
my $self = shift;
my @backups;
my @list = $self->_ssh( sprintf( "ls %s", $self->get_dest_dir ) );
foreach my $e (@list) {
chomp $e;
if ( $e =~ /^\d\d\d\d-\d\d-\d\d_\d\d:\d\d:\d\d$/ ) {
push( @backups, $e );
}
}
return @backups;
}
1;
( run in 0.783 second using v1.01-cache-2.11-cpan-cdf2f3d4e48 )