App-Yabsm

 view release on metacpan or  search on metacpan

MANIFEST  view on Meta::CPAN

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

META.yml  view on Meta::CPAN

---
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",

bin/yabsm  view on Meta::CPAN

__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 *

bin/yabsm  view on Meta::CPAN


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.

bin/yabsm  view on Meta::CPAN

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.

bin/yabsm  view on Meta::CPAN


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;

t/Parser.t  view on Meta::CPAN

    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',

t/Query.t  view on Meta::CPAN

                                             }
                               , 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'
                                                           }
                                     }
                  );

                 ####################################

t/Query.t  view on Meta::CPAN


{
    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"
}

t/Query.t  view on Meta::CPAN

{
    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";

t/Query.t  view on Meta::CPAN


{
    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");

t/Query.t  view on Meta::CPAN

{
    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
}



( run in 0.895 second using v1.01-cache-2.11-cpan-49f99fa48dc )