view release on metacpan or search on metacpan
t/test-configs/README
t/test-configs/invalid/empty
t/test-configs/invalid/invalid-keep
t/test-configs/invalid/invalid-month-day
t/test-configs/invalid/invalid-mountpoint
t/test-configs/invalid/invalid-ssh-dest
t/test-configs/invalid/invalid-thing
t/test-configs/invalid/invalid-time
t/test-configs/invalid/invalid-timeframes
t/test-configs/invalid/invalid-weekly-day
t/test-configs/invalid/local-backup-invalid-name
t/test-configs/invalid/local-backup-invalid-setting
t/test-configs/invalid/local-backup-missing-setting
t/test-configs/invalid/local-backup-undefined-subvol
t/test-configs/invalid/missing-closing-brace
t/test-configs/invalid/missing-opening-brace
t/test-configs/invalid/missing-timeframe-setting
t/test-configs/invalid/missing-yabsm-dir
t/test-configs/invalid/redefine-local-backup
t/test-configs/invalid/redefine-snap
t/test-configs/invalid/redefine-ssh-backup
t/test-configs/invalid/redefine-subvol
t/test-configs/invalid/snap-invalid-name
t/test-configs/invalid/snap-invalid-setting
t/test-configs/invalid/snap-missing-required-setting
t/test-configs/invalid/snap-undefined-subvol
t/test-configs/invalid/ssh-backup-invalid-name
t/test-configs/invalid/ssh-backup-invalid-setting
t/test-configs/invalid/ssh-backup-missing-setting
t/test-configs/invalid/ssh-backup-undefined-subvol
t/test-configs/invalid/subvol-invalid-setting
t/test-configs/valid/comments-and-whitespace
t/test-configs/valid/local-backup-basic
t/test-configs/valid/multiple-subjects
t/test-configs/valid/snap-basic
t/test-configs/valid/ssh-backup-basic
t/test-configs/valid/timeframes-settings-order
---
abstract: 'a btrfs snapshot and backup management system'
author:
- 'Nicholas Hubbard <nicholashubbard@posteo.net>'
build_requires:
Getopt::Long: '0'
Test::Exception: '0'
Test::More: '0'
configure_requires:
ExtUtils::MakeMaker: '0'
dynamic_config: 0
generated_by: 'Dist::Zilla version 6.025, CPAN::Meta::Converter version 2.150010'
Makefile.PL view on Meta::CPAN
# This file was automatically generated by Dist::Zilla::Plugin::MakeMaker v6.025.
use strict;
use warnings;
use 5.016003;
use ExtUtils::MakeMaker;
my %WriteMakefileArgs = (
"ABSTRACT" => "a btrfs snapshot and backup management system",
"AUTHOR" => "Nicholas Hubbard <nicholashubbard\@posteo.net>",
"CONFIGURE_REQUIRES" => {
"ExtUtils::MakeMaker" => 0
},
"DISTNAME" => "App-Yabsm",
"EXE_FILES" => [
"bin/yabsm"
],
"LICENSE" => "mit",
"MIN_PERL_VERSION" => "5.016003",
__END__
=pod
=head1 Name
Yabsm - yet another btrfs snapshot manager
=head1 What is Yabsm?
Yabsm is a btrfs snapshot and backup management system that provides the
following features:
=over 4
=item *
Takes read only snapshots and performs both remote and local incremental backups.
=item *
Separates snapshots and backups into 5minute, hourly, daily, weekly, and monthly
timeframe categories.
=item *
Provides a simple query language for locating snapshots and backups.
=back
=head1 Usage
Yabsm provides 3 commands: L<config|/"Configuration Querying">,
L<find|/"Finding Snapshots">, and L<daemon|/"The Yabsm Daemon">
usage: yabsm [--help] [--version] [<COMMAND> <ARGS>]
commands:
<config|c> [--help] [check ?file] [ssh-check <SSH_BACKUP>] [ssh-key]
[yabsm-user-home] [yabsm_dir] [subvols] [snaps] [ssh_backups]
[local_backups] [backups]
<find|f> [--help] [<SNAP|SSH_BACKUP|LOCAL_BACKUP> <QUERY>]
<daemon|d> [--help] [start] [stop] [restart] [status] [init]
=head1 Dependencies
=over 4
=item *
Until then Yabsm can be installed with L<cpanminus|https://metacpan.org/pod/App::cpanminus>.
# apt install cpanminus
# cpanm App::Yabsm
=head1 The Yabsm Daemon
usage: yabsm <daemon|d> [--help] [start] [stop] [restart] [status] [init]
Snapshots and backups are performed by the Yabsm daemon. The Yabsm daemon must be
started as root so it can initialize its runtime environment, which includes
creating a locked user named I<yabsm> (and a group named I<yabsm>) that the
daemon will run as. You can initialize the daemon's runtime environment without
actually starting the daemon by running C<yabsm daemon init>.
When the daemon starts, it reads the C</etc/yabsm.conf> file that specifies its
L<configuration|/"Configuration"> to determine when to schedule the snapshots and
backups and how to perform them. If the Yabsm daemon is already running and you
make a configuration change, you must run C<yabsm daemon restart> to apply the
changes.
=head3 Initialize Daemon Runtime Environment
You can use the command C<yabsm daemon init> to initialize the daemon's runtime
environment without actually starting the daemon. Running this command creates
the I<yabsm> user and group, gives the I<yabsm> user sudo access to btrfs-progs,
creates I<yabsms> SSH keys, and creates the directories needed for performing all
the I<snaps>, I<ssh_backups>, and I<local_backups> defined in C</etc/yabsm.conf>.
=head3 Daemon Logging
The Yabsm daemon logs all of its errors to C</var/log/yabsm>. If, for example,
you have an I<ssh_backup> that is not being performed, the first thing you should
do is check the logs.
=head1 Configuration
The Yabsm daemon is configured via the C</etc/yabsm.conf> file.
You can run the command C<yabsm config check> that will check your config and
output useful error messages if there are any problems.
=head3 Configuration Grammar
First things first: you must specify a C<yabsm_dir> that Yabsm will use for
storing snapshots and as a cache for holding data needed for performing snapshots
and backups. Most commonly this directory is set to C</.snapshots/yabsm>. Yabsm
will take this directory literally so you almost certainly want the path to end
in C</yabsm>. If this directory does not exist, the Yabsm daemon will create it
automatically when it starts.
There are 4 different configuration objects: I<subvols>, I<snaps>,
I<ssh_backups>, and I<local_backups>. The general form of each configuration
object is:
type name {
key=val
...
}
All configuration objects share a namespace, so you must make sure they all have
unique names. You can define as many configuration objects as you want.
on your system. A subvol definition accepts one field named C<mountpoint> which
takes a value that is a path to a subvolume.
subvol home_subvol {
mountpoint=/home
}
=head4 Timeframes
We need to understand timeframes before we can understand I<snaps>,
I<ssh_backups>, and I<local_backups>. There are 5 timeframes: 5minute, hourly,
daily, weekly, and monthly.
I<Snaps>, I<ssh_backups>, and I<local_backups> are performed in one or more
timeframes. For example, a I<ssh_backup> may be configured to take backups in the
I<hourly> and I<weekly> categories, which means that we want to backup every hour
and once a week.
The following table describes in plain English what each timeframe means:
5minute -> Every 5 minutes.
hourly -> At the beginning of every hour.
daily -> Every day at one or more times of the day.
weekly -> Once a week on a specific weekday at a specific time.
monthly -> Once a month on a specific day at a specific time.
Each timeframe you specify adds new required settings for the configuration
object. Here is a table that shows the timeframe settings:
5minute -> 5minute_keep
hourly -> hourly_keep
daily -> daily_keep, daily_times
weekly -> weekly_keep, weekly_time, weekly_day
monthly -> monthly_keep, monthly_time, monthly_day
Any C<*_keep> setting defines how many snapshots/backups you want to keep at one
time for the configuration object. A common configuration is to keep 48 hourly
snapshots so you can go back 2 days in one-hour increments.
The C<daily_times> setting for daily snapshots takes a comma separated list of
I<hh:mm> times. Yabsm will perform the snapshot/backup every day at all the given
times.
The weekly timeframe requires a C<weekly_day> setting that takes a day of week
string such as I<monday>, I<thursday>, or I<saturday> and a I<weekly_time>
setting that takes a I<hh:mm> time. The weekly snapshot/backup will be performed
on the given day of the week at the given time.
The monthly timeframe requires a C<monthly_day> setting that takes an integer
between 1-31 and a C<monthly_time> setting that takes a I<hh:mm> time. The
monthly snapshot/backup will be performed on the given day of the month at the
given time.
=head4 Snaps
A I<snap> represents a snapshot configuration for some I<subvol>. Here is an
example of a I<snap> that snapshots I<home_subvol> twice a day.
snap home_subvol_snap {
subvol=home_subvol
timeframes=daily
daily_keep=62 # two months
daily_times=13:40,23:59
}
=head4 SSH Backups
A I<ssh_backup> represents a backup configuration that sends snapshots over a
network via SSH. See this example of a I<ssh_backup> that backs up I<home_subvol>
to C<larry@192.168.1.73:/backups/yabsm/laptop_home> every night at midnight:
ssh_backup home_subvol_larry_server {
subvol=home_subvol
ssh_dest=larry@192.168.1.73
dir=/backups/yabsm/laptop_home
timeframes=daily
daily_keep=31
daily_times=23:59
}
The difficult part of configuring a I<ssh_backup> is making sure the SSH server
is properly configured. You can test that a I<ssh_backup> is able to be performed
by running C<yabsm config ssh-check E<lt>SSH_BACKUPE<gt>>. For a I<ssh_backup> to
be able to be performed the following conditions must be satisfied:
=over 4
=item *
The host's I<yabsm> user can sign into the SSH destination (I<ssh_dest>) using
key based authentication. To achieve this you must add the I<yabsm> users SSH key
(available via C<# yabsm ssh print-key>) to the server user's
C<$HOME/.ssh/authorized_keys> file.
=item *
The remote backup directory (I<dir>) is an existing directory residing on a btrfs
filesystem that the remote user has read and write permissions to.
=item *
The SSH user has root access to btrfs-progs via sudo. To do this you can add a
file containing a string like C<larry ALL=(root) NOPASSWD: /sbin/btrfs> to
a file in C</etc/sudoers.d/>.
=back
=head4 Local Backups
A I<local_backup> represents a backup configuration that sends snapshots to a
partition mounted on the host OS. This is useful for sending snapshots to an
external hard drive plugged into your computer.
Here is an example I<local_backup> that backs up C<home_subvol> every hour, and
once a week.
local_backup home_subvol_easystore {
subvol=home_subvol
dir=/mnt/easystore/backups/yabsm/home_subvol
timeframes=hourly,weekly
hourly_keep=48
weekly_keep=56
weekly_day=sunday
weekly_time=23:59
}
The backup directory (C<dir>) must be an existing directory residing on a btrfs
filesystem that the I<yabsm> user has read permission on.
=head1 Configuration Querying
Yabsm comes with a C<config> command that allows you to check and query your
configuration.
usage: yabsm <config|c> [--help] [check ?file] [ssh-check <SSH_BACKUP>]
[ssh-key] [yabsm-user-home] [yabsm_dir] [subvols]
[snaps] [ssh_backups] [local_backups] [backups]
The C<check ?file> subcommand checks that C<?file> is a valid Yabsm configuration
file and if not prints useful error messages. If the C<?file> argument is omitted
it defaults to C</etc/yabsm.conf>.
The C<ssh-check E<lt>SSH_BACKUPE<gt>> subcommand checks that C<E<lt>SSH_BACKUPE<gt>> can be
performed and if not prints useful error messages. See the section
L<SSH Backups|/"SSH Backups"> for an explanation on the configuration required
for performing an I<ssh_backup>.
The C<ssh-key> subcommand prints the I<yabsm> user's public SSH key.
All of the other subcommands query for information derived from your
C</etc/yabsm.conf>:
subvols -> The names of all subvols.
snaps -> The names of all snaps.
ssh_backups -> The names of all ssh_backups.
local_backups -> The names of all local_backups.
backups -> The names of all ssh_backups and local_backups.
yabsm_dir -> The directory used as the yabsm_dir.
yabsm_user_home -> The 'yabsm' users home directory.
=head1 Finding Snapshots
Now that we know how to configure Yabsm to take snapshots, we are going to want
to locate those snapshots. Yabsm comes with a command C<find> that allows you to
locate snapshots and backups using a simple query language. Here is the usage
string for the C<find> command.
usage: yabsm <find|f> [--help] [<SNAP|SSH_BACKUP|LOCAL_BACKUP> <QUERY>]
Here are a few examples:
$ yabsm find home_snap back-2-mins
$ yabsm f root_ssh_backup 'after b-2-m'
$ yabsm f home_local_backup 10:45
The first argument is the name of any I<snap>, I<ssh_backup>, or I<local_backup>.
Because these configuration entities share the same namespace there is no risk of
ambiguity.
The second argument is a snapshot location query. There are 7 types of queries:
all -> Every snapshot sorted newest to oldest
newest -> The most recent snapshot/backup.
oldest -> The oldest snapshot/backup.
after TIME -> All the snapshot/backups that are newer than TIME.
before TIME -> All the snapshot/backups that are older than TIME.
between TIME1 TIME2 -> All the snapshot/backups that were taken between TIME1 and TIME2.
TIME -> The snapshot/backup that was taken closest to TIME.
=head2 Time Abbreviations
In the list above the C<TIME> variables stand for a I<time abbreviation>.
There are two different kinds of I<time abbreviations>: I<relative times> and
I<immediate times>.
=head3 Relative Times
examples/yabsm.conf.example view on Meta::CPAN
# See the 'Configuration' section in 'man yabsm' for a
# detailed overview on how to create a yabsm configuration.
# This is the directory for yabsm to place snapshots, and use
# as a working dir for performing backups.
yabsm_dir=/.snapshots/yabsm
### Subvols ###
# A 'subvol' is yabsm's interface to a btrfs subvolume
#
# This configuration makes sense for a system that has
# two btrfs subvolumes mounted at '/' and '/home'.
subvol root_subvol {
examples/yabsm.conf.example view on Meta::CPAN
snap home_snap {
subvol=home_subvol
timeframes=hourly,daily
hourly_keep=24
daily_keep=31
daily_times=23:59
}
### SSH Backups ###
# A 'ssh_backup' represents a configuration for performing
# incremental backups over SSH.
#
# See the 'SSH Backups' section of 'man yabsm' for a detailed
# overview.
ssh_backup home_ssh_backup {
subvol=home_subvol
ssh_dest=larry@192.168.1.73
# this is a directory on the remote machine
dir=/.snapshots/yabsm-home-backup
timeframes=daily
daily_keep=365
daily_times=23:59
}
### Local Backups ###
# A 'local_backup' represents a configuration for performing
# incremental backups to a seperate partition of the same system.
# This is useful for backing up to an external hard drive.
#
# See the 'Local Backups' section of 'man yabsm' for a detailed
# overview.
local_backup home_local_backup {
subvol=home_subvol
dir=/mnt/easystore/yabsm-home-backup
timeframes=weekly
weekly_keep=56
weekly_day=sunday
weekly_time=23:59
}
lib/App/Yabsm.pm view on Meta::CPAN
# Author: Nicholas Hubbard
# WWW: https://github.com/NicholasBHubbard/yabsm
# License: MIT
# The main module of Yabsm.
# ABSTRACT: a btrfs snapshot and backup management system
use strict;
use warnings;
use v5.16.3;
package App::Yabsm;
our $VERSION = '3.14';
use App::Yabsm::Command::Daemon;
lib/App/Yabsm.pm view on Meta::CPAN
sub usage {
return <<'END_USAGE';
usage: yabsm [--help] [--version] [<COMMAND> <ARGS>]
see 'man yabsm' for a detailed overview of yabsm.
commands:
<config|c> [--help] [check ?file] [ssh-check <SSH_BACKUP>] [ssh-key]
[yabsm-user-home] [yabsm_dir] [subvols] [snaps] [ssh_backups]
[local_backups] [backups]
<find|f> [--help] [<SNAP|SSH_BACKUP|LOCAL_BACKUP> <QUERY>]
<daemon|d> [--help] [start] [stop] [restart] [status] [init]
END_USAGE
}
sub main {
# This is the toplevel subroutine of Yabsm.
lib/App/Yabsm/Backup/Generic.pm view on Meta::CPAN
# Author: Nicholas Hubbard
# WWW: https://github.com/NicholasBHubbard/yabsm
# License: MIT
# Functions needed for both SSH and local backups.
use strict;
use warnings;
use v5.16.3;
package App::Yabsm::Backup::Generic;
use App::Yabsm::Tools qw( :ALL );
use App::Yabsm::Config::Query qw( :ALL );
lib/App/Yabsm/Backup/Generic.pm view on Meta::CPAN
use Exporter 'import';
our @EXPORT_OK = qw(take_tmp_snapshot
tmp_snapshot_dir
take_bootstrap_snapshot
maybe_take_bootstrap_snapshot
bootstrap_snapshot_dir
the_local_bootstrap_snapshot
bootstrap_lock_file
create_bootstrap_lock_file
is_backup_type_or_die
);
####################################
# SUBROUTINES #
####################################
sub take_tmp_snapshot {
# Take a tmp snapshot for $backup. The tmp snapshot is the snapshot that is
# actually replicated in an incremental backup with 'btrfs send -p'.
arg_count_or_die(4, 4, @_);
my $backup = shift;
my $backup_type = shift;
my $tframe = shift;
my $config_ref = shift;
my $tmp_snapshot_dir = tmp_snapshot_dir(
$backup,
$backup_type,
$tframe,
$config_ref,
DIE_UNLESS_EXISTS => 1
);
# Remove any old tmp snapshots that were never deleted because of a failed
# incremental backup attempt.
opendir my $dh, $tmp_snapshot_dir or confess("yabsm: internal error: cannot opendir '$tmp_snapshot_dir'");
my @tmp_snapshots = grep { is_snapshot_name($_, ALLOW_BOOTSTRAP => 0) } readdir($dh);
closedir $dh;
map { $_ = "$tmp_snapshot_dir/$_" } @tmp_snapshots;
# The old tmp snapshot may be in the process of being sent which will cause
# the deletion to fail. In this case we can just ignore the failure.
for (@tmp_snapshots) {
try {
delete_snapshot($_);
}
catch ($e) {
; # do nothing
}
}
my $mountpoint;
if ($backup_type eq 'ssh') {
$mountpoint = ssh_backup_mountpoint($backup, $config_ref);
}
elsif ($backup_type eq 'local') {
$mountpoint = local_backup_mountpoint($backup, $config_ref);
}
else { is_backup_type_or_die($backup_type) }
return take_snapshot($mountpoint, $tmp_snapshot_dir);
}
sub tmp_snapshot_dir {
# Return path to $backup's tmp snapshot directory. If passed
# 'DIE_UNLESS_EXISTS => 1' # then die unless the directory exists and is
# readable+writable for the current user.
arg_count_or_die(4, 6, @_);
my $backup = shift;
my $backup_type = shift;
my $tframe = shift;
my $config_ref = shift;
my %die_unless_exists = (DIE_UNLESS_EXISTS => 0, @_);
is_timeframe_or_die($tframe);
if ($backup_type eq 'ssh') {
ssh_backup_exists_or_die($backup, $config_ref);
}
elsif ($backup_type eq 'ssh') {
local_backup_exists_or_die($backup, $config_ref);
}
else { is_backup_type_or_die($backup_type) }
my $tmp_snapshot_dir = yabsm_dir($config_ref) . "/.yabsm-var/${backup_type}_backups/$backup/tmp-snapshot/$tframe";
if ($die_unless_exists{DIE_UNLESS_EXISTS}) {
unless (-d $tmp_snapshot_dir && -r $tmp_snapshot_dir) {
my $username = getpwuid $<;
die "yabsm: error: no directory '$tmp_snapshot_dir' that is readable by user '$username'. This directory should have been initialized when the daemon started.\n";
}
}
return $tmp_snapshot_dir;
}
sub take_bootstrap_snapshot {
# Take a btrfs bootstrap snapshot of $backup and return its path.
# If there is already a bootstrap snapshot for $backup then delete
# it and take a new one.
arg_count_or_die(3, 3, @_);
my $backup = shift;
my $backup_type = shift;
my $config_ref = shift;
my $mountpoint;
if ($backup_type eq 'ssh') {
$mountpoint = ssh_backup_mountpoint($backup, $config_ref);
}
elsif ($backup_type eq 'local') {
$mountpoint = local_backup_mountpoint($backup, $config_ref);
}
else { is_backup_type_or_die($backup_type) }
if (my $bootstrap_snapshot = the_local_bootstrap_snapshot($backup, $backup_type, $config_ref)) {
delete_snapshot($bootstrap_snapshot);
}
my $bootstrap_dir = bootstrap_snapshot_dir($backup, $backup_type, $config_ref, DIE_UNLESS_EXISTS => 1);
my $snapshot_name = '.BOOTSTRAP-' . current_time_snapshot_name();
return take_snapshot($mountpoint, $bootstrap_dir, $snapshot_name);
}
sub maybe_take_bootstrap_snapshot {
# If $backup does not already have a bootstrap snapshot then take
# a bootstrap snapshot and return its path. Otherwise return the
# path of the existing bootstrap snapshot.
arg_count_or_die(3, 3, @_);
my $backup = shift;
my $backup_type = shift;
my $config_ref = shift;
if (my $boot_snap = the_local_bootstrap_snapshot($backup, $backup_type, $config_ref)) {
return $boot_snap;
}
return take_bootstrap_snapshot($backup, $backup_type, $config_ref);
}
sub bootstrap_snapshot_dir {
# Return the path to $ssh_backup's bootstrap snapshot directory.
# Logdie if the bootstrap snapshot directory does not exist.
arg_count_or_die(3, 5, @_);
my $backup = shift;
my $backup_type = shift;
my $config_ref = shift;
my %or_die = (DIE_UNLESS_EXISTS => 0, @_);
is_backup_type_or_die($backup_type);
if ($backup_type eq 'ssh') {
ssh_backup_exists_or_die($backup, $config_ref);
}
if ($backup_type eq 'local') {
local_backup_exists_or_die($backup, $config_ref);
}
my $bootstrap_dir = yabsm_dir($config_ref) . "/.yabsm-var/${backup_type}_backups/$backup/bootstrap-snapshot";
if ($or_die{DIE_UNLESS_EXISTS}) {
unless (-d $bootstrap_dir && -r $bootstrap_dir) {
my $username = getpwuid $<;
die "yabsm: error: no directory '$bootstrap_dir' that is readable by user '$username'. This directory should have been initialized when the daemon started.\n";
}
}
return $bootstrap_dir;
}
sub the_local_bootstrap_snapshot {
# Return the local bootstrap snapshot for $backup if it exists and return
# undef otherwise. Die if there are multiple bootstrap snapshots.
arg_count_or_die(3, 3, @_);
my $backup = shift;
my $backup_type = shift;
my $config_ref = shift;
my $bootstrap_dir = bootstrap_snapshot_dir(
$backup,
$backup_type,
$config_ref,
DIE_UNLESS_EXISTS => 1
);
opendir my $dh, $bootstrap_dir or confess "yabsm: internal error: cannot opendir '$bootstrap_dir'";
my @boot_snaps = grep { is_snapshot_name($_, ONLY_BOOTSTRAP => 1) } readdir($dh);
map { $_ = "$bootstrap_dir/$_" } @boot_snaps;
close $dh;
if (0 == @boot_snaps) {
return undef;
}
elsif (1 == @boot_snaps) {
return $boot_snaps[0];
}
else {
die "yabsm: error: found multiple local bootstrap snapshots for ${backup_type}_backup '$backup' in '$bootstrap_dir'\n";
}
}
sub bootstrap_lock_file {
# Return the path to the BOOTSTRAP-LOCK for $backup if it exists and return
# undef otherwise.
arg_count_or_die(3, 3, @_);
my $backup = shift;
my $backup_type = shift;
my $config_ref = shift;
my $rx = qr/yabsm-${backup_type}_backup_${backup}_BOOTSTRAP-LOCK/;
my $lock_file = [ grep /$rx/, glob('/tmp/*') ]->[0];
return $lock_file;
}
sub create_bootstrap_lock_file {
# Create the bootstrap lock file for $backup. This function should be called
# when performing the bootstrap phase of an incremental backup after checking
# to make sure a lock file doesn't already exist. If a lock file already
# exists we die, so check beforehand!
arg_count_or_die(3, 3, @_);
my $backup = shift;
my $backup_type = shift;
my $config_ref = shift;
backup_exists_or_die($backup, $config_ref);
is_backup_type_or_die($backup_type);
if (my $existing_lock_file = bootstrap_lock_file($backup, $backup_type, $config_ref)) {
die "yabsm: error: ${backup_type}_backup '$backup' is already locked out of performing a bootstrap. This was determined by the existence of '$existing_lock_file'\n";
}
# The file will be deleted when $tmp_fh is destroyed.
my $tmp_fh = File::Temp->new(
TEMPLATE => "yabsm-${backup_type}_backup_${backup}_BOOTSTRAP-LOCKXXXX",
DIR => '/tmp',
UNLINK => 1
);
return $tmp_fh;
}
sub is_backup_type_or_die {
# Logdie unless $backup_type equals 'ssh' or 'local'.
arg_count_or_die(1, 1, @_);
my $backup_type = shift;
unless ( $backup_type =~ /^(ssh|local)$/ ) {
confess("yabsm: internal error: '$backup_type' is not 'ssh' or 'local'");
}
return 1;
}
1;
lib/App/Yabsm/Backup/Local.pm view on Meta::CPAN
# Author: Nicholas Hubbard
# WWW: https://github.com/NicholasBHubbard/yabsm
# License: MIT
# Provides the &do_local_backup subroutine, which performs a single local_backup
# This is a top-level subroutine that is directly scheduled to be run by the
# daemon.
use strict;
use warnings;
use v5.16.3;
package App::Yabsm::Backup::Local;
use App::Yabsm::Backup::Generic qw(take_tmp_snapshot
lib/App/Yabsm/Backup/Local.pm view on Meta::CPAN
bootstrap_lock_file
create_bootstrap_lock_file
);
use App::Yabsm::Snapshot qw(delete_snapshot sort_snapshots is_snapshot_name);
use App::Yabsm::Tools qw( :ALL );
use App::Yabsm::Config::Query qw( :ALL );
use File::Basename qw(basename);
use Exporter qw(import);
our @EXPORT_OK = qw(do_local_backup
do_local_backup_bootstrap
maybe_do_local_backup_bootstrap
the_remote_bootstrap_snapshot
);
####################################
# SUBROUTINES #
####################################
sub do_local_backup {
# Perform a $tframe local_backup for $local_backup.
arg_count_or_die(3, 3, @_);
my $local_backup = shift;
my $tframe = shift;
my $config_ref = shift;
# We can't perform a backup if the bootstrap process is currently being
# performed.
if (bootstrap_lock_file($local_backup, 'local', $config_ref)) {
return undef;
}
my $backup_dir = local_backup_dir($local_backup, $tframe, $config_ref);
unless (is_btrfs_dir($backup_dir) && -r $backup_dir) {
my $username = getpwuid $<;
die "yabsm: error: '$backup_dir' is not a directory residing on a btrfs filesystem that is readable by user '$username'\n";
}
my $tmp_snapshot = take_tmp_snapshot($local_backup, 'local', $tframe, $config_ref);
my $bootstrap_snapshot = maybe_do_local_backup_bootstrap($local_backup, $config_ref);
system_or_die("sudo -n btrfs send -p '$bootstrap_snapshot' '$tmp_snapshot' | sudo -n btrfs receive '$backup_dir' >/dev/null 2>&1");
# @backups is sorted from newest to oldest
my @backups = sort_snapshots(do {
opendir my $dh, $backup_dir or confess("yabsm: internal error: cannot opendir '$backup_dir'");
my @backups = grep { is_snapshot_name($_) } readdir($dh);
closedir $dh;
map { $_ = "$backup_dir/$_" } @backups;
\@backups;
});
my $num_backups = scalar @backups;
my $to_keep = local_backup_timeframe_keep($local_backup, $tframe, $config_ref);
# There is 1 more backup than should be kept because we just performed a
# backup.
if ($num_backups == $to_keep + 1) {
my $oldest = pop @backups;
delete_snapshot($oldest);
}
# We have not reached the backup quota yet so we don't delete anything.
elsif ($num_backups <= $to_keep) {
;
}
# User changed their settings to keep less backups than they were keeping
# prior.
else {
for (; $num_backups > $to_keep; $num_backups--) {
my $oldest = pop @backups;
delete_snapshot($oldest);
}
}
return "$backup_dir/" . basename($tmp_snapshot);
}
sub do_local_backup_bootstrap {
# Perform the bootstrap phase of an incremental backup for $local_backup.
arg_count_or_die(2, 2, @_);
my $local_backup = shift;
my $config_ref = shift;
if (bootstrap_lock_file($local_backup, 'local', $config_ref)) {
return undef;
}
# The lock file will be deleted when $lock_fh goes out of scope (uses File::Temp).
my $lock_fh = create_bootstrap_lock_file($local_backup, 'local', $config_ref);
if (my $local_boot_snap = the_local_bootstrap_snapshot($local_backup, 'local', $config_ref)) {
delete_snapshot($local_boot_snap);
}
if (my $remote_boot_snap = the_remote_bootstrap_snapshot($local_backup, $config_ref)) {
delete_snapshot($remote_boot_snap);
}
my $local_boot_snap = take_bootstrap_snapshot($local_backup, 'local', $config_ref);
my $backup_dir_base = local_backup_dir($local_backup, undef, $config_ref);
system_or_die("sudo -n btrfs send '$local_boot_snap' | sudo -n btrfs receive '$backup_dir_base' >/dev/null 2>&1");
return $local_boot_snap;
}
sub maybe_do_local_backup_bootstrap {
# Like &do_local_backup_bootstrap but only perform the bootstrap if it hasn't
# been performed yet.
arg_count_or_die(2, 2, @_);
my $local_backup = shift;
my $config_ref = shift;
my $local_boot_snap = the_local_bootstrap_snapshot($local_backup, 'local', $config_ref);
my $remote_boot_snap = the_remote_bootstrap_snapshot($local_backup, $config_ref);
unless ($local_boot_snap && $remote_boot_snap) {
$local_boot_snap = do_local_backup_bootstrap($local_backup, $config_ref);
}
return $local_boot_snap;
}
sub the_remote_bootstrap_snapshot {
# Return the remote bootstrap snapshot for $local_backup if it exists and
# return undef otherwise. Die if we find multiple bootstrap snapshots.
arg_count_or_die(2, 2, @_);
my $local_backup = shift;
my $config_ref = shift;
my $backup_dir_base = local_backup_dir($local_backup, undef, $config_ref);
unless (-d $backup_dir_base && -r $backup_dir_base) {
my $username = getpwuid $<;
die "yabsm: error: no directory '$backup_dir_base' that is readable by user '$username'\n";
}
opendir my $dh, $backup_dir_base or confess("yabsm: internal error: cannot opendir '$backup_dir_base'");
my @boot_snaps = grep { is_snapshot_name($_, ONLY_BOOTSTRAP => 1) } readdir($dh);
closedir $dh;
map { $_ = "$backup_dir_base/$_" } @boot_snaps;
if (0 == @boot_snaps) {
return undef;
}
elsif (1 == @boot_snaps) {
return $boot_snaps[0];
}
else {
die "yabsm: error: found multiple remote bootstrap snapshots for local_backup '$local_backup' in '$backup_dir_base'\n";
}
}
1;
lib/App/Yabsm/Backup/SSH.pm view on Meta::CPAN
# Author: Nicholas Hubbard
# WWW: https://github.com/NicholasBHubbard/yabsm
# License: MIT
# Provides the &do_ssh_backup subroutine, which performs a single
# ssh_backup. This is a top-level subroutine that is directly scheduled to be
# run by the daemon.
use strict;
use warnings;
use v5.16.3;
package App::Yabsm::Backup::SSH;
use App::Yabsm::Tools qw( :ALL );
use App::Yabsm::Config::Query qw( :ALL );
lib/App/Yabsm/Backup/SSH.pm view on Meta::CPAN
take_tmp_snapshot
bootstrap_lock_file
create_bootstrap_lock_file
);
use Net::OpenSSH;
use Carp qw(confess);
use File::Basename qw(basename);
use Exporter 'import';
our @EXPORT_OK = qw(do_ssh_backup
do_ssh_backup_bootstrap
maybe_do_ssh_backup_bootstrap
the_remote_bootstrap_snapshot
new_ssh_conn
ssh_system_or_die
check_ssh_backup_config_or_die
);
####################################
# SUBROUTINES #
####################################
sub do_ssh_backup {
# Perform a $tframe ssh_backup for $ssh_backup.
arg_count_or_die(4, 4, @_);
my $ssh = shift;
my $ssh_backup = shift;
my $tframe = shift;
my $config_ref = shift;
# We can't do a backup if the bootstrap process is currently being performed.
if (bootstrap_lock_file($ssh_backup, 'ssh', $config_ref)) {
return undef;
}
$ssh //= new_ssh_conn($ssh_backup, $config_ref);
check_ssh_backup_config_or_die($ssh, $ssh_backup, $config_ref);
my $tmp_snapshot = take_tmp_snapshot($ssh_backup, 'ssh', $tframe, $config_ref);
my $bootstrap_snapshot = maybe_do_ssh_backup_bootstrap($ssh, $ssh_backup, $config_ref);
my $backup_dir = ssh_backup_dir($ssh_backup, $tframe, $config_ref);
my $backup_dir_base = ssh_backup_dir($ssh_backup, undef, $config_ref);
ssh_system_or_die(
$ssh,
# This is why we need the remote user to have write permission on the
# backup dir
"if ! [ -d '$backup_dir' ]; then mkdir '$backup_dir'; fi"
);
ssh_system_or_die(
$ssh,
{stdin_file => ['-|', "sudo -n btrfs send -p '$bootstrap_snapshot' '$tmp_snapshot'"]},
"sudo -n btrfs receive '$backup_dir'"
);
# The tmp snapshot is irrelevant now
delete_snapshot($tmp_snapshot);
# Delete old backups
my @remote_backups = grep { is_snapshot_name($_) } ssh_system_or_die($ssh, "ls -1 '$backup_dir'");
map { chomp $_ ; $_ = "$backup_dir/$_" } @remote_backups;
# sorted from newest to oldest
@remote_backups = sort_snapshots(\@remote_backups);
my $num_backups = scalar @remote_backups;
my $to_keep = ssh_backup_timeframe_keep($ssh_backup, $tframe, $config_ref);
# There is 1 more backup than should be kept because we just performed a
# backup.
if ($num_backups == $to_keep + 1) {
my $oldest = pop @remote_backups;
ssh_system_or_die($ssh, "sudo -n btrfs subvolume delete '$oldest'");
}
# We havent reached the backup quota yet so we don't delete anything
elsif ($num_backups <= $to_keep) {
;
}
# User changed their settings to keep less backups than they were keeping
# prior.
else {
for (; $num_backups > $to_keep; $num_backups--) {
my $oldest = pop @remote_backups;
ssh_system_or_die($ssh, "sudo -n btrfs subvolume delete '$oldest'");
}
}
return "$backup_dir/" . basename($tmp_snapshot);
}
sub do_ssh_backup_bootstrap {
# Perform the bootstrap phase of an incremental backup for $ssh_backup.
arg_count_or_die(3, 3, @_);
my $ssh = shift;
my $ssh_backup = shift;
my $config_ref = shift;
if (bootstrap_lock_file($ssh_backup, 'ssh', $config_ref)) {
return undef;
}
# The lock file will be deleted when $lock_fh goes out of scope (uses File::Temp).
my $lock_fh = create_bootstrap_lock_file($ssh_backup, 'ssh', $config_ref);
$ssh //= new_ssh_conn($ssh_backup, $config_ref);
if (my $local_boot_snap = the_local_bootstrap_snapshot($ssh_backup, 'ssh', $config_ref)) {
delete_snapshot($local_boot_snap);
}
if (my $remote_boot_snap = the_remote_bootstrap_snapshot($ssh, $ssh_backup, $config_ref)) {
ssh_system_or_die($ssh, "sudo -n btrfs subvolume delete '$remote_boot_snap'");
}
my $local_boot_snap = take_bootstrap_snapshot($ssh_backup, 'ssh', $config_ref);
my $remote_backup_dir = ssh_backup_dir($ssh_backup, undef, $config_ref);
ssh_system_or_die(
$ssh,
{stdin_file => ['-|', "sudo -n btrfs send '$local_boot_snap'"]},
"sudo -n btrfs receive '$remote_backup_dir'"
);
return $local_boot_snap;
}
sub maybe_do_ssh_backup_bootstrap {
# Like &do_ssh_backup_bootstrap but only perform the bootstrap if it hasn't
# been performed yet.
arg_count_or_die(3, 3, @_);
my $ssh = shift;
my $ssh_backup = shift;
my $config_ref = shift;
$ssh //= new_ssh_conn($ssh_backup, $config_ref);
my $local_boot_snap = the_local_bootstrap_snapshot($ssh_backup, 'ssh', $config_ref);
my $remote_boot_snap = the_remote_bootstrap_snapshot($ssh, $ssh_backup, $config_ref);
unless ($local_boot_snap && $remote_boot_snap) {
$local_boot_snap = do_ssh_backup_bootstrap($ssh, $ssh_backup, $config_ref);
}
return $local_boot_snap;
}
sub the_remote_bootstrap_snapshot {
# Return the remote bootstrap snapshot for $ssh_backup if it exists and
# return undef otherwise. Die if we find multiple bootstrap snapshots.
arg_count_or_die(3, 3, @_);
my $ssh = shift;
my $ssh_backup = shift;
my $config_ref = shift;
$ssh //= new_ssh_conn($ssh_backup, $config_ref);
my $remote_backup_dir = ssh_backup_dir($ssh_backup, undef, $config_ref);
my @boot_snaps = grep { is_snapshot_name($_, ONLY_BOOTSTRAP => 1) } ssh_system_or_die($ssh, "ls -1 -a '$remote_backup_dir'");
map { chomp $_ ; $_ = "$remote_backup_dir/$_" } @boot_snaps;
if (0 == @boot_snaps) {
return undef;
}
elsif (1 == @boot_snaps) {
return $boot_snaps[0];
}
else {
my $ssh_dest = ssh_backup_ssh_dest($ssh_backup, $config_ref);
die "yabsm: ssh error: $ssh_dest: found multiple remote bootstrap snapshots in '$remote_backup_dir'\n";
}
}
sub new_ssh_conn {
# Return a Net::OpenSSH connection object to $ssh_backup's ssh destination or
# die if a connection cannot be established.
arg_count_or_die(2, 2, @_);
my $ssh_backup = shift;
my $config_ref = shift;
my $home_dir = (getpwuid $<)[7]
or die q(yabsm: error: user ').scalar(getpwuid $<).q(' does not have a home directory to hold SSH keys);
my $pub_key = "$home_dir/.ssh/id_ed25519.pub";
my $priv_key = "$home_dir/.ssh/id_ed25519";
unless (-f $pub_key) {
my $username = getpwuid $<;
die "yabsm: error: cannot not find '$username' users SSH public SSH key '$pub_key'\n";
}
unless (-f $priv_key) {
my $username = getpwuid $<;
die "yabsm: error: cannot not find '$username' users private SSH key '$priv_key'\n";
}
my $ssh_dest = ssh_backup_ssh_dest($ssh_backup, $config_ref);
my $ssh = Net::OpenSSH->new(
$ssh_dest,
master_opts => [ '-q' ], # quiet
batch_mode => 1, # Key based auth only
ctl_dir => '/tmp',
remote_shell => 'sh',
);
if ($ssh->error) {
lib/App/Yabsm/Backup/SSH.pm view on Meta::CPAN
wantarray ? my @out = $ssh->capture(\%opts, $cmd) : my $out = $ssh->capture(\%opts, $cmd);
if ($ssh->error) {
my $host = $ssh->get_host;
die "yabsm: ssh error: $host: remote command '$cmd' failed:".$ssh->error."\n";
}
return wantarray ? @out : $out;
}
sub check_ssh_backup_config_or_die {
# Ensure that the $ssh_backup's ssh destination server is configured
# properly and die with useful errors if not.
arg_count_or_die(3, 3, @_);
my $ssh = shift;
my $ssh_backup = shift;
my $config_ref = shift;
$ssh //= new_ssh_conn($ssh_backup, $config_ref);
my $remote_backup_dir = ssh_backup_dir($ssh_backup, undef, $config_ref);
my $ssh_dest = ssh_backup_ssh_dest($ssh_backup, $config_ref);
my (undef, $stderr) = $ssh->capture2(qq(
ERRORS=''
add_error() {
if [ -z "\$ERRORS" ]; then
ERRORS="yabsm: ssh error: $ssh_dest: \$1"
else
ERRORS="\${ERRORS}\nyabsm: ssh error: $ssh_dest: \$1"
fi
lib/App/Yabsm/Backup/SSH.pm view on Meta::CPAN
if ! which btrfs >/dev/null 2>&1; then
HAVE_BTRFS=false
add_error "btrfs-progs not in '\$(whoami)'s path"
fi
if [ "\$HAVE_BTRFS" = true ] && ! sudo -n btrfs --help >/dev/null 2>&1; then
add_error "user '\$(whoami)' does not have root sudo access to btrfs-progs"
fi
if ! [ -d '$remote_backup_dir' ] || ! [ -r '$remote_backup_dir' ] || ! [ -w '$remote_backup_dir' ]; then
add_error "no directory '$remote_backup_dir' that is readable+writable by user '\$(whoami)'"
else
if [ "\$HAVE_BTRFS" = true ] && ! btrfs property list '$remote_backup_dir' >/dev/null 2>&1; then
add_error "'$remote_backup_dir' is not a directory residing on a btrfs filesystem"
fi
fi
if [ -n '\$ERRORS' ]; then
1>&2 printf %s "\$ERRORS"
exit 1
else
exit 0
fi
));
lib/App/Yabsm/Command/Config.pm view on Meta::CPAN
use App::Yabsm::Config::Query qw( :ALL );
use App::Yabsm::Config::Parser qw(parse_config_or_die);
use App::Yabsm::Backup::SSH;
use App::Yabsm::Command::Daemon;
sub usage {
arg_count_or_die(0, 0, @_);
return <<'END_USAGE';
usage: yabsm <config|c> [--help] [check ?file] [ssh-check <SSH_BACKUP>] [ssh-key]
[yabsm-user-home] [yabsm_dir] [subvols] [snaps]
[ssh_backups] [local_backups] [backups]
END_USAGE
}
sub help {
@_ == 0 or die usage();
my $usage = usage();
$usage =~ s/\s+$//;
print <<"END_HELP";
$usage
--help Print this help message.
check ?file Check ?file for errors and print their messages. If ?file
is omitted it defaults to /etc/yabsm.conf.
ssh-check <SSH_BACKUP> Check that backups for <SSH_BACKUP> are able to be
performed and if not print useful error messages.
ssh-key Print the 'yabsm' users public SSH key.
yabsm-user-home Print the 'yabsm' users home directory.
yabsm_dir Print the value of yabsm_dir in /etc/yabsm.conf.
subvols Print names of all subvols defined in /etc/yabsm.conf.
snaps Print names of all snaps defined in /etc/yabsm.conf.
ssh_backups Print names of all ssh_backups defined in /etc/yabsm.conf.
local_backups Print the of all local_backups defined in /etc/yabsm.conf.
backups Print names of all ssh_backups and local_backups defined
in /etc/yabsm.conf.
END_HELP
}
####################################
# MAIN #
####################################
sub main {
my $cmd = shift or die usage();
if ($cmd =~ /^(-h|--help)$/ ) { help(@_) }
elsif ($cmd eq 'check' ) { check_config(@_) }
elsif ($cmd eq 'ssh-check' ) { check_ssh_backup(@_) }
elsif ($cmd eq 'ssh-key' ) { print_yabsm_user_ssh_key(@_) }
elsif ($cmd eq 'yabsm_user_home') { print_yabsm_user_home(@_) }
elsif ($cmd eq 'yabsm_dir' ) { print_yabsm_dir(@_) }
elsif ($cmd eq 'subvols' ) { print_subvols(@_) }
elsif ($cmd eq 'snaps' ) { print_snaps(@_) }
elsif ($cmd eq 'ssh_backups' ) { print_ssh_backups(@_) }
elsif ($cmd eq 'local_backups' ) { print_local_backups(@_) }
elsif ($cmd eq 'backups' ) { print_backups(@_) }
else {
die usage();
}
}
####################################
# SUBCOMMANDS #
####################################
sub check_config {
lib/App/Yabsm/Command/Config.pm view on Meta::CPAN
my $config_ref = parse_config_or_die();
say for all_subvols($config_ref);
}
sub print_snaps {
@_ == 0 or die usage();
my $config_ref = parse_config_or_die();
say for all_snaps($config_ref);
}
sub print_ssh_backups {
@_ == 0 or die usage();
my $config_ref = parse_config_or_die();
say for all_ssh_backups($config_ref);
}
sub print_local_backups {
@_ == 0 or die usage();
my $config_ref = parse_config_or_die();
say for all_local_backups($config_ref);
}
sub print_backups {
@_ == 0 or die usage();
my $config_ref = parse_config_or_die();
my @ssh_backups = all_ssh_backups($config_ref);
my @local_backups = all_local_backups($config_ref);
say for sort @ssh_backups, @local_backups;
}
sub print_yabsm_dir {
@_ == 0 or die usage();
my $config_ref = parse_config_or_die();
my $yabsm_dir = yabsm_dir($config_ref);
say $yabsm_dir;
}
sub print_yabsm_user_home {
@_ == 0 or die usage();
my $config_ref = parse_config_or_die();
my $yabsm_user_home = yabsm_user_home($config_ref);
say $yabsm_user_home;
}
sub check_ssh_backup {
# This is mostly just a wrapper around
# &App::Yabsm::Backup::SSH::check_ssh_backup_config_or_die.
@_ == 1 or die usage();
die 'yabsm: error: permission denied'."\n" unless i_am_root();
my $ssh_backup = shift;
my $config_ref = parse_config_or_die();
unless (ssh_backup_exists($ssh_backup, $config_ref)) {
die "yabsm: error: no such ssh_backup named '$ssh_backup'\n";
}
unless (App::Yabsm::Command::Daemon::yabsm_user_exists()) {
die q(yabsm: error: cannot find user named 'yabsm')."\n";
}
unless (App::Yabsm::Command::Daemon::yabsm_group_exists()) {
die q(yabsm: error: cannot find group named 'yabsm')."\n";
}
POSIX::setgid(scalar(getgrnam 'yabsm'));
POSIX::setuid(scalar(getpwnam 'yabsm'));
App::Yabsm::Backup::SSH::check_ssh_backup_config_or_die(undef, $ssh_backup, $config_ref);
say 'all good';
}
sub print_yabsm_user_ssh_key {
# Print the yabsm users public key to STDOUT.
@_ == 0 or die usage();
lib/App/Yabsm/Command/Daemon.pm view on Meta::CPAN
####################################
# HELPERS #
####################################
sub initialize_yabsmd_runtime_environment {
# Initialize yabsmd's runtime environment:
#
# * Install the signal handlers that remove the PID file before exiting
# * Create dirs needed for performing snaps, ssh_backups, and local_backups
# * Create the yabsm user and group if they don't already exists
# * If $create_log_file, create /var/log/yabsm if it does not exist and chown it to yabsm:yabsm
# * If $create_pid_file, create the (empty) file /run/yabsmd.pid and chown it to yabsm:yabsm
# * Create the yabsm users SSH keys if they don't already exist
# * Set this processes UID and GID to yabsm:yabsm
arg_count_or_die(3, 3, @_);
my $create_log_file = shift;
my $create_pid_file = shift;
lib/App/Yabsm/Command/Daemon.pm view on Meta::CPAN
POSIX::setgid($yabsm_gid);
POSIX::setuid($yabsm_uid);
create_yabsm_user_ssh_key(0, $config_ref);
return 1;
}
sub create_cron_scheduler {
# Return a Schedule::Cron object that schedules every snap, ssh_backup, and
# local_backup that is defined in the users config.
arg_count_or_die(1, 1, @_);
my $config_ref = shift;
my $cron_scheduler = Schedule::Cron->new(
sub { confess("yabsm: internal error: default Schedule::Cron dispatcher was invoked") },
processprefix => 'yabsmd'
);
lib/App/Yabsm/Command/Daemon.pm view on Meta::CPAN
my $hr = time_hour($time);
my $min = time_minute($time);
my $day = snap_monthly_day($snap, $config_ref);
$cron_scheduler->add_entry(
"$min $hr $day * *",
sub { with_error_catch_log(\&App::Yabsm::Snap::do_snap, $snap, 'monthly', $config_ref) }
);
}
}
for my $ssh_backup (all_ssh_backups($config_ref)) {
if (ssh_backup_wants_timeframe($ssh_backup, '5minute', $config_ref)) {
$cron_scheduler->add_entry(
'*/5 * * * *',
sub { with_error_catch_log(\&App::Yabsm::Backup::SSH::do_ssh_backup, undef, $ssh_backup, '5minute', $config_ref) }
);
}
if (ssh_backup_wants_timeframe($ssh_backup, 'hourly', $config_ref)) {
$cron_scheduler->add_entry(
'0 */1 * * *',
sub { with_error_catch_log(\&App::Yabsm::Backup::SSH::do_ssh_backup, undef, $ssh_backup, 'hourly', $config_ref) }
);
}
if (ssh_backup_wants_timeframe($ssh_backup, 'daily', $config_ref)) {
for my $time (ssh_backup_daily_times($ssh_backup, $config_ref)) {
my $hr = time_hour($time);
my $min = time_minute($time);
$cron_scheduler->add_entry(
"$min $hr * * *",
sub { with_error_catch_log(\&App::Yabsm::Backup::SSH::do_ssh_backup, undef, $ssh_backup, 'daily', $config_ref) }
);
}
}
if (ssh_backup_wants_timeframe($ssh_backup, 'weekly', $config_ref)) {
my $time = ssh_backup_weekly_time($ssh_backup, $config_ref);
my $hr = time_hour($time);
my $min = time_minute($time);
my $day = weekday_number(ssh_backup_weekly_day($ssh_backup, $config_ref));
$cron_scheduler->add_entry(
"$min $hr * * $day",
sub { with_error_catch_log(\&App::Yabsm::Backup::SSH::do_ssh_backup, undef, $ssh_backup, 'weekly', $config_ref) }
);
}
if (ssh_backup_wants_timeframe($ssh_backup, 'monthly', $config_ref)) {
my $time = ssh_backup_monthly_time($ssh_backup, $config_ref);
my $hr = time_hour($time);
my $min = time_minute($time);
my $day = ssh_backup_monthly_day($ssh_backup, $config_ref);
$cron_scheduler->add_entry(
"$min $hr $day * *",
sub { with_error_catch_log(\&App::Yabsm::Backup::SSH::do_ssh_backup, undef, $ssh_backup, 'monthly', $config_ref) }
);
}
}
for my $local_backup (all_local_backups($config_ref)) {
if (local_backup_wants_timeframe($local_backup, '5minute', $config_ref)) {
$cron_scheduler->add_entry(
'*/5 * * * *',
sub { with_error_catch_log(\&App::Yabsm::Backup::Local::do_local_backup, $local_backup, '5minute', $config_ref) }
);
}
if (local_backup_wants_timeframe($local_backup, 'hourly', $config_ref)) {
$cron_scheduler->add_entry(
'0 */1 * * *',
sub { with_error_catch_log(\&App::Yabsm::Backup::Local::do_local_backup, $local_backup, 'hourly', $config_ref) }
);
}
if (local_backup_wants_timeframe($local_backup, 'daily', $config_ref)) {
for my $time (local_backup_daily_times($local_backup, $config_ref)) {
my $hr = time_hour($time);
my $min = time_minute($time);
$cron_scheduler->add_entry(
"$min $hr * * *",
sub { with_error_catch_log(\&App::Yabsm::Backup::Local::do_local_backup, $local_backup, 'daily', $config_ref) }
);
}
}
if (local_backup_wants_timeframe($local_backup, 'weekly', $config_ref)) {
my $time = local_backup_weekly_time($local_backup, $config_ref);
my $hr = time_hour($time);
my $min = time_minute($time);
my $day = weekday_number(local_backup_weekly_day($local_backup, $config_ref));
$cron_scheduler->add_entry(
"$min $hr * * $day",
sub { with_error_catch_log(\&App::Yabsm::Backup::Local::do_local_backup, $local_backup, 'weekly', $config_ref) }
);
}
if (local_backup_wants_timeframe($local_backup, 'monthly', $config_ref)) {
my $time = local_backup_monthly_time($local_backup, $config_ref);
my $hr = time_hour($time);
my $min = time_minute($time);
my $day = local_backup_monthly_day($local_backup, $config_ref);
$cron_scheduler->add_entry(
"$min $hr $day * *",
sub { with_error_catch_log(\&App::Yabsm::Backup::Local::do_local_backup, $local_backup, 'monthly', $config_ref) }
);
}
}
return $cron_scheduler;
}
sub create_yabsmd_runtime_dirs {
# Create the directories needed for the daemon to perform every snap,
# ssh_backup, and local_backup.
arg_count_or_die(1, 1, @_);
my $config_ref = shift;
i_am_root_or_die();
for my $snap (all_snaps($config_ref)) {
for my $tframe (snap_timeframes($snap, $config_ref)) {
make_path_or_die(snap_dest($snap, $tframe, $config_ref));
}
}
for my $ssh_backup (all_ssh_backups($config_ref)) {
make_path_or_die(App::Yabsm::Backup::Generic::bootstrap_snapshot_dir($ssh_backup, 'ssh', $config_ref));
for my $tframe (ssh_backup_timeframes($ssh_backup, $config_ref)) {
make_path_or_die(App::Yabsm::Backup::Generic::tmp_snapshot_dir($ssh_backup, 'ssh', $tframe, $config_ref));
}
}
for my $local_backup (all_local_backups($config_ref)) {
make_path_or_die(App::Yabsm::Backup::Generic::bootstrap_snapshot_dir($local_backup, 'local', $config_ref));
my $backup_dir_exists = -d local_backup_dir($local_backup, undef, $config_ref);
for my $tframe (local_backup_timeframes($local_backup, $config_ref)) {
make_path_or_die(App::Yabsm::Backup::Generic::tmp_snapshot_dir($local_backup, 'local', $tframe, $config_ref));
if ($backup_dir_exists) {
make_path_or_die(local_backup_dir($local_backup, $tframe, $config_ref));
}
}
}
return 1;
}
sub yabsmd_pid {
# If there is a running instance of yabsmd return its pid and otherwise
# return 0.
lib/App/Yabsm/Command/Daemon.pm view on Meta::CPAN
$SIG{XFSZ} = $cleanup_and_exit;
}
sub create_yabsm_user_ssh_key {
# Create an SSH key for the yabsm user if one doesn't already exist. This
# function dies unless the processes ruid and rgid are that of the yabsm user
# and group.
#
# If the $force value is false then only create the key if the users
# configuration defines at least one ssh_backup, and if it is true then
# create the key even if no ssh_backup's are defined.
arg_count_or_die(2, 2, @_);
my $force = shift;
my $config_ref = shift;
if ($force || all_ssh_backups($config_ref)) {
my $yabsm_uid = getpwnam('yabsm') or confess(q(yabsm: internal error: cannot find user named 'yabsm'));
my $yabsm_gid = getgrnam('yabsm') or confess(q(yabsm: internal error: cannot find group named 'yabsm'));
unless (POSIX::getuid() == $yabsm_uid && POSIX::getgid() == $yabsm_gid) {
my $username = getpwuid POSIX::getuid();
my $groupname = getgrgid POSIX::getgid();
confess "yabsm: internal error: expected to be running as user and group yabsm but instead running as user '$username' and group '$groupname'";
}
lib/App/Yabsm/Command/Find.pm view on Meta::CPAN
}
sub help {
@_ == 0 or die usage();
my $usage = usage();
$usage =~ s/\s+$//;
print <<"END_HELP";
$usage
see the section "Finding Snapshots" in 'man yabsm' for a detailed explanation on
how to find snapshots and backups.
examples:
yabsm find home_snap back-10-hours
yabsm f root_ssh_backup newest
yabsm f home_local_backup oldest
yabsm f home_snap 'between b-10-mins 15:45'
yabsm f root_snap 'after back-2-days'
yabsm f root_local_backup 'before b-14-d'
END_HELP
}
####################################
# MAIN #
####################################
sub main {
if (@_ == 1) {
lib/App/Yabsm/Command/Find.pm view on Meta::CPAN
help();
}
elsif (@_ == 2) {
my $thing = shift;
my $query = shift;
my $config_ref = parse_config_or_die();
unless (snap_exists($thing, $config_ref) || ssh_backup_exists($thing, $config_ref) || local_backup_exists($thing, $config_ref)) {
die "yabsm: error: no such snap, ssh_backup, or local_backup named '$thing'\n";
}
my @snapshots = answer_query($thing, parse_query_or_die($query), $config_ref);
say for @snapshots;
}
else {
die usage()
}
}
####################################
# QUERY ANSWERING #
####################################
sub answer_query {
# Return a subset of all the snapshots/backups of $thing that satisfy
# $query.
arg_count_or_die(3, 3, @_);
my $thing = shift;
my %query = %{+shift};
my $config_ref = shift;
my @snapshots;
lib/App/Yabsm/Command/Find.pm view on Meta::CPAN
my $dir = snap_dest($thing, $tframe, $config_ref);
unless (-r $dir) {
die "yabsm: error: do not have read permission on '$dir'\n";
}
opendir my $dh, $dir or confess "yabsm: internal error: could not opendir '$dir'";
push @snapshots, map { $_ = "$dir/$_" } grep { is_snapshot_name($_) } readdir($dh);
closedir $dh;
}
}
elsif (ssh_backup_exists($thing, $config_ref)) {
die 'yabsm: error: permission denied'."\n" unless i_am_root();
my $yabsm_uid = getpwnam('yabsm') or die q(yabsm: error: no user named 'yabsm')."\n";
POSIX::setuid($yabsm_uid);
my $ssh = App::Yabsm::Backup::SSH::new_ssh_conn($thing, $config_ref);
my $ssh_dest = ssh_backup_ssh_dest($thing, $config_ref);
if ($ssh->error) {
die "yabsm: ssh error: $ssh_dest: ".$ssh->error."\n";
}
for my $tframe (ssh_backup_timeframes($thing, $config_ref)) {
my $dir = ssh_backup_dir($thing, $tframe, $config_ref);
unless ($ssh->system("[ -r '$dir' ]")) {
die "yabsm: ssh error: $ssh_dest: remote user does not have read permission on '$dir'\n";
}
push @snapshots, grep { chomp $_; is_snapshot_name($_) } App::Yabsm::Backup::SSH::ssh_system_or_die($ssh, "ls -1 '$dir'");
map { $_ = "$dir/$_" } @snapshots;
}
}
elsif (local_backup_exists($thing, $config_ref)) {
for my $tframe (local_backup_timeframes($thing, $config_ref)) {
my $dir = local_backup_dir($thing, $tframe, $config_ref);
unless (-r $dir) {
die "yabsm: error: do not have read permission on '$dir'\n";
}
opendir my $dh, $dir or confess "yabsm: internal error: could not opendir '$dir'";
push @snapshots, map { $_ = "$dir/$_" } grep { is_snapshot_name($_) } readdir($dh);
closedir $dh;
}
}
else {
die "yabsm: internal error: no such snap, ssh_backup, or local_backup named '$thing'";
}
@snapshots = sort_snapshots(\@snapshots);
if ($query{type} eq 'all') {
;
}
elsif ($query{type} eq 'newest') {
@snapshots = answer_newest_query(\@snapshots);
lib/App/Yabsm/Config/Parser.pm view on Meta::CPAN
# %config = ( yabsm_dir => '/.snapshots/yabsm'
#
# subvols => { foo => { mountpoint=/foo_dir }
# , bar => { mountpoint=/bar_dir }
# , ...
# },
# snaps => { foo_snap => { key=val, ... }
# , bar_snap => { key=val, ... }
# , ...
# },
# ssh_backups => { foo_ssh_backup => { key=val, ... }
# , bar_ssh_backup => { key=val, ... }
# , ...
# },
# local_backups => { foo_local_backup => { key=val, ... }
# , bar_local_backup => { key=val, ... }
# , ...
# }
# );
use strict;
use warnings;
use v5.16.3;
package App::Yabsm::Config::Parser;
lib/App/Yabsm/Config/Parser.pm view on Meta::CPAN
my %snap_settings_grammar = (
subvol => $grammar{subvol},
timeframes => $grammar{timeframes},
%timeframe_sub_grammar
);
return wantarray ? %snap_settings_grammar : \%snap_settings_grammar;
}
sub ssh_backup_settings_grammar {
# Return a hash of a ssh_backups key=val grammar. Optionally takes a false
# value to exclude the timeframe subgrammar from the returned grammar.
arg_count_or_die(0, 1, @_);
my $include_tf = shift // 1;
my %grammar = grammar();
my %timeframe_sub_grammar =
$include_tf ? %{ $grammar{timeframe_sub_grammar} } : ();
my %ssh_backup_settings_grammar = (
subvol => $grammar{subvol},
ssh_dest => $grammar{ssh_dest},
dir => $grammar{dir},
timeframes => $grammar{timeframes},
%timeframe_sub_grammar
);
return wantarray ? %ssh_backup_settings_grammar : \%ssh_backup_settings_grammar;
}
sub local_backup_settings_grammar {
# Return a hash of a local_backups key=val grammar. Optionally takes a false
# value to exclude the timeframe subgrammar from the returned grammar.
arg_count_or_die(0, 1, @_);
my $include_tf = shift // 1;
my %grammar = grammar();
my %timeframe_sub_grammar =
$include_tf ? %{ $grammar{timeframe_sub_grammar} } : ();
my %local_backup_settings_grammar = (
subvol => $grammar{subvol},
dir => $grammar{dir},
timeframes => $grammar{timeframes},
%timeframe_sub_grammar
);
return wantarray ? %local_backup_settings_grammar : \%local_backup_settings_grammar;
}
####################################
# PARSER #
####################################
sub config_parser {
# Top level parser
lib/App/Yabsm/Config/Parser.pm view on Meta::CPAN
my $dir = $self->maybe_expect($grammar{dir}) // $self->fail(grammar_msg->{dir});
$config{yabsm_dir} = $dir;
},
sub {
$self->expect( 'subvol' );
$self->commit;
my $name = $self->maybe_expect( $grammar{name} );
$name // $self->fail('expected subvol name');
exists $config{subvols}{$name} and $self->fail("already have a subvol named '$name'");
exists $config{snaps}{$name} and $self->fail("already have a snap named '$name'");
exists $config{ssh_backups}{$name} and $self->fail("already have a ssh_backup named '$name'");
exists $config{local_backups}{$name} and $self->fail("already have a local_backup named '$name'");
my $kvs = $self->scope_of('{', 'subvol_settings_parser' ,'}');
$config{subvols}{$name} = $kvs;
},
sub {
$self->expect( 'snap' );
$self->commit;
my $name = $self->maybe_expect( $grammar{name} );
$name // $self->fail('expected snap name');
exists $config{subvols}{$name} and $self->fail("already have a subvol named '$name'");
exists $config{snaps}{$name} and $self->fail("already have a snap named '$name'");
exists $config{ssh_backups}{$name} and $self->fail("already have a ssh_backup named '$name'");
exists $config{local_backups}{$name} and $self->fail("already have a local_backup named '$name'");
my $kvs = $self->scope_of('{', 'snap_settings_parser', '}');
$config{snaps}{$name} = $kvs;
},
sub {
$self->expect( 'ssh_backup' );
$self->commit;
my $name = $self->maybe_expect( $grammar{name} );
$name // $self->fail('expected ssh_backup name');
exists $config{subvols}{$name} and $self->fail("already have a subvol named '$name'");
exists $config{snaps}{$name} and $self->fail("already have a snap named '$name'");
exists $config{ssh_backups}{$name} and $self->fail("already have a ssh_backup named '$name'");
exists $config{local_backups}{$name} and $self->fail("already have a local_backup named '$name'");
my $kvs = $self->scope_of('{', 'ssh_backup_settings_parser', '}');
$config{ssh_backups}{$name} = $kvs;
},
sub {
$self->expect( 'local_backup' );
$self->commit;
my $name = $self->maybe_expect( $grammar{name} );
$name // $self->fail('expected local_backup name');
exists $config{subvols}{$name} and $self->fail("already have a subvol named '$name'");
exists $config{snaps}{$name} and $self->fail("already have a snap named '$name'");
exists $config{ssh_backups}{$name} and $self->fail("already have a ssh_backup named '$name'");
exists $config{local_backups}{$name} and $self->fail("already have a local_backup named '$name'");
my $kvs = $self->scope_of('{', 'local_backup_settings_parser', '}');
$config{local_backups}{$name} = $kvs;
},
sub {
$self->commit;
$self->skip_ws; # skip_ws also skips comments
$self->fail(q(expected one of 'subvol', 'snap', 'ssh_backup', or 'local_backup'));
}
);
});
return wantarray ? %config : \%config;
}
sub settings_parser {
# Abstract method that parses a sequence of key=val pairs based off of the
# input grammar %grammar. The arg $type is simply a string that is either
# 'subvol', 'snap', 'ssh_backup', or 'local_backup' and is only used for
# error message generation. This method should be called from a wrapper
# method.
arg_count_or_die(3, 3, @_);
my $self = shift;
my $type = shift;
my $grammar = shift;
my @settings = keys %{ $grammar };
lib/App/Yabsm/Config/Parser.pm view on Meta::CPAN
$self->settings_parser('subvol', $subvol_settings_grammar);
}
sub snap_settings_parser {
arg_count_or_die(1, 1, @_);
my $self = shift;
my $snap_settings_grammar = snap_settings_grammar();
$self->settings_parser('snap', $snap_settings_grammar);
}
sub ssh_backup_settings_parser {
arg_count_or_die(1, 1, @_);
my $self = shift;
my $ssh_backup_settings_grammar = ssh_backup_settings_grammar();
$self->settings_parser('ssh_backup', $ssh_backup_settings_grammar);
}
sub local_backup_settings_parser {
arg_count_or_die(1, 1, @_);
my $self = shift;
my $local_backup_settings_grammar = local_backup_settings_grammar();
$self->settings_parser('local_backup', $local_backup_settings_grammar);
}
####################################
# ERROR ANALYSIS #
####################################
sub check_config {
# Ensure that $config_ref references a valid yabsm configuration. If the
# config is valid return a list containing only the value 1, otherwise
lib/App/Yabsm/Config/Parser.pm view on Meta::CPAN
arg_count_or_die(1, 1, @_);
my $config_ref = shift;
my @error_msgs;
unless ($config_ref->{yabsm_dir}) {
push @error_msgs, q(yabsm: config error: missing required setting 'yabsm_dir');
}
unless ($config_ref->{snaps} || $config_ref->{ssh_backups} || $config_ref->{local_backups}) {
push @error_msgs, 'yabsm: config error: no defined snaps, ssh_backups, or local_backups';
}
push @error_msgs, snap_errors($config_ref);
push @error_msgs, ssh_backup_errors($config_ref);
push @error_msgs, local_backup_errors($config_ref);
if (@error_msgs) {
return (0, @error_msgs);
}
else {
return (1);
}
}
sub snap_errors {
lib/App/Yabsm/Config/Parser.pm view on Meta::CPAN
my @defined_settings = keys %{ $config_ref->{snaps}{$snap} };
my @missing_settings = array_minus(@required_settings, @defined_settings);
foreach my $missing (@missing_settings) {
push @error_msgs, "yabsm: config error: snap '$snap' missing required setting '$missing'";
}
}
return wantarray ? @error_msgs : \@error_msgs;
}
sub ssh_backup_errors {
# Ensure that all the ssh_backups defined in the config referenced by
# $config_ref are not missing required ssh_backup settings and are backing
# up a defined subvol.
arg_count_or_die(1, 1, @_);
my $config_ref = shift;
# return this
my @error_msgs;
# Base required settings. Passing 0 to ssh_backup_settings_grammar excludes
# timeframe settings from the returned hash.
my @base_required_settings = keys %{ ssh_backup_settings_grammar(0) };
foreach my $ssh_backup (keys %{ $config_ref->{ssh_backups} }) {
# Make sure that the subvol being backed up exists
my $subvol = $config_ref->{ssh_backups}{$ssh_backup}{subvol};
if (defined $subvol) {
unless (grep { $subvol eq $_ } keys %{ $config_ref->{subvols} }) {
push @error_msgs, "yabsm: config error: ssh_backup '$ssh_backup' is backing up a non-existent subvol '$subvol'";
}
}
# Make sure all required settings are defined
my @required_settings = @base_required_settings;
my $timeframes = $config_ref->{ssh_backups}{$ssh_backup}{timeframes};
if (defined $timeframes) {
push @required_settings, required_timeframe_settings($timeframes);
}
my @defined_settings = keys %{ $config_ref->{ssh_backups}{$ssh_backup} };
my @missing_settings = array_minus(@required_settings, @defined_settings);
foreach my $missing (@missing_settings) {
push @error_msgs, "yabsm: config error: ssh_backup '$ssh_backup' missing required setting '$missing'";
}
}
return wantarray ? @error_msgs : \@error_msgs;
}
sub local_backup_errors {
# Ensure that all the local_backups defined in the config referenced by
# $config_ref are not missing required local_backup settings and are backing
# up a defined subvol
arg_count_or_die(1, 1, @_);
my $config_ref = shift;
# return this
my @error_msgs;
# Base required settings. Passing 0 to local_backup_settings_grammar
# excludes timeframe settings from the returned hash.
my @base_required_settings = keys %{ local_backup_settings_grammar(0) };
foreach my $local_backup (keys %{ $config_ref->{local_backups} }) {
# Make sure that the subvol being backed up exists
my $subvol = $config_ref->{local_backups}{$local_backup}{subvol};
if (defined $subvol) {
unless (grep { $subvol eq $_ } keys %{ $config_ref->{subvols} }) {
push @error_msgs, "yabsm: config error: local_backup '$local_backup' is backing up a non-existent subvol '$subvol'";
}
}
# Make sure all required settings are defined
my @required_settings = @base_required_settings;
my $timeframes = $config_ref->{local_backups}{$local_backup}{timeframes};
if (defined $timeframes) {
push @required_settings, required_timeframe_settings($timeframes);
}
my @defined_settings = keys %{ $config_ref->{local_backups}{$local_backup} };
my @missing_settings = array_minus(@required_settings, @defined_settings);
foreach my $missing (@missing_settings) {
push @error_msgs, "yabsm: config error: local_backup '$local_backup' missing required setting '$missing'";
}
}
return wantarray ? @error_msgs : \@error_msgs;
}
sub required_timeframe_settings {
# Given a timeframes value like 'hourly,daily,monthly' returns a list of
# required settings. This subroutine is used to dynamically determine what
lib/App/Yabsm/Config/Query.pm view on Meta::CPAN
is_weekday
is_weekday_or_die
time_hour
time_minute
yabsm_dir
yabsm_user_home
subvol_exists
subvol_exists_or_die
snap_exists
snap_exists_or_die
ssh_backup_exists
ssh_backup_exists_or_die
local_backup_exists
local_backup_exists_or_die
backup_exists
backup_exists_or_die
all_subvols
all_snaps
all_ssh_backups
all_local_backups
subvol_mountpoint
snap_subvol
snap_mountpoint
snap_dest
snap_dir
snap_timeframes
ssh_backup_subvol
ssh_backup_mountpoint
ssh_backup_dir
ssh_backup_timeframes
ssh_backup_ssh_dest
local_backup_subvol
local_backup_mountpoint
local_backup_dir
local_backup_timeframes
all_snaps_of_subvol
all_ssh_backups_of_subvol
all_local_backups_of_subvol
snap_wants_timeframe
snap_wants_timeframe_or_die
ssh_backup_wants_timeframe
ssh_backup_wants_timeframe_or_die
local_backup_wants_timeframe
local_backup_wants_timeframe_or_die
snap_timeframe_keep
snap_5minute_keep
snap_hourly_keep
snap_daily_keep
snap_daily_times
snap_weekly_keep
snap_weekly_time
snap_weekly_day
snap_monthly_keep
snap_monthly_time
snap_monthly_day
ssh_backup_timeframe_keep
ssh_backup_5minute_keep
ssh_backup_hourly_keep
ssh_backup_daily_keep
ssh_backup_daily_times
ssh_backup_weekly_keep
ssh_backup_weekly_time
ssh_backup_weekly_day
ssh_backup_monthly_keep
ssh_backup_monthly_time
ssh_backup_monthly_day
local_backup_timeframe_keep
local_backup_5minute_keep
local_backup_hourly_keep
local_backup_daily_keep
local_backup_daily_times
local_backup_weekly_keep
local_backup_weekly_time
local_backup_weekly_day
local_backup_monthly_keep
local_backup_monthly_time
local_backup_monthly_day
);
our %EXPORT_TAGS = ( ALL => [ @EXPORT_OK ] );
####################################
# SUBROUTINES #
####################################
sub is_timeframe {
# Return 1 if given a valid timeframe and return 0 otherwise.
lib/App/Yabsm/Config/Query.pm view on Meta::CPAN
my $snap = shift;
my $config_ref = shift;
unless ( snap_exists($snap, $config_ref) ) {
confess("yabsm: internal error: no snap named '$snap'");
}
return 1;
}
sub ssh_backup_exists {
# Return 1 if $ssh_backup is a ssh_backup defined in $config_ref and return 0
# otherwise.
arg_count_or_die(2, 2, @_);
my $ssh_backup = shift;
my $config_ref = shift;
return 0+(exists $config_ref->{ssh_backups}{$ssh_backup});
}
sub ssh_backup_exists_or_die {
# Wrapper around &ssh_backup_exists that Carp::Confess's if it returns false.
arg_count_or_die(2, 2, @_);
my $ssh_backup = shift;
my $config_ref = shift;
unless ( ssh_backup_exists($ssh_backup, $config_ref) ) {
confess("yabsm: internal error: no ssh_backup named '$ssh_backup'");
}
return 1;
}
sub local_backup_exists {
# Return 1 if $local_backup is a lcoal_backup defined in $config_ref and
# return 0 otherwise.
arg_count_or_die(2, 2, @_);
my $local_backup = shift;
my $config_ref = shift;
return 0+(exists $config_ref->{local_backups}{$local_backup});
}
sub local_backup_exists_or_die {
# Wrapper around &local_backup_exists that Carp::Confess's if it returns
# false.
arg_count_or_die(2, 2, @_);
my $local_backup = shift;
my $config_ref = shift;
unless ( local_backup_exists($local_backup, $config_ref) ) {
confess("yabsm: internal error: no local_backup named '$local_backup'");
}
return 1;
}
sub backup_exists {
# Return 1 if $backup is either an ssh_backup or a local_backup and return 0
# otherwise.
arg_count_or_die(2, 2, @_);
my $backup = shift;
my $config_ref = shift;
return 1 if ssh_backup_exists($backup, $config_ref);
return local_backup_exists($backup, $config_ref);
}
sub backup_exists_or_die {
# Wrapper around &backup_exists that Carp::Confess's if it returns false.
arg_count_or_die(2, 2, @_);
my $backup = shift;
my $config_ref = shift;
unless ( backup_exists($backup, $config_ref) ) {
confess("yabsm: internal error: no ssh_backup or local_backup named '$backup'");
}
return 1;
}
sub all_subvols {
# Return a list of all the subvol names defined in $config_ref.
arg_count_or_die(1, 1, @_);
lib/App/Yabsm/Config/Query.pm view on Meta::CPAN
arg_count_or_die(1, 1, @_);
my $config_ref = shift;
my @snaps = sort keys %{ $config_ref->{snaps} };
return @snaps;
}
sub all_ssh_backups {
# Return a list of all the ssh_backup names defined in $config_ref.
arg_count_or_die(1, 1, @_);
my $config_ref = shift;
my @ssh_backups = sort keys %{ $config_ref->{ssh_backups} };
return @ssh_backups;
}
sub all_local_backups {
# Return a list of all the local_backup names defined in $config_ref.
arg_count_or_die(1, 1, @_);
my $config_ref = shift;
my @all_local_backups = sort keys %{ $config_ref->{local_backups} };
return @all_local_backups;
}
sub subvol_mountpoint {
# Return the the subvol $subvol's mountpoint value.
arg_count_or_die(2, 2, @_);
my $subvol = shift;
my $config_ref = shift;
lib/App/Yabsm/Config/Query.pm view on Meta::CPAN
arg_count_or_die(2, 2, @_);
my $snap = shift;
my $config_ref = shift;
snap_exists_or_die($snap, $config_ref);
return sort split ',', $config_ref->{snaps}{$snap}{timeframes};
}
sub ssh_backup_subvol {
# Return the name of the subvol that $ssh_backup is backing up.
arg_count_or_die(2, 2, @_);
my $ssh_backup = shift;
my $config_ref = shift;
ssh_backup_exists_or_die($ssh_backup, $config_ref);
return $config_ref->{ssh_backups}{$ssh_backup}{subvol};
}
sub ssh_backup_mountpoint {
# Return the mountpoint of the subvol that $ssh_backup is backing up.
arg_count_or_die(2, 2, @_);
my $ssh_backup = shift;
my $config_ref = shift;
ssh_backup_exists_or_die($ssh_backup, $config_ref);
my $subvol = ssh_backup_subvol($ssh_backup, $config_ref);
return subvol_mountpoint($subvol, $config_ref);
}
sub ssh_backup_dir {
# Return $ssh_backup's ssh_backup dir value. Optionally pass a timeframe via
# the $tframe value to append "/$tframe" to the returned dir.
arg_count_or_die(3, 3, @_);
my $ssh_backup = shift;
my $tframe = shift;
my $config_ref = shift;
ssh_backup_exists_or_die($ssh_backup, $config_ref);
my $dir = $config_ref->{ssh_backups}{$ssh_backup}{dir} =~ s/\/+$//r;
if ($tframe) {
ssh_backup_wants_timeframe_or_die($ssh_backup, $tframe, $config_ref);
return "$dir/$tframe";
}
else {
return $dir;
}
}
sub ssh_backup_timeframes {
# Return a list of $ssh_backups's timeframes.
arg_count_or_die(2, 2, @_);
my $ssh_backup = shift;
my $config_ref = shift;
ssh_backup_exists_or_die($ssh_backup, $config_ref);
return sort split ',', $config_ref->{ssh_backups}{$ssh_backup}{timeframes};
}
sub ssh_backup_ssh_dest {
# Return $ssh_backup's ssh_dest value.
arg_count_or_die(2, 2, @_);
my $ssh_backup = shift;
my $config_ref = shift;
ssh_backup_exists_or_die($ssh_backup, $config_ref);
return $config_ref->{ssh_backups}{$ssh_backup}{ssh_dest};
}
sub local_backup_subvol {
# Return the name of the subvol that $local_backup is backing up.
arg_count_or_die(2, 2, @_);
my $local_backup = shift;
my $config_ref = shift;
local_backup_exists_or_die($local_backup, $config_ref);
return $config_ref->{local_backups}{$local_backup}{subvol};
}
sub local_backup_mountpoint {
# Return the mountpoint of the subvol that $local_backup is backing up.
arg_count_or_die(2, 2, @_);
my $local_backup = shift;
my $config_ref = shift;
local_backup_exists_or_die($local_backup, $config_ref);
my $subvol = local_backup_subvol($local_backup, $config_ref);
return subvol_mountpoint($subvol, $config_ref);
}
sub local_backup_dir {
# Return $local_backup's local_backup dir value. Optionally pass a timeframe
# via the $tframe value to append "/$tframe" to the returned dir.
arg_count_or_die(3, 3, @_);
my $local_backup = shift;
my $tframe = shift;
my $config_ref = shift;
local_backup_exists_or_die($local_backup, $config_ref);
my $dir = $config_ref->{local_backups}{$local_backup}{dir} =~ s/\/+$//r;
if ($tframe) {
local_backup_wants_timeframe_or_die($local_backup, $tframe, $config_ref);
return "$dir/$tframe";
}
else {
return $dir;
}
}
sub local_backup_timeframes {
# Return a list of $local_backups's timeframes.
arg_count_or_die(2, 2, @_);
my $local_backup = shift;
my $config_ref = shift;
local_backup_exists_or_die($local_backup, $config_ref);
return sort split ',', $config_ref->{local_backups}{$local_backup}{timeframes};
}
sub all_snaps_of_subvol {
# Return a list of all the snaps in $config_ref that are snapshotting
# $subvol.
arg_count_or_die(2, 2, @_);
my $subvol = shift;
lib/App/Yabsm/Config/Query.pm view on Meta::CPAN
my @snaps;
for my $snap ( all_snaps($config_ref) ) {
push @snaps, $snap
if ($subvol eq $config_ref->{snaps}{$snap}{subvol});
}
return sort @snaps;
}
sub all_ssh_backups_of_subvol {
# Return a list of all the ssh_backups in $config_ref that are backing up
# $subvol.
arg_count_or_die(2, 2, @_);
my $subvol = shift;
my $config_ref = shift;
my @ssh_backups;
for my $ssh_backup ( all_ssh_backups($config_ref) ) {
push @ssh_backups, $ssh_backup
if ($subvol eq $config_ref->{ssh_backups}{$ssh_backup}{subvol});
}
return sort @ssh_backups;
}
sub all_local_backups_of_subvol {
# Return a list of all the local_backups in $config_ref that are backing up
# $subvol.
arg_count_or_die(2, 2, @_);
my $subvol = shift;
my $config_ref = shift;
my @local_backups;
for my $local_backup ( all_local_backups($config_ref) ) {
push @local_backups, $local_backup
if ($subvol eq $config_ref->{local_backups}{$local_backup}{subvol});
}
return sort @local_backups;
}
sub snap_wants_timeframe {
# Return 1 if the snap $snap wants snapshots in timeframe $tframe and return
# 0 otherwise;
arg_count_or_die(3, 3, @_);
my $snap = shift;
lib/App/Yabsm/Config/Query.pm view on Meta::CPAN
my $tframe = shift;
my $config_ref = shift;
unless ( snap_wants_timeframe($snap, $tframe, $config_ref) ) {
confess("yabsm: internal error: snap '$snap' is not taking $tframe snapshots");
}
return 1;
}
sub ssh_backup_wants_timeframe {
# Return 1 if the ssh_backup $ssh_backup wants backups in timeframe $tframe
# and return 0 otherwise.
arg_count_or_die(3, 3, @_);
my $ssh_backup = shift;
my $tframe = shift;
my $config_ref = shift;
ssh_backup_exists_or_die($ssh_backup, $config_ref);
is_timeframe_or_die($tframe);
return 1 if grep { $tframe eq $_ } ssh_backup_timeframes($ssh_backup, $config_ref);
return 0;
}
sub ssh_backup_wants_timeframe_or_die {
# Wrapper around &ssh_backup_wants_timeframe that Carp::Confess's if it
# returns false.
arg_count_or_die(3, 3, @_);
my $ssh_backup = shift;
my $tframe = shift;
my $config_ref = shift;
unless ( ssh_backup_wants_timeframe($ssh_backup, $tframe, $config_ref) ) {
confess("yabsm: internal error: ssh_backup '$ssh_backup' is not taking $tframe backups");
}
return 1;
}
sub local_backup_wants_timeframe {
# Return 1 if the local_backup $local_backup wants backups in timeframe
# $tframe and return 0 otherwise.
arg_count_or_die(3, 3, @_);
my $local_backup = shift;
my $tframe = shift;
my $config_ref = shift;
local_backup_exists_or_die($local_backup, $config_ref);
is_timeframe_or_die($tframe);
return 1 if grep { $tframe eq $_ } local_backup_timeframes($local_backup, $config_ref);
return 0;
}
sub local_backup_wants_timeframe_or_die {
# Wrapper around &local_backup_wants_timeframe that Carp::Confess's if it
# returns false.
arg_count_or_die(3, 3, @_);
my $local_backup = shift;
my $tframe = shift;
my $config_ref = shift;
unless ( local_backup_wants_timeframe($local_backup, $tframe, $config_ref) ) {
confess("yabsm: internal error: local_backup '$local_backup' is not taking $tframe backups");
}
return 1;
}
sub snap_timeframe_keep {
# Return snap $snap's ${tframe}_keep value.
arg_count_or_die(3, 3, @_);
lib/App/Yabsm/Config/Query.pm view on Meta::CPAN
my $snap = shift;
my $config_ref = shift;
snap_exists_or_die($snap, $config_ref);
snap_wants_timeframe_or_die($snap, 'monthly', $config_ref);
return $config_ref->{snaps}{$snap}{monthly_day};
}
sub ssh_backup_timeframe_keep {
# Return ssh_backup $ssh_backup's ${tframe}_keep value.
arg_count_or_die(3, 3, @_);
my $ssh_backup = shift;
my $tframe = shift;
my $config_ref = shift;
ssh_backup_exists_or_die($ssh_backup, $config_ref);
is_timeframe_or_die($tframe);
$tframe eq '5minute' and return ssh_backup_5minute_keep($ssh_backup, $config_ref);
$tframe eq 'hourly' and return ssh_backup_hourly_keep($ssh_backup, $config_ref);
$tframe eq 'daily' and return ssh_backup_daily_keep($ssh_backup, $config_ref);
$tframe eq 'weekly' and return ssh_backup_weekly_keep($ssh_backup, $config_ref);
$tframe eq 'monthly' and return ssh_backup_monthly_keep($ssh_backup, $config_ref);
}
sub ssh_backup_5minute_keep {
# Return ssh_backup $ssh_backup's 5minute_keep value.
arg_count_or_die(2, 2, @_);
my $ssh_backup = shift;
my $config_ref = shift;
ssh_backup_exists_or_die($ssh_backup, $config_ref);
ssh_backup_wants_timeframe_or_die($ssh_backup, '5minute', $config_ref);
return $config_ref->{ssh_backups}{$ssh_backup}{'5minute_keep'};
}
sub ssh_backup_hourly_keep {
# Return ssh_backup $ssh_backup's hourly_keep value.
arg_count_or_die(2, 2, @_);
my $ssh_backup = shift;
my $config_ref = shift;
ssh_backup_exists_or_die($ssh_backup, $config_ref);
ssh_backup_wants_timeframe_or_die($ssh_backup, 'hourly', $config_ref);
return $config_ref->{ssh_backups}{$ssh_backup}{hourly_keep};
}
sub ssh_backup_daily_keep {
# Return ssh_backup $ssh_backup's daily_keep value.
arg_count_or_die(2, 2, @_);
my $ssh_backup = shift;
my $config_ref = shift;
ssh_backup_exists_or_die($ssh_backup, $config_ref);
ssh_backup_wants_timeframe_or_die($ssh_backup, 'daily', $config_ref);
return $config_ref->{ssh_backups}{$ssh_backup}{daily_keep};
}
sub ssh_backup_daily_times {
# Return a list of ssh_backup $ssh_backup's daily_times values.
arg_count_or_die(2, 2, @_);
my $ssh_backup = shift;
my $config_ref = shift;
ssh_backup_exists_or_die($ssh_backup, $config_ref);
ssh_backup_wants_timeframe_or_die($ssh_backup, 'daily', $config_ref);
my @times = split ',', $config_ref->{ssh_backups}{$ssh_backup}{daily_times};
# removes duplicates
@times = sort keys %{{ map { $_ => 1 } @times }};
return @times;
}
sub ssh_backup_weekly_keep {
# Return ssh_backup $ssh_backup's weekly_keep value.
arg_count_or_die(2, 2, @_);
my $ssh_backup = shift;
my $config_ref = shift;
ssh_backup_exists_or_die($ssh_backup, $config_ref);
ssh_backup_wants_timeframe_or_die($ssh_backup, 'weekly', $config_ref);
return $config_ref->{ssh_backups}{$ssh_backup}{weekly_keep};
}
sub ssh_backup_weekly_time {
# Return ssh_backup $ssh_backup's weekly_time value.
arg_count_or_die(2, 2, @_);
my $ssh_backup = shift;
my $config_ref = shift;
ssh_backup_exists_or_die($ssh_backup, $config_ref);
ssh_backup_wants_timeframe_or_die($ssh_backup, 'weekly', $config_ref);
return $config_ref->{ssh_backups}{$ssh_backup}{weekly_time};
}
sub ssh_backup_weekly_day {
# Return ssh_backup $ssh_backup's weekly_day value.
arg_count_or_die(2, 2, @_);
my $ssh_backup = shift;
my $config_ref = shift;
ssh_backup_exists_or_die($ssh_backup, $config_ref);
ssh_backup_wants_timeframe_or_die($ssh_backup, 'weekly', $config_ref);
return $config_ref->{ssh_backups}{$ssh_backup}{weekly_day};
}
sub ssh_backup_monthly_keep {
# Return ssh_backup $ssh_backup's monthly_keep value.
arg_count_or_die(2, 2, @_);
my $ssh_backup = shift;
my $config_ref = shift;
ssh_backup_exists_or_die($ssh_backup, $config_ref);
ssh_backup_wants_timeframe_or_die($ssh_backup, 'monthly', $config_ref);
return $config_ref->{ssh_backups}{$ssh_backup}{monthly_keep};
}
sub ssh_backup_monthly_time {
# Return ssh_backup $ssh_backup's monthly_time value.
arg_count_or_die(2, 2, @_);
my $ssh_backup = shift;
my $config_ref = shift;
ssh_backup_exists_or_die($ssh_backup, $config_ref);
ssh_backup_wants_timeframe_or_die($ssh_backup, 'monthly', $config_ref);
return $config_ref->{ssh_backups}{$ssh_backup}{monthly_time};
}
sub ssh_backup_monthly_day {
# Return ssh_backup $ssh_backup's monthly_day value.
arg_count_or_die(2, 2, @_);
my $ssh_backup = shift;
my $config_ref = shift;
ssh_backup_exists_or_die($ssh_backup, $config_ref);
ssh_backup_wants_timeframe_or_die($ssh_backup, 'monthly', $config_ref);
return $config_ref->{ssh_backups}{$ssh_backup}{monthly_day};
}
sub local_backup_timeframe_keep {
# Return local_backup $local_backup's ${tframe}_keep value.
arg_count_or_die(3, 3, @_);
my $local_backup = shift;
my $tframe = shift;
my $config_ref = shift;
local_backup_exists_or_die($local_backup, $config_ref);
is_timeframe_or_die($tframe);
$tframe eq '5minute' and return local_backup_5minute_keep($local_backup, $config_ref);
$tframe eq 'hourly' and return local_backup_hourly_keep($local_backup, $config_ref);
$tframe eq 'daily' and return local_backup_daily_keep($local_backup, $config_ref);
$tframe eq 'weekly' and return local_backup_weekly_keep($local_backup, $config_ref);
$tframe eq 'monthly' and return local_backup_monthly_keep($local_backup, $config_ref);
}
sub local_backup_5minute_keep {
# Return local_backup $local_backup's 5minute_keep value.
arg_count_or_die(2, 2, @_);
my $local_backup = shift;
my $config_ref = shift;
local_backup_exists_or_die($local_backup, $config_ref);
local_backup_wants_timeframe_or_die($local_backup, '5minute', $config_ref);
return $config_ref->{local_backups}{$local_backup}{'5minute_keep'};
}
sub local_backup_hourly_keep {
# Return local_backup $local_backup's hourly_keep value.
arg_count_or_die(2, 2, @_);
my $local_backup = shift;
my $config_ref = shift;
local_backup_exists_or_die($local_backup, $config_ref);
local_backup_wants_timeframe_or_die($local_backup, 'hourly', $config_ref);
return $config_ref->{local_backups}{$local_backup}{hourly_keep};
}
sub local_backup_daily_keep {
# Return local_backup $local_backup's daily_keep value.
arg_count_or_die(2, 2, @_);
my $local_backup = shift;
my $config_ref = shift;
local_backup_exists_or_die($local_backup, $config_ref);
local_backup_wants_timeframe_or_die($local_backup, 'daily', $config_ref);
return $config_ref->{local_backups}{$local_backup}{daily_keep};
}
sub local_backup_daily_times {
# Return a list of local_backup $local_backup's daily_times values.
arg_count_or_die(2, 2, @_);
my $local_backup = shift;
my $config_ref = shift;
local_backup_exists_or_die($local_backup, $config_ref);
local_backup_wants_timeframe_or_die($local_backup, 'daily', $config_ref);
my @times = split ',', $config_ref->{local_backups}{$local_backup}{daily_times};
# removes duplicates
@times = sort keys %{{ map { $_ => 1 } @times }};
return @times;
}
sub local_backup_weekly_keep {
# Return local_backup $local_backup's weekly_keep value.
arg_count_or_die(2, 2, @_);
my $local_backup = shift;
my $config_ref = shift;
local_backup_exists_or_die($local_backup, $config_ref);
local_backup_wants_timeframe_or_die($local_backup, 'weekly', $config_ref);
return $config_ref->{local_backups}{$local_backup}{weekly_keep};
}
sub local_backup_weekly_time {
# Return local_backup $local_backup's weekly_time value.
arg_count_or_die(2, 2, @_);
my $local_backup = shift;
my $config_ref = shift;
local_backup_exists_or_die($local_backup, $config_ref);
local_backup_wants_timeframe_or_die($local_backup, 'weekly', $config_ref);
return $config_ref->{local_backups}{$local_backup}{weekly_time};
}
sub local_backup_weekly_day {
# Return local_backup $local_backup's weekly_day value.
arg_count_or_die(2, 2, @_);
my $local_backup = shift;
my $config_ref = shift;
local_backup_exists_or_die($local_backup, $config_ref);
local_backup_wants_timeframe_or_die($local_backup, 'weekly', $config_ref);
return $config_ref->{local_backups}{$local_backup}{weekly_day};
}
sub local_backup_monthly_keep {
# Return local_backup $local_backup's monthly_keep value.
arg_count_or_die(2, 2, @_);
my $local_backup = shift;
my $config_ref = shift;
local_backup_exists_or_die($local_backup, $config_ref);
local_backup_wants_timeframe_or_die($local_backup, 'monthly', $config_ref);
return $config_ref->{local_backups}{$local_backup}{monthly_keep};
}
sub local_backup_monthly_time {
# Return local_backup $local_backup's monthly_time value.
arg_count_or_die(2, 2, @_);
my $local_backup = shift;
my $config_ref = shift;
local_backup_exists_or_die($local_backup, $config_ref);
local_backup_wants_timeframe_or_die($local_backup, 'monthly', $config_ref);
return $config_ref->{local_backups}{$local_backup}{monthly_time};
}
sub local_backup_monthly_day {
# Return local_backup $local_backup's monthly_day value.
arg_count_or_die(2, 2, @_);
my $local_backup = shift;
my $config_ref = shift;
local_backup_exists_or_die($local_backup, $config_ref);
local_backup_wants_timeframe_or_die($local_backup, 'monthly', $config_ref);
return $config_ref->{local_backups}{$local_backup}{monthly_day};
}
1;
t/GenericBackup.t view on Meta::CPAN
is_btrfs_subvolume($BTRFS_SUBVOLUME) or plan skip_all => "'$BTRFS_SUBVOLUME' is not a btrfs subvolume";
my $BTRFS_DIR = tempdir( 'yabsm-GenericBackup.t-tmpXXXXXX', DIR => $BTRFS_SUBVOLUME, CLEANUP => 1 );
####################################
# TEST CONFIG #
####################################
my %TEST_CONFIG = ( yabsm_dir => $BTRFS_DIR
, subvols => { foo => { mountpoint => $BTRFS_SUBVOLUME } }
, ssh_backups => { foo_ssh_backup => { subvol => 'foo'
, ssh_dest => 'yabsm-test@localhost'
, dir => '/bar'
, timeframes => '5minute'
, '5minute_keep' => 12
}
}
, local_backups => { foo_local_backup => { subvol => 'foo'
, dir => '/baz'
, timeframes => '5minute'
, '5minute_keep' => 12
}
}
);
####################################
# TESTS #
####################################
{
my $n = 'is_backup_type_or_die';
my $f = \&App::Yabsm::Backup::Generic::is_backup_type_or_die;
lives_and { is $f->('ssh'), 1 } "$n - 1 if passed 'ssh'";
lives_and { is $f->('local'), 1 } "$n - 1 if passed 'local'";
throws_ok { $f->('quux') } qr/'quux' is not 'ssh' or 'local'/, "$n - dies if not 'ssh' or 'local'";
}
# BOOTSTRAP SNAPSHOT TESTS
{
my $n = 'bootstrap_snapshot_dir';
my $f = \&App::Yabsm::Backup::Generic::bootstrap_snapshot_dir;
my $expected_bootstrap_dir = "$BTRFS_DIR/.yabsm-var/ssh_backups/foo_ssh_backup/bootstrap-snapshot";
lives_and { is $f->('foo_ssh_backup', 'ssh', \%TEST_CONFIG), $expected_bootstrap_dir } "$n - returns correct bootstrap dir";
throws_ok { $f->('foo_ssh_backup', 'ssh', \%TEST_CONFIG, DIE_UNLESS_EXISTS => 1) } qr/no directory '$expected_bootstrap_dir' that is readable by user 'root'/, "$n - if DIE_UNLESS_EXISTS dies if bootstrap dir doesn't exist";
make_path_or_die($expected_bootstrap_dir);
lives_and { is $f->('foo_ssh_backup', 'ssh', \%TEST_CONFIG, DIE_UNLESS_EXISTS => 1), $expected_bootstrap_dir} "$n - returns correct directory if dir exists and DIE_UNLESS_EXISTS";
$expected_bootstrap_dir = "$BTRFS_DIR/.yabsm-var/local_backups/foo_local_backup/bootstrap-snapshot";
make_path_or_die($expected_bootstrap_dir);
lives_and { is $f->('foo_local_backup', 'local', \%TEST_CONFIG, DIE_UNLESS_EXISTS => 1), $expected_bootstrap_dir } "$n - returns correct directory for local_backup";
throws_ok { $f->('foo_ssh_backup', 'quux', \%TEST_CONFIG) } qr/'quux' is not 'ssh' or 'local'/, "$n - dies if invalid backup type";
}
{
my $n;
my $f;
my $bootstrap_dir = App::Yabsm::Backup::Generic::bootstrap_snapshot_dir('foo_local_backup', 'local', \%TEST_CONFIG);
$n = 'the_local_bootstrap_snapshot';
$f = \&App::Yabsm::Backup::Generic::the_local_bootstrap_snapshot;
lives_and { is $f->('foo_local_backup', 'local', \%TEST_CONFIG), undef } "$n - returns undef if there is no bootstrap snapshot";
$n = 'take_bootstrap_snapshot';
$f = \&App::Yabsm::Backup::Generic::take_bootstrap_snapshot;
my $bootstrap_snapshot = $bootstrap_dir . '/.BOOTSTRAP-' . App::Yabsm::Snapshot::current_time_snapshot_name();
lives_and { is $f->('foo_local_backup', 'local', \%TEST_CONFIG), $bootstrap_snapshot } "$n - takes bootstrap snapshot";
sleep 60;
$bootstrap_snapshot = $bootstrap_dir . '/.BOOTSTRAP-' . App::Yabsm::Snapshot::current_time_snapshot_name();
lives_and { is $f->('foo_local_backup', 'local', \%TEST_CONFIG), $bootstrap_snapshot } "$n - takes second bootstrap snapshot";
opendir my $dh, $bootstrap_dir or die "error: cannot opendir '$bootstrap_dir'\n";
my @boot_snaps = grep { App::Yabsm::Snapshot::is_snapshot_name($_, ONLY_BOOTSTRAP => 1) } readdir($dh);
closedir $dh;
ok(1 == @boot_snaps && $boot_snaps[0] eq basename($bootstrap_snapshot), "$n - taking second bootstrap snapshot deletes the old bootstrap snapshot");
$n = 'the_local_bootstrap_snapshot';
$f = \&App::Yabsm::Backup::Generic::the_local_bootstrap_snapshot;
lives_and { is $f->('foo_local_backup', 'local', \%TEST_CONFIG), $bootstrap_snapshot } "$n - return the bootstrap snapshot";
sleep 60;
$n = 'maybe_take_bootstrap_snapshot';
$f = \&App::Yabsm::Backup::Generic::maybe_take_bootstrap_snapshot;
lives_and { is $f->('foo_local_backup', 'local', \%TEST_CONFIG), $bootstrap_snapshot } "$n - doesn't take bootstrap snapshot if it already exists";
App::Yabsm::Snapshot::delete_snapshot($bootstrap_snapshot);
$n = 'bootstrap_lock_file';
$f = \&App::Yabsm::Backup::Generic::bootstrap_lock_file;
lives_and { is $f->('foo_local_backup', 'local', \%TEST_CONFIG), undef } "$n - returns undef in no lock file exists";
$n = 'create_bootstrap_lock_file';
$f = \&App::Yabsm::Backup::Generic::create_bootstrap_lock_file;
lives_and {
my $lock_fh = $f->('foo_local_backup', 'local', \%TEST_CONFIG);
ok $lock_fh->filename =~ /BOOTSTRAP-LOCK/;
throws_ok { $f->('foo_local_backup', 'local', \%TEST_CONFIG) } qr/local_backup 'foo_local_backup' is already locked out of performing a bootstrap/, "$n - dies if bootstrap lock already exists";
$n = 'bootstrap_lock_file';
$f = \&App::Yabsm::Backup::Generic::bootstrap_lock_file;
lives_and { ok $f->('foo_local_backup', 'local', \%TEST_CONFIG) =~ /BOOTSTRAP-LOCK/ } "$n - returns correct lock file";
} "$n - bootstrap lock file functions";
}
# TMP SNAPSHOT TESTS
{
my $n;
my $f;
$n = 'tmp_snapshot_dir';
$f = \&App::Yabsm::Backup::Generic::tmp_snapshot_dir;
my $tmp_snapshot_dir = "$BTRFS_DIR/.yabsm-var/local_backups/foo_local_backup/tmp-snapshot/5minute";
lives_and { $f->('foo_local_backup', 'local', '5minute', \%TEST_CONFIG), $tmp_snapshot_dir } "$n - returns path even if tmp dir doesn't exist";
throws_ok { $f->('foo_local_backup', 'local', '5minute', \%TEST_CONFIG, DIE_UNLESS_EXISTS=>1) } qr/no directory '$tmp_snapshot_dir'/, "$n - dies if tmp dir doesn't exist and DIE_UNLESS_EXISTS";
$n = 'take_tmp_snapshot';
$f = \&App::Yabsm::Backup::Generic::take_tmp_snapshot;
throws_ok { $f->('foo_local_backup', 'local', '5minute', \%TEST_CONFIG) } qr/no directory '$tmp_snapshot_dir'/, "$n - dies if tmp dir doesn't exist";
make_path_or_die($tmp_snapshot_dir);
$n = 'tmp_snapshot_dir';
$f = \&App::Yabsm::Backup::Generic::tmp_snapshot_dir;
lives_and { is $f->('foo_local_backup', 'local', '5minute', \%TEST_CONFIG, DIE_UNLESS_EXISTS=>1), $tmp_snapshot_dir } "$n - lives and returns correct dir if it exists and DIE_UNLESS_EXISTS";
$n = 'take_tmp_snapshot';
$f = \&App::Yabsm::Backup::Generic::take_tmp_snapshot;
my $tmp_snapshot = "$tmp_snapshot_dir/".App::Yabsm::Snapshot::current_time_snapshot_name();
lives_and { is $f->('foo_local_backup', 'local', '5minute', \%TEST_CONFIG), $tmp_snapshot } "$n - takes tmp snapshot";
sleep 60;
$tmp_snapshot = "$tmp_snapshot_dir/".App::Yabsm::Snapshot::current_time_snapshot_name();
lives_and { is $f->('foo_local_backup', 'local', '5minute', \%TEST_CONFIG), $tmp_snapshot } "$n - takes tmp snapshot even if one exists";
opendir my $dh, $tmp_snapshot_dir or die "error: cannot opendir '$tmp_snapshot_dir'\n";
my @tmp_snaps = grep { App::Yabsm::Snapshot::is_snapshot_name($_) } readdir($dh);
map { $_ = "$tmp_snapshot_dir/$_" } @tmp_snaps;
closedir $dh;
ok (1 == @tmp_snaps, "$n - deletes old tmp snapshot");
App::Yabsm::Snapshot::delete_snapshot($_) for @tmp_snaps;
}
t/LocalBackup.t view on Meta::CPAN
i_am_root() or plan skip_all => 'Must be root user';
my $BTRFS_DIR = tempdir( 'yabsm-SSH.t-tmpXXXXXX', DIR => $BTRFS_SUBVOLUME, CLEANUP => 1 );
####################################
# TEST CONFIG #
####################################
my %TEST_CONFIG = ( yabsm_dir => "$BTRFS_DIR"
, subvols => { foo => { mountpoint => $BTRFS_SUBVOLUME } }
, local_backups => { foo_local_backup => { subvol => 'foo'
, dir => "$BTRFS_DIR/foo_local_backup"
, timeframes => '5minute'
, '5minute_keep' => 1
}
}
);
my $BACKUP_DIR = App::Yabsm::Config::Query::local_backup_dir('foo_local_backup', '5minute', \%TEST_CONFIG);
my $BACKUP_DIR_BASE = dirname($BACKUP_DIR);
my $BOOTSTRAP_DIR = App::Yabsm::Backup::Generic::bootstrap_snapshot_dir('foo_local_backup','local',\%TEST_CONFIG);
my $TMP_DIR = App::Yabsm::Backup::Generic::tmp_snapshot_dir('foo_local_backup','local','5minute',\%TEST_CONFIG);
my $BACKUP = "$BACKUP_DIR/" . App::Yabsm::Snapshot::current_time_snapshot_name();
make_path_or_die($BACKUP_DIR);
make_path_or_die($BOOTSTRAP_DIR);
make_path_or_die($TMP_DIR);
####################################
# TESTS #
####################################
my $n;
my $f;
my $lock_file = App::Yabsm::Backup::Generic::create_bootstrap_lock_file('foo_local_backup', 'local', \%TEST_CONFIG);
$n = 'do_local_backup_bootstrap';
$f = \&App::Yabsm::Backup::Local::do_local_backup_bootstrap;
lives_and { is $f->('foo_local_backup', \%TEST_CONFIG), undef } "$n - returns undef if bootstrap lock file exists";
unlink $lock_file;
my $expected_boot_snap = "$BOOTSTRAP_DIR/.BOOTSTRAP-".App::Yabsm::Snapshot::current_time_snapshot_name();
lives_and { is $f->('foo_local_backup', \%TEST_CONFIG), $expected_boot_snap } "$n - performs successful bootstrap";
$n = 'the_remote_bootstrap_snapshot';
$f = \&App::Yabsm::Backup::Local::the_remote_bootstrap_snapshot;
lives_and { is $f->('foo_local_backup', \%TEST_CONFIG), "$BACKUP_DIR_BASE/".basename($expected_boot_snap) } "$n - returns correct remote boot snap";
$n = 'maybe_do_local_backup_bootstrap';
$f = \&App::Yabsm::Backup::Local::maybe_do_local_backup_bootstrap;
sleep 60;
lives_and { is $f->('foo_local_backup', \%TEST_CONFIG), $expected_boot_snap } "$n - doesn't redo bootstrap";
$n = 'do_local_backup';
$f = \&App::Yabsm::Backup::Local::do_local_backup;
my $expected_backup = "$BACKUP_DIR/".App::Yabsm::Snapshot::current_time_snapshot_name();
lives_and { is $f->('foo_local_backup', '5minute', \%TEST_CONFIG), $expected_backup } "$n - performs backup";
done_testing();
####################################
# CLEANUP #
####################################
sub cleanup_snapshots {
opendir(my $dh, $BACKUP_DIR_BASE) if -d $BACKUP_DIR_BASE;
monthly_keep=12
}
snap root_snap {
subvol=root
timeframes=hourly,daily
hourly_keep=72
daily_times=07:03
daily_keep=14
}
ssh_backup root_my_server {
subvol=root
ssh_dest=nick@192.168.1.37
dir=/backups/btrfs/yabsm/desktop_root
timeframes=5minute,hourly
5minute_keep=24
hourly_keep=24
}
local_backup home_external_drive {
subvol=home
dir=/mnt/backup_drive/yabsm/desktop_home
timeframes=hourly
hourly_keep=48
}
END_CONFIG
my %expected_config = (
yabsm_dir => '/.snapshots/yabsm',
subvols => {
root => {
'mountpoint' => '/'
},
home => {
'mountpoint' => '/home'
}
},
local_backups => {
home_external_drive => {
subvol => 'home',
hourly_keep => '48',
timeframes => 'hourly',
dir => '/mnt/backup_drive/yabsm/desktop_home'
}
},
ssh_backups => {
root_my_server => {
'5minute_keep' => '24',
subvol => 'root',
hourly_keep => '24',
ssh_dest => 'nick@192.168.1.37',
timeframes => '5minute,hourly',
dir => '/backups/btrfs/yabsm/desktop_root'
}
},
snaps => {
home_snap => {
monthly_day => '31',
subvol => 'home',
daily_times => '23:59,12:30',
hourly_keep => '48',
monthly_time => '23:59',
monthly_keep => '12',
}
, bar_snap => { subvol => 'bar'
, timeframes => '5minute'
, '5minute_keep' => '24'
}
, baz_snap => { subvol => 'baz'
, timeframes => 'hourly'
, hourly_keep => '24'
}
}
, ssh_backups => { foo_ssh_backup => { subvol => 'foo'
, ssh_dest => 'localhost'
, dir => '/foo'
, timeframes => '5minute,hourly,daily,weekly,monthly'
, '5minute_keep' => 36
, hourly_keep => 48
, daily_keep => 365
, weekly_keep => 56
, monthly_keep => 12
, daily_times => '23:59,12:30,12:30'
, weekly_day => 'wednesday'
, weekly_time => '00:00'
, monthly_day => 31
, monthly_time => '23:59'
}
, bar_ssh_backup => { subvol => 'bar'
, ssh_dest => 'localhost'
, dir => '/bar'
, timeframes => 'hourly'
, hourly_keep => 14
}
, baz_ssh_backup => { subvol => 'baz'
, ssh_dest => 'localhost'
, dir => '/baz'
, timeframes => 'daily'
, daily_keep => 14
, daily_times => '23:59'
}
}
, local_backups => { foo_local_backup => { subvol => 'foo'
, dir => '/foo'
, timeframes => '5minute,hourly,daily,weekly,monthly'
, '5minute_keep' => 36
, hourly_keep => 48
, daily_keep => 365
, weekly_keep => 56
, monthly_keep => 12
, daily_times => '23:59,12:30,12:30'
, weekly_day => 'wednesday'
, weekly_time => '00:00'
, monthly_day => 31
, monthly_time => '23:59'
}
, bar_local_backup => { subvol => 'bar'
, dir => '/bar'
, timeframes => 'weekly'
, weekly_keep => 56
, weekly_day => 'monday'
, weekly_time => '00:00'
}
, baz_local_backup => { subvol => 'baz'
, dir => '/baz'
, timeframes => 'monthly'
, monthly_keep => 12
, monthly_day => '30'
, monthly_time => '00:00'
}
}
);
####################################
{
my $n = 'snap_exists_or_die';
my $f = \&App::Yabsm::Config::Query::snap_exists_or_die;
is($f->('foo_snap', \%TEST_CONFIG), 1, "$n - 1 when snap exists");
throws_ok { $f->('quux', \%TEST_CONFIG) } qr/no snap named 'quux'/, "$n - dies if snap doesn't exist";
}
{
my $n = 'ssh_backup_exists';
my $f = \&App::Yabsm::Config::Query::ssh_backup_exists;
is($f->('foo_ssh_backup', \%TEST_CONFIG), 1, "$n - 1 when ssh_backup exists");
is($f->('quux', \%TEST_CONFIG), 0, "$n - 0 when ssh_backup doesn't exist");
}
{
my $n = 'ssh_backup_exists_or_die';
my $f = \&App::Yabsm::Config::Query::ssh_backup_exists_or_die;
is($f->('foo_ssh_backup', \%TEST_CONFIG), 1, "$n - 1 when ssh_backup exists");
throws_ok { $f->('quux', \%TEST_CONFIG) } qr/no ssh_backup named 'quux'/, "$n - dies if ssh_backup doesn't exist";
}
{
my $n = 'local_backup_exists';
my $f = \&App::Yabsm::Config::Query::local_backup_exists;
is($f->('foo_local_backup', \%TEST_CONFIG), 1, "$n - 1 when local_backup exists");
is($f->('quux', \%TEST_CONFIG), 0, "$n - 0 when local_backup doesn't exist");
}
{
my $n = 'local_backup_exists_or_die';
my $f = \&App::Yabsm::Config::Query::local_backup_exists_or_die;
is($f->('foo_local_backup', \%TEST_CONFIG), 1, "$n - 1 when local_backup exists");
throws_ok { $f->('quux', \%TEST_CONFIG) } qr/no local_backup named 'quux'/, "$n - dies if local_backup doesn't exist";
}
{
my $n = 'backup_exists';
my $f = \&App::Yabsm::Config::Query::backup_exists;
is($f->('foo_ssh_backup', \%TEST_CONFIG), 1, "$n - 1 if given ssh_backup");
is($f->('foo_local_backup', \%TEST_CONFIG), 1, "$n - 1 if given local_backup");
is($f->('quux', \%TEST_CONFIG), 0, "$n - 0 if given neither ssh_backup or local_backup");
}
{
my $n = 'backup_exists_or_die';
my $f = \&App::Yabsm::Config::Query::backup_exists_or_die;
is($f->('foo_ssh_backup', \%TEST_CONFIG), 1, "$n - 1 if given ssh_backup");
is($f->('foo_local_backup', \%TEST_CONFIG), 1, "$n - 1 if given local_backup");
throws_ok { $f->('quux', \%TEST_CONFIG) } qr/no ssh_backup or local_backup named 'quux'/, "$n - dies if neither ssh_backup or local_backup";
}
{
my $n = 'all_subvols';
my $f = \&App::Yabsm::Config::Query::all_subvols;
is_deeply([ $f->(\%TEST_CONFIG) ], ['bar','baz','foo'], "$n - correct subvol name list");
}
{
my $n = 'all_snaps';
my $f = \&App::Yabsm::Config::Query::all_snaps;
my @arr = $f->(\%TEST_CONFIG);
is_deeply([ $f->(\%TEST_CONFIG) ], ['bar_snap','baz_snap','foo_snap'], "$n - correct snap name list");
}
{
my $n = 'all_ssh_backups';
my $f = \&App::Yabsm::Config::Query::all_ssh_backups;
is_deeply([ $f->(\%TEST_CONFIG) ], ['bar_ssh_backup', 'baz_ssh_backup','foo_ssh_backup'], "$n - correct ssh_backup name list");
}
{
my $n = 'all_local_backups';
my $f = \&App::Yabsm::Config::Query::all_local_backups;
is_deeply([ $f->(\%TEST_CONFIG) ], ['bar_local_backup', 'baz_local_backup','foo_local_backup'], "$n - correct local_backup name list");
}
{
my $n = 'subvol_mountpoint';
my $f = \&App::Yabsm::Config::Query::subvol_mountpoint;
is($f->('foo', \%TEST_CONFIG), '/', "$n - got correct mountpoint");
throws_ok { $f->('quux', \%TEST_CONFIG) } qr/no subvol named 'quux'/, "$n - dies if non-existent subvol";
}
{
my $n = 'all_snaps_of_subvol';
my $f = \&App::Yabsm::Config::Query::all_snaps_of_subvol;
is_deeply([ $f->('foo', \%TEST_CONFIG) ], [ 'foo_snap' ], "$n - correct snap list");
}
{
my $n = 'all_ssh_backups_of_subvol';
my $f = \&App::Yabsm::Config::Query::all_ssh_backups_of_subvol;
is_deeply([ $f->('foo', \%TEST_CONFIG) ], [ 'foo_ssh_backup' ], "$n - correct ssh_backup list");
}
{
my $n = 'all_local_backups_of_subvol';
my $f = \&App::Yabsm::Config::Query::all_local_backups_of_subvol;
is_deeply([ $f->('foo', \%TEST_CONFIG) ], [ 'foo_local_backup' ], "$n - correct local_backup list");
}
{
my $n = 'snap_subvol';
my $f = \&App::Yabsm::Config::Query::snap_subvol;
is($f->('foo_snap', \%TEST_CONFIG), 'foo', "$n - correct subvol");
throws_ok { $f->('quux', \%TEST_CONFIG) } qr/no snap named 'quux'/, "$n - dies on non-existent snap"
}
{
my $n = 'snap_timeframes';
my $f = \&App::Yabsm::Config::Query::snap_timeframes;
is_deeply([ $f->('foo_snap', \%TEST_CONFIG) ], [ '5minute', 'daily', 'hourly', 'monthly', 'weekly' ], "$n - correct timeframes comma seperated");
is_deeply([ $f->('bar_snap', \%TEST_CONFIG )], [ '5minute' ], "$n - correct timeframes single timeframes");
throws_ok { $f->('quux', \%TEST_CONFIG) } qr/no snap named 'quux'/, "$n - dies on non-existent snap"
}
{
my $n = 'ssh_backup_subvol';
my $f = \&App::Yabsm::Config::Query::ssh_backup_subvol;
is($f->('foo_ssh_backup', \%TEST_CONFIG), 'foo', "$n - correct subvol");
throws_ok { $f->('quux', \%TEST_CONFIG) } qr/no ssh_backup named 'quux'/, "$n - dies on non-existent ssh_backup";
}
{
my $n = 'ssh_backup_mountpoint';
my $f = \&App::Yabsm::Config::Query::ssh_backup_mountpoint;
is($f->('foo_ssh_backup', \%TEST_CONFIG), '/', "$n - correct mountpoint");
throws_ok { $f->('quux', \%TEST_CONFIG) } qr/no ssh_backup named 'quux'/, "$n - dies on non-existent ssh_backup"
}
{
my $n = 'ssh_backup_dir';
my $f = \&App::Yabsm::Config::Query::ssh_backup_dir;
is($f->('foo_ssh_backup', 'hourly', \%TEST_CONFIG), '/foo/hourly', "$n - correct dir with timeframe");
is($f->('foo_ssh_backup', undef, \%TEST_CONFIG), '/foo', "$n - correct dir without timeframe");
throws_ok { $f->('quux', 'hourly', \%TEST_CONFIG) } qr/no ssh_backup named 'quux'/, "$n - dies on non-existent ssh_backup";
throws_ok { $f->('foo_ssh_backup', 'quux', \%TEST_CONFIG) } qr/no such timeframe 'quux'/, "$n - dies on non-existent ssh_backup";
}
{
my $n = 'ssh_backup_timeframes';
my $f = \&App::Yabsm::Config::Query::ssh_backup_timeframes;
is_deeply([ $f->('foo_ssh_backup', \%TEST_CONFIG) ], [ '5minute', 'daily', 'hourly', 'monthly', 'weekly' ], "$n - correct timeframes comma seperated");
is_deeply([ $f->('bar_ssh_backup', \%TEST_CONFIG) ], [ 'hourly' ], "$n - correct timeframes single timeframes");
throws_ok { $f->('quux', \%TEST_CONFIG) } qr/no ssh_backup named 'quux'/, "$n - dies on non-existent ssh_backup";
}
{
my $n = 'ssh_backup_ssh_dest';
my $f = \&App::Yabsm::Config::Query::ssh_backup_ssh_dest;
is($f->('foo_ssh_backup', \%TEST_CONFIG), 'localhost', "$n - correct ssh_dest");
throws_ok { $f->('quux', \%TEST_CONFIG) } qr/no ssh_backup named 'quux'/, "$n - dies on non-existent ssh_backup";
}
{
my $n = 'local_backup_subvol';
my $f = \&App::Yabsm::Config::Query::local_backup_subvol;
is($f->('foo_local_backup', \%TEST_CONFIG), 'foo', "$n - correct subvol");
throws_ok { $f->('quux', \%TEST_CONFIG) } qr/no local_backup named 'quux'/, "$n - dies on non-existent local_backup";
}
{
my $n = 'local_backup_dir';
my $f = \&App::Yabsm::Config::Query::local_backup_dir;
is($f->('foo_local_backup', 'hourly', \%TEST_CONFIG), '/foo/hourly', "$n - correct dir with timeframe");
is($f->('foo_local_backup', undef, \%TEST_CONFIG), '/foo', "$n - correct dir without timeframe");
throws_ok { $f->('quux', 'hourly', \%TEST_CONFIG) } qr/no local_backup named 'quux'/, "$n - dies on non-existent local_backup";
throws_ok { $f->('foo_local_backup', 'quux', \%TEST_CONFIG) } qr/no such timeframe 'quux'/, "$n - dies on non-existent ssh_backup";
}
{
my $n = 'local_backup_mountpoint';
my $f = \&App::Yabsm::Config::Query::local_backup_mountpoint;
is($f->('foo_local_backup', \%TEST_CONFIG), '/', "$n - correct mountpoint");
throws_ok { $f->('quux', \%TEST_CONFIG) } qr/no local_backup named 'quux'/, "$n - dies on non-existent local_backup"
}
{
my $n = 'local_backup_timeframes';
my $f = \&App::Yabsm::Config::Query::local_backup_timeframes;
is_deeply([ $f->('foo_local_backup', \%TEST_CONFIG) ], [ '5minute', 'daily', 'hourly', 'monthly', 'weekly' ], "$n - correct timeframes comma seperated");
is_deeply([ $f->('bar_local_backup', \%TEST_CONFIG) ], [ 'weekly' ], "$n - correct timeframes single timeframes");
throws_ok { $f->('quux', \%TEST_CONFIG) } qr/no local_backup named 'quux'/, "$n - dies on non-existent local_backup";
}
{
my $n = 'snap_wants_timeframe';
my $f = \&App::Yabsm::Config::Query::snap_wants_timeframe;
is($f->('foo_snap', 'hourly', \%TEST_CONFIG), 1, "$n - succeeds when does want");
is($f->('bar_snap', 'hourly', \%TEST_CONFIG), 0, "$n - fails when doesn't want");
throws_ok { $f->('quux', 'daily', \%TEST_CONFIG) } qr/no snap named 'quux'/, "$n dies on non-existent snap";
throws_ok { $f->('foo_snap', 'quux', \%TEST_CONFIG) } qr/no such timeframe 'quux'/, "$n dies on invalid timeframe";
{
my $n = 'snap_wants_timeframe_or_die';
my $f = \&App::Yabsm::Config::Query::snap_wants_timeframe_or_die;
is($f->('foo_snap', 'hourly', \%TEST_CONFIG), 1, "$n - succeeds when does want");
throws_ok { $f->('bar_snap', 'hourly', \%TEST_CONFIG) } qr/snap 'bar_snap' is not taking hourly snapshots/, "$n - dies if doesn't want"
}
{
my $n = 'ssh_backup_wants_timeframe';
my $f = \&App::Yabsm::Config::Query::ssh_backup_wants_timeframe;
is($f->('foo_ssh_backup', 'daily', \%TEST_CONFIG), 1, "$n - succeeds when does want");
is($f->('bar_ssh_backup', 'monthly', \%TEST_CONFIG), 0, "$n - fails when does want");
throws_ok { $f->('quux', 'daily', \%TEST_CONFIG) } qr/no ssh_backup named 'quux'/, "$n dies on non-existent ssh_backup";
throws_ok { $f->('foo_ssh_backup', 'quux', \%TEST_CONFIG) } qr/no such timeframe 'quux'/, "$n dies on invalid timeframe";
}
{
my $n = 'ssh_backup_wants_timeframe_or_die';
my $f = \&App::Yabsm::Config::Query::ssh_backup_wants_timeframe_or_die;
is($f->('foo_ssh_backup', 'daily', \%TEST_CONFIG), 1, "$n - succeeds when does want");
throws_ok { $f->('bar_ssh_backup', 'monthly', \%TEST_CONFIG) } qr/ssh_backup 'bar_ssh_backup' is not taking monthly backups/, "$n - dies when does want";
}
{
my $n = 'local_backup_wants_timeframe';
my $f = \&App::Yabsm::Config::Query::local_backup_wants_timeframe;
is($f->('foo_local_backup', 'daily', \%TEST_CONFIG), 1, "$n - succeeds when does want");
is($f->('bar_local_backup', 'monthly', \%TEST_CONFIG), 0, "$n - fails when does want");
throws_ok { $f->('quux', 'daily', \%TEST_CONFIG) } qr/no local_backup named 'quux'/, "$n dies on non-existent local_backup";
throws_ok { $f->('foo_local_backup', 'quux', \%TEST_CONFIG) } qr/no such timeframe 'quux'/, "$n dies on invalid timeframe";
}
{
my $n = 'local_backup_wants_timeframe_or_die';
my $f = \&App::Yabsm::Config::Query::local_backup_wants_timeframe_or_die;
is($f->('foo_local_backup', 'daily', \%TEST_CONFIG), 1, "$n - succeeds when does want");
throws_ok { $f->('bar_local_backup', 'monthly', \%TEST_CONFIG) } qr/local_backup 'bar_local_backup' is not taking monthly backups/, "$n - dies when does want";
}
{
my $n = 'snap_timeframe_keep';
my $f = \&App::Yabsm::Config::Query::snap_timeframe_keep;
is($f->('foo_snap', '5minute', \%TEST_CONFIG), 36, "$n - got correct 5minute_keep value");
is($f->('foo_snap', 'hourly', \%TEST_CONFIG), 48, "$n - got correct hourly_keep value");
is($f->('foo_snap', 'daily', \%TEST_CONFIG), 365, "$n - got correct daily_keep value");
is($f->('foo_snap', 'weekly', \%TEST_CONFIG), 56, "$n - got correct weekly_keep value");
{
my $n = 'snap_monthly_day';
my $f = \&App::Yabsm::Config::Query::snap_monthly_day;
is($f->('foo_snap', \%TEST_CONFIG), 31, "$n - got correct monthly_day value");
throws_ok { $f->('quux', \%TEST_CONFIG) } qr/no snap named 'quux'/, "$n - dies if non-existent snap";
throws_ok { $f->('bar_snap', \%TEST_CONFIG) } qr/snap 'bar_snap' is not taking monthly snapshots/, "$n - dies if not taking monthly snapshots";
}
{
my $n = 'ssh_backup_timeframe_keep';
my $f = \&App::Yabsm::Config::Query::ssh_backup_timeframe_keep;
is($f->('foo_ssh_backup', '5minute', \%TEST_CONFIG), 36, "$n - got correct 5minute_keep value");
is($f->('foo_ssh_backup', 'hourly', \%TEST_CONFIG), 48, "$n - got correct hourly_keep value");
is($f->('foo_ssh_backup', 'daily', \%TEST_CONFIG), 365, "$n - got correct daily_keep value");
is($f->('foo_ssh_backup', 'weekly', \%TEST_CONFIG), 56, "$n - got correct weekly_keep value");
is($f->('foo_ssh_backup', 'monthly', \%TEST_CONFIG), 12, "$n - got correct monthly_keep value");
throws_ok { $f->('quux', '5minute', \%TEST_CONFIG) } qr/no ssh_backup named 'quux'/, "$n - dies if non-existent ssh_backup";
throws_ok { $f->('foo_ssh_backup', 'quux', \%TEST_CONFIG) } qr/no such timeframe 'quux'/, "$n - dies if non-existent timeframe";
}
{
my $n = 'ssh_backup_5minute_keep';
my $f = \&App::Yabsm::Config::Query::ssh_backup_5minute_keep;
is($f->('foo_ssh_backup', \%TEST_CONFIG), 36, "$n - got correct 5minute_keep value");
throws_ok { $f->('quux', \%TEST_CONFIG) } qr/no ssh_backup named 'quux'/, "$n - dies if non-existent ssh_backup";
throws_ok { $f->('baz_ssh_backup', \%TEST_CONFIG) } qr/ssh_backup 'baz_ssh_backup' is not taking 5minute backups/, "$n - dies if not taking 5minute backups";
}
{
my $n = 'ssh_backup_hourly_keep';
my $f = \&App::Yabsm::Config::Query::ssh_backup_hourly_keep;
is($f->('foo_ssh_backup', \%TEST_CONFIG), 48, "$n - got correct hourly_keep value");
throws_ok { $f->('quux', \%TEST_CONFIG) } qr/no ssh_backup named 'quux'/, "$n - dies if non-existent ssh_backup";
throws_ok { $f->('baz_ssh_backup', \%TEST_CONFIG) } qr/ssh_backup 'baz_ssh_backup' is not taking hourly backups/, "$n - dies if not taking hourly backups";
}
{
my $n = 'ssh_backup_daily_keep';
my $f = \&App::Yabsm::Config::Query::ssh_backup_daily_keep;
is($f->('foo_ssh_backup', \%TEST_CONFIG), 365, "$n - got correct daily_keep value");
throws_ok { $f->('quux', \%TEST_CONFIG) } qr/no ssh_backup named 'quux'/, "$n - dies if non-existent ssh_backup";
throws_ok { $f->('bar_ssh_backup', \%TEST_CONFIG) } qr/ssh_backup 'bar_ssh_backup' is not taking daily backups/, "$n - dies if not taking daily backups";
}
{
my $n = 'ssh_backup_daily_times';
my $f = \&App::Yabsm::Config::Query::ssh_backup_daily_times;
is_deeply([$f->('foo_ssh_backup', \%TEST_CONFIG)], ['12:30', '23:59'], "$n - got correct daily_times value (remove dups)");
throws_ok { $f->('quux', \%TEST_CONFIG) } qr/no ssh_backup named 'quux'/, "$n - dies if non-existent ssh_backup";
throws_ok { $f->('bar_ssh_backup', \%TEST_CONFIG) } qr/ssh_backup 'bar_ssh_backup' is not taking daily backups/, "$n - dies if not taking daily backups";
}
{
my $n = 'ssh_backup_weekly_keep';
my $f = \&App::Yabsm::Config::Query::ssh_backup_weekly_keep;
is($f->('foo_ssh_backup', \%TEST_CONFIG), 56, "$n - got correct weekly_keep value");
throws_ok { $f->('quux', \%TEST_CONFIG) } qr/no ssh_backup named 'quux'/, "$n - dies if non-existent ssh_backup";
throws_ok { $f->('bar_ssh_backup', \%TEST_CONFIG) } qr/ssh_backup 'bar_ssh_backup' is not taking weekly backups/, "$n - dies if not taking weekly backups";
}
{
my $n = 'ssh_backup_weekly_time';
my $f = \&App::Yabsm::Config::Query::ssh_backup_weekly_time;
is($f->('foo_ssh_backup', \%TEST_CONFIG), '00:00', "$n - got correct weekly_time value");
throws_ok { $f->('quux', \%TEST_CONFIG) } qr/no ssh_backup named 'quux'/, "$n - dies if non-existent ssh_backup";
throws_ok { $f->('bar_ssh_backup', \%TEST_CONFIG) } qr/ssh_backup 'bar_ssh_backup' is not taking weekly backups/, "$n - dies if not taking weekly backups";
}
{
my $n = 'ssh_backup_weekly_day';
my $f = \&App::Yabsm::Config::Query::ssh_backup_weekly_day;
is($f->('foo_ssh_backup', \%TEST_CONFIG), 'wednesday', "$n - got correct weekly_day value");
throws_ok { $f->('quux', \%TEST_CONFIG) } qr/no ssh_backup named 'quux'/, "$n - dies if non-existent ssh_backup";
throws_ok { $f->('bar_ssh_backup', \%TEST_CONFIG) } qr/ssh_backup 'bar_ssh_backup' is not taking weekly backups/, "$n - dies if not taking weekly backups";
}
{
my $n = 'ssh_backup_monthly_keep';
my $f = \&App::Yabsm::Config::Query::ssh_backup_monthly_keep;
is($f->('foo_ssh_backup', \%TEST_CONFIG), 12, "$n - got correct monthly_keep value");
throws_ok { $f->('quux', \%TEST_CONFIG) } qr/no ssh_backup named 'quux'/, "$n - dies if non-existent ssh_backup";
throws_ok { $f->('bar_ssh_backup', \%TEST_CONFIG) } qr/ssh_backup 'bar_ssh_backup' is not taking monthly backups/, "$n - dies if not taking monthly backups";
}
{
my $n = 'ssh_backup_monthly_time';
my $f = \&App::Yabsm::Config::Query::ssh_backup_monthly_time;
is($f->('foo_ssh_backup', \%TEST_CONFIG), '23:59', "$n - got correct monthly_time value");
throws_ok { $f->('quux', \%TEST_CONFIG) } qr/no ssh_backup named 'quux'/, "$n - dies if non-existent ssh_backup";
throws_ok { $f->('bar_ssh_backup', \%TEST_CONFIG) } qr/ssh_backup 'bar_ssh_backup' is not taking monthly backups/, "$n - dies if not taking monthly backups";
}
{
my $n = 'ssh_backup_monthly_day';
my $f = \&App::Yabsm::Config::Query::ssh_backup_monthly_day;
is($f->('foo_ssh_backup', \%TEST_CONFIG), 31, "$n - got correct monthly_day value");
throws_ok { $f->('quux', \%TEST_CONFIG) } qr/no ssh_backup named 'quux'/, "$n - dies if non-existent ssh_backup";
throws_ok { $f->('bar_ssh_backup', \%TEST_CONFIG) } qr/ssh_backup 'bar_ssh_backup' is not taking monthly backups/, "$n - dies if not taking monthly backups";
}
{
my $n = 'local_backup_timeframe_keep';
my $f = \&App::Yabsm::Config::Query::local_backup_timeframe_keep;
is($f->('foo_local_backup', '5minute', \%TEST_CONFIG), 36, "$n - got correct 5minute_keep value");
is($f->('foo_local_backup', 'hourly', \%TEST_CONFIG), 48, "$n - got correct hourly_keep value");
is($f->('foo_local_backup', 'daily', \%TEST_CONFIG), 365, "$n - got correct daily_keep value");
is($f->('foo_local_backup', 'weekly', \%TEST_CONFIG), 56, "$n - got correct weekly_keep value");
is($f->('foo_local_backup', 'monthly', \%TEST_CONFIG), 12, "$n - got correct monthly_keep value");
throws_ok { $f->('quux', '5minute', \%TEST_CONFIG) } qr/no local_backup named 'quux'/, "$n - dies if non-existent local_backup";
throws_ok { $f->('foo_local_backup', 'quux', \%TEST_CONFIG) } qr/no such timeframe 'quux'/, "$n - dies if non-existent timeframe";
}
{
my $n = 'local_backup_5minute_keep';
my $f = \&App::Yabsm::Config::Query::local_backup_5minute_keep;
is($f->('foo_local_backup', \%TEST_CONFIG), 36, "$n - got correct 5minute_keep value");
throws_ok { $f->('quux', \%TEST_CONFIG) } qr/no local_backup named 'quux'/, "$n - dies if non-existent local_backup";
throws_ok { $f->('baz_local_backup', \%TEST_CONFIG) } qr/local_backup 'baz_local_backup' is not taking 5minute backups/, "$n - dies if not taking 5minute backups";
}
{
my $n = 'local_backup_hourly_keep';
my $f = \&App::Yabsm::Config::Query::local_backup_hourly_keep;
is($f->('foo_local_backup', \%TEST_CONFIG), 48, "$n - got correct hourly_keep value");
throws_ok { $f->('quux', \%TEST_CONFIG) } qr/no local_backup named 'quux'/, "$n - dies if non-existent local_backup";
throws_ok { $f->('baz_local_backup', \%TEST_CONFIG) } qr/local_backup 'baz_local_backup' is not taking hourly backups/, "$n - dies if not taking hourly backups";
}
{
my $n = 'local_backup_daily_keep';
my $f = \&App::Yabsm::Config::Query::local_backup_daily_keep;
is($f->('foo_local_backup', \%TEST_CONFIG), 365, "$n - got correct daily_keep value");
throws_ok { $f->('quux', \%TEST_CONFIG) } qr/no local_backup named 'quux'/, "$n - dies if non-existent local_backup";
throws_ok { $f->('bar_local_backup', \%TEST_CONFIG) } qr/local_backup 'bar_local_backup' is not taking daily backups/, "$n - dies if not taking daily backups";
}
{
my $n = 'local_backup_daily_times';
my $f = \&App::Yabsm::Config::Query::local_backup_daily_times;
is_deeply([$f->('foo_local_backup', \%TEST_CONFIG)], ['12:30','23:59'], "$n - got correct daily_times value (removes dups)");
throws_ok { $f->('quux', \%TEST_CONFIG) } qr/no local_backup named 'quux'/, "$n - dies if non-existent local_backup";
throws_ok { $f->('bar_local_backup', \%TEST_CONFIG) } qr/local_backup 'bar_local_backup' is not taking daily backups/, "$n - dies if not taking daily backups";
}
{
my $n = 'local_backup_weekly_keep';
my $f = \&App::Yabsm::Config::Query::local_backup_weekly_keep;
is($f->('foo_local_backup', \%TEST_CONFIG), 56, "$n - got correct weekly_keep value");
throws_ok { $f->('quux', \%TEST_CONFIG) } qr/no local_backup named 'quux'/, "$n - dies if non-existent local_backup";
throws_ok { $f->('baz_local_backup', \%TEST_CONFIG) } qr/local_backup 'baz_local_backup' is not taking weekly backups/, "$n - dies if not taking weekly backups";
}
{
my $n = 'local_backup_weekly_time';
my $f = \&App::Yabsm::Config::Query::local_backup_weekly_time;
is($f->('foo_local_backup', \%TEST_CONFIG), '00:00', "$n - got correct weekly_time value");
throws_ok { $f->('quux', \%TEST_CONFIG) } qr/no local_backup named 'quux'/, "$n - dies if non-existent local_backup";
throws_ok { $f->('baz_local_backup', \%TEST_CONFIG) } qr/local_backup 'baz_local_backup' is not taking weekly backups/, "$n - dies if not taking weekly backups";
}
{
my $n = 'local_backup_weekly_day';
my $f = \&App::Yabsm::Config::Query::local_backup_weekly_day;
is($f->('foo_local_backup', \%TEST_CONFIG), 'wednesday', "$n - got correct weekly_day value");
throws_ok { $f->('quux', \%TEST_CONFIG) } qr/no local_backup named 'quux'/, "$n - dies if non-existent local_backup";
throws_ok { $f->('baz_local_backup', \%TEST_CONFIG) } qr/local_backup 'baz_local_backup' is not taking weekly backups/, "$n - dies if not taking weekly backups";
}
{
my $n = 'local_backup_monthly_keep';
my $f = \&App::Yabsm::Config::Query::local_backup_monthly_keep;
is($f->('foo_local_backup', \%TEST_CONFIG), 12, "$n - got correct monthly_keep value");
throws_ok { $f->('quux', \%TEST_CONFIG) } qr/no local_backup named 'quux'/, "$n - dies if non-existent local_backup";
throws_ok { $f->('bar_local_backup', \%TEST_CONFIG) } qr/local_backup 'bar_local_backup' is not taking monthly backups/, "$n - dies if not taking monthly backups";
}
{
my $n = 'local_backup_monthly_time';
my $f = \&App::Yabsm::Config::Query::local_backup_monthly_time;
is($f->('foo_local_backup', \%TEST_CONFIG), '23:59', "$n - got correct monthly_time value");
throws_ok { $f->('quux', \%TEST_CONFIG) } qr/no local_backup named 'quux'/, "$n - dies if non-existent local_backup";
throws_ok { $f->('bar_local_backup', \%TEST_CONFIG) } qr/local_backup 'bar_local_backup' is not taking monthly backups/, "$n - dies if not taking monthly backups";
}
{
my $n = 'local_backup_monthly_day';
my $f = \&App::Yabsm::Config::Query::local_backup_monthly_day;
is($f->('foo_local_backup', \%TEST_CONFIG), 31, "$n - got correct monthly_day value");
throws_ok { $f->('quux', \%TEST_CONFIG) } qr/no local_backup named 'quux'/, "$n - dies if non-existent local_backup";
throws_ok { $f->('bar_local_backup', \%TEST_CONFIG) } qr/local_backup 'bar_local_backup' is not taking monthly backups/, "$n - dies if not taking monthly backups";
}
{
my $n = 'is_timeframe';
my $f = \&App::Yabsm::Config::Query::is_timeframe;
is($f->('5minute'), 1, "$n - accepts '5minute'");
is($f->('hourly'), 1, "$n - accepts 'hourly'");
is($f->('daily'), 1, "$n - accepts 'daily'");
is($f->('weekly'), 1, "$n - accepts 'weekly'");
t/SSHBackup.t view on Meta::CPAN
or plan skip_all => q(User 'yabsm' does not have sudo access to btrfs);
my $BTRFS_DIR = tempdir( 'yabsm-SSH.t-tmpXXXXXX', DIR => $BTRFS_SUBVOLUME, CLEANUP => 1 );
####################################
# TEST CONFIG #
####################################
my %TEST_CONFIG = ( yabsm_dir => $BTRFS_DIR
, subvols => { foo => { mountpoint => $BTRFS_SUBVOLUME } }
, ssh_backups => { foo_ssh_backup => { subvol => 'foo'
, ssh_dest => 'yabsm@localhost'
, dir => "$BTRFS_DIR/foo_ssh_backup"
, timeframes => '5minute'
, '5minute_keep' => 1
}
}
);
my $BACKUP_DIR = App::Yabsm::Config::Query::ssh_backup_dir('foo_ssh_backup', '5minute', \%TEST_CONFIG);
my $BACKUP_DIR_BASE = App::Yabsm::Config::Query::ssh_backup_dir('foo_ssh_backup', undef, \%TEST_CONFIG);
my $BACKUP = "$BACKUP_DIR/" . App::Yabsm::Snapshot::current_time_snapshot_name();
my $BOOTSTRAP_DIR = App::Yabsm::Backup::Generic::bootstrap_snapshot_dir('foo_ssh_backup','ssh',\%TEST_CONFIG);
my $TMP_DIR = App::Yabsm::Backup::Generic::tmp_snapshot_dir('foo_ssh_backup','ssh','5minute',\%TEST_CONFIG);
####################################
# TESTS #
####################################
my $n;
my $f;
$n = 'new_ssh_conn';
$f = \&App::Yabsm::Backup::SSH::new_ssh_conn;
throws_ok { $f->('quux', \%TEST_CONFIG) } qr/no ssh_backup named 'quux'/, "$n - dies if non-existent ssh_backup";
$n = 'ssh_system_or_die';
$f = \&App::Yabsm::Backup::SSH::ssh_system_or_die;
lives_and { is $f->($SSH, 'echo foo'), "foo\n" } "$n - returns correct output in scalar context";
lives_and { is_deeply [$f->($SSH, 'echo foo; echo bar')], ["foo\n","bar\n"] } "$n - returns correct output in list context";
throws_ok { $f->($SSH, 'false') } qr/remote command 'false' failed/, "$n - dies if command fails";
$n = 'check_ssh_backup_config_or_die';
$f = \&App::Yabsm::Backup::SSH::check_ssh_backup_config_or_die;
throws_ok { $f->($SSH, 'foo_ssh_backup', \%TEST_CONFIG) } qr/no directory '$BACKUP_DIR_BASE' that is readable\+writable by user 'yabsm'/, "$n - dies unless backup dir exists";
make_path_or_die($BACKUP_DIR_BASE);
throws_ok { $f->($SSH, 'foo_ssh_backup', \%TEST_CONFIG) } qr/no directory '$BACKUP_DIR_BASE' that is readable\+writable by user 'yabsm'/, "$n - dies unless backup dir is readable and writable by remote user";
system_or_die(qq(chown -R yabsm '$BTRFS_DIR'));
lives_and { is $f->($SSH, 'foo_ssh_backup', \%TEST_CONFIG), 1 } "$n - lives if properly configured";
$n = 'the_remote_bootstrap_snapshot';
$f = \&App::Yabsm::Backup::SSH::the_remote_bootstrap_snapshot;
lives_and { is $f->($SSH, 'foo_ssh_backup', \%TEST_CONFIG), undef } "$n - returns undef if no remote boot snap";
$n = 'do_ssh_backup';
$f = \&App::Yabsm::Backup::SSH::do_ssh_backup;
throws_ok { $f->($SSH, 'foo_ssh_backup', '5minute', \%TEST_CONFIG) } qr/no directory '$TMP_DIR' that is readable by user/, "$n - dies if tmp dir doesn't exist";
make_path_or_die($TMP_DIR);
throws_ok { $f->($SSH, 'foo_ssh_backup', '5minute', \%TEST_CONFIG) } qr/no directory '$BOOTSTRAP_DIR' that is readable by user/, "$n - dies if bootstrap dir doesn't exist";
make_path_or_die($BOOTSTRAP_DIR);
lives_ok { $f->($SSH, 'foo_ssh_backup', '5minute', \%TEST_CONFIG) } "$n - performs successful bootstrap";
my $lock_file = App::Yabsm::Backup::Generic::create_bootstrap_lock_file('foo_ssh_backup', 'ssh', \%TEST_CONFIG);
lives_and { is $f->($SSH, 'foo_ssh_backup', '5minute', \%TEST_CONFIG), undef } "$n - returns undef if bootstrap lock file exists";
$n = 'do_ssh_backup_bootstrap';
$f = \&App::Yabsm::Backup::SSH::do_ssh_backup_bootstrap;
lives_and { is $f->($SSH, 'foo_ssh_backup', \%TEST_CONFIG), undef } "$n - returns undef if lock file exists";
unlink $lock_file;
sleep 60;
my $got_boot_snap;
my $expected_boot_snap = "$BOOTSTRAP_DIR/.BOOTSTRAP-".App::Yabsm::Snapshot::current_time_snapshot_name();
lives_ok { $got_boot_snap = $f->($SSH, 'foo_ssh_backup', \%TEST_CONFIG) } "$n - performs another successful bootstrap";
is $got_boot_snap, $expected_boot_snap, "$n - replaces old bootstrap snapshot";
$n = 'the_remote_bootstrap_snapshot';
$f = \&App::Yabsm::Backup::SSH::the_remote_bootstrap_snapshot;
lives_and { is $f->($SSH, 'foo_ssh_backup', \%TEST_CONFIG), "$BACKUP_DIR_BASE/".basename($expected_boot_snap) } "$n - returns correct remote boot snap";
$n = 'maybe_do_ssh_backup_bootstrap';
$f = \&App::Yabsm::Backup::SSH::maybe_do_ssh_backup_bootstrap;
sleep 60;
lives_and { is $f->($SSH, 'foo_ssh_backup', \%TEST_CONFIG), $expected_boot_snap } "$n - doesn't do bootstrap if already done";
done_testing();
####################################
# CLEANUP #
####################################
sub cleanup_snapshots {
opendir(my $dh, $BACKUP_DIR_BASE) if -d $BACKUP_DIR_BASE;
t/test-configs/invalid/invalid-ssh-dest view on Meta::CPAN
# This config should fail because nick@192.foo is not a valid ssh_dest
yabsm_dir=/.snapshots/yabsm
subvol foo {
mountpoint=/
}
ssh_backup foo_backup {
ssh_dest=nick@192.foo
subvol=foo
dir=/backups
timeframes=5minute
5minute_keep=1
}
t/test-configs/invalid/local-backup-invalid-name view on Meta::CPAN
# This config should fail because 3foo_backup is not
# a valid local_backup name.
yabsm_dir=/.snapshots/yabsm
subvol foo {
mountpoint=/
}
local_backup 3foo_backup {
subvol=foo
dir=/backups
timeframes=5minute
5minute_keep=1
}
t/test-configs/invalid/local-backup-invalid-setting view on Meta::CPAN
# This config should fail because quux is not a valid local_backup setting
yabsm_dir=/.snapshots/yabsm
subvol foo {
mountpoint=/
}
local_backup foo_backup {
quux=bar
subvol=foo
dir=/backups
timeframes=5minute
5minute_keep=1
}
t/test-configs/invalid/local-backup-missing-setting view on Meta::CPAN
# This config should fail because foo_backup is missing it's dir setting
yabsm_dir=/.snapshots/yabsm
subvol foo {
mountpoint=/
}
local_backup foo_backup {
###dir=/backups
subvol=foo
timeframes=5minute
5minute_keep=1
}
t/test-configs/invalid/local-backup-undefined-subvol view on Meta::CPAN
# This config should fail because foo_ssh_backup is backing
# up an undefined subvol.
yabsm_dir=/.snapshots/yabsm
subvol foo {
mountpoint=/
}
local_backup foo_local_backup {
subvol=bar
dir=/.snapshots/yabsm/bar
timeframes=5minute
5minute_keep=1
}
t/test-configs/invalid/redefine-local-backup view on Meta::CPAN
# This config should fail because we are defining two
# local_backups with the same name.
yabsm_dir=/.snapshots/yabsm
subvol foo {
mountpoint=/
}
local_backup foo_backup {
subvol=foo
local_dest=nick@192.168.1.12
dir=/backups
timeframes=5minute
5minute_keep=36
}
local_backup foo_backup {
subvol=foo
local_dest=nick@192.168.1.12
dir=/backups/foo
timeframes=5minute
5minute_keep=1
}
t/test-configs/invalid/redefine-ssh-backup view on Meta::CPAN
# This config should fail because we are defining two
# ssh_backups with the same name.
yabsm_dir=/.snapshots/yabsm
subvol foo {
mountpoint=/
}
ssh_backup foo_backup {
subvol=foo
ssh_dest=nick@192.168.1.12
dir=/backups
timeframes=5minute
5minute_keep=36
}
ssh_backup foo_backup {
subvol=foo
ssh_dest=nick@192.168.1.12
dir=/backups/foo
timeframes=5minute
5minute_keep=1
}
t/test-configs/invalid/ssh-backup-invalid-name view on Meta::CPAN
# This config should fail because -foo_backup is not
# a valid ssh_backup name.
yabsm_dir=/.snapshots/yabsm
subvol foo {
mountpoint=/
}
ssh_backup -foo_backup {
subvol=foo
ssh_dest=nick@192.168.1.12
dir=/backups
timeframes=5minute
5minute_keep=1
}
t/test-configs/invalid/ssh-backup-invalid-setting view on Meta::CPAN
# This config should fail because quux is not a valid ssh_backup setting
yabsm_dir=/.snapshots/yabsm
subvol foo {
mountpoint=/
}
ssh_backup foo_backup {
quux=bar
subvol=foo
ssh_dest=nick@192.168.1.12
dir=/backups
timeframes=5minute
5minute_keep=1
}
t/test-configs/invalid/ssh-backup-missing-setting view on Meta::CPAN
# This config should fail because foo_backup is missing it's dir setting
yabsm_dir=/.snapshots/yabsm
subvol foo {
mountpoint=/
}
ssh_backup foo_backup {
###dir=/backups
subvol=foo
ssh_dest=nick@192.168.1.12
timeframes=5minute
5minute_keep=1
}
t/test-configs/invalid/ssh-backup-undefined-subvol view on Meta::CPAN
# This config should fail because foo_ssh_backup is backing
# up an undefined subvol.
yabsm_dir=/.snapshots/yabsm
subvol foo {
mountpoint=/
}
ssh_backup foo_ssh_backup {
subvol=bar
ssh_dest=nick@192.168.1.12
dir=/.snapshots/yabsm/bar
timeframes=5minute
5minute_keep=1
}