mirror of
https://github.com/ovh/the-bastion.git
synced 2024-11-10 17:26:51 +08:00
747 lines
25 KiB
Perl
Executable file
747 lines
25 KiB
Perl
Executable file
#! /usr/bin/env perl
|
|
# vim: set filetype=perl ts=4 sw=4 sts=4 et:
|
|
use strict;
|
|
use warnings;
|
|
use 5.010;
|
|
|
|
use File::Temp;
|
|
use File::Find;
|
|
use File::Path;
|
|
use File::Copy 'move';
|
|
use Getopt::Long;
|
|
use Fcntl qw{ :flock };
|
|
use IPC::Open3;
|
|
|
|
use File::Basename;
|
|
use lib dirname(__FILE__) . '/../../lib/perl';
|
|
|
|
use OVH::Bastion;
|
|
use OVH::SimpleLog;
|
|
|
|
my %config;
|
|
my ($dryRun, $configTest, $forceDelete, $forceEncrypt, $noDelete, $encryptOnly, $rsyncOnly, $verbose, $help);
|
|
$verbose = 0;
|
|
local $| = 1;
|
|
|
|
sub is_new_gpg {
|
|
state $cached_response;
|
|
return $cached_response if defined $cached_response;
|
|
|
|
open(my $stdout, "-|", qw{ gpg --dump-options }) or die "is gnupg installed? ($!)";
|
|
$cached_response = 0;
|
|
while (<$stdout>) {
|
|
$cached_response = 1 if /--pinentry-mode/;
|
|
}
|
|
close($stdout);
|
|
|
|
return $cached_response;
|
|
}
|
|
|
|
sub gpg_sign {
|
|
my %params = @_;
|
|
my @cmd = qw{ gpg --batch --trust-model always --sign --passphrase-fd 0 };
|
|
push @cmd, qw{ --pinentry-mode loopback } if is_new_gpg();
|
|
push @cmd, "-v" if $verbose >= 2;
|
|
push @cmd, '--local-user', $params{'signkey'}, '--output', '-', $params{'infile'};
|
|
|
|
my $outfile;
|
|
if (!open($outfile, '>', $params{'outfile'})) {
|
|
_err "Failed to open output file: $!";
|
|
return 1;
|
|
}
|
|
|
|
my ($pid, $in, $out);
|
|
eval { $pid = open3($in, $out, '>&STDERR', @cmd); };
|
|
if ($@) {
|
|
_err "Failed to run gpg_sign(): $!";
|
|
return 1;
|
|
}
|
|
print {$in} $config{'signing_key_passphrase'};
|
|
close($in);
|
|
|
|
while (<$out>) {
|
|
print {$outfile} $out;
|
|
}
|
|
|
|
waitpid($pid, 0);
|
|
close($out);
|
|
close($outfile);
|
|
|
|
return 0; # success
|
|
}
|
|
|
|
sub gpg_encrypt {
|
|
my %params = @_;
|
|
my @cmd = qw{ gpg --batch --yes --trust-model always --encrypt };
|
|
if ($params{'signkey'}) {
|
|
push @cmd, qw{ --passphrase-fd 0 };
|
|
push @cmd, qw{ --pinentry-mode loopback } if is_new_gpg();
|
|
push @cmd, '--local-user', $params{'signkey'};
|
|
}
|
|
push @cmd, "-v" if $verbose >= 2;
|
|
foreach my $recipient (@{$params{'recipients'}}) {
|
|
push @cmd, "-r", $recipient;
|
|
}
|
|
|
|
push @cmd, '--output', $params{'outfile'};
|
|
push @cmd, $params{'infile'};
|
|
|
|
my ($pid, $infh);
|
|
eval { $pid = open3($infh, '>&STDOUT', '>&STDERR', @cmd); };
|
|
if ($@) {
|
|
_err "Failed to run gpg_sign(): $!";
|
|
return 1;
|
|
}
|
|
if ($params{'signkey'}) {
|
|
print {$infh} $config{'signing_key_passphrase'};
|
|
}
|
|
close($infh);
|
|
|
|
# ensure gpg is done
|
|
waitpid($pid, 0);
|
|
|
|
return 0; # success
|
|
}
|
|
|
|
sub config_load_and_lint {
|
|
my $fnret;
|
|
|
|
# Useful when erroring before we had a chance to actually read the config,
|
|
# and the configured syslog_facility value. This will be overridden below once we
|
|
# know what the user configured.
|
|
OVH::SimpleLog::setSyslog('local6');
|
|
|
|
# we can have CONFIGDIR/osh-encrypt-rsync.conf
|
|
# but also CONFIGDIR/osh-encrypt-rsync.conf.d/*
|
|
# later files override the previous ones, item by item
|
|
my @configfilelist;
|
|
if (-f -r OVH::Bastion::main_configuration_directory() . "/osh-encrypt-rsync.conf") {
|
|
push @configfilelist, OVH::Bastion::main_configuration_directory() . "/osh-encrypt-rsync.conf";
|
|
}
|
|
|
|
if (-d -x OVH::Bastion::main_configuration_directory() . "/osh-encrypt-rsync.conf.d") {
|
|
if (opendir(my $dh, OVH::Bastion::main_configuration_directory() . "/osh-encrypt-rsync.conf.d")) {
|
|
my @subfiles = map { OVH::Bastion::main_configuration_directory() . "/osh-encrypt-rsync.conf.d/" . $_ } grep { /\.conf$/ } readdir($dh);
|
|
closedir($dh);
|
|
push @configfilelist, sort @subfiles;
|
|
}
|
|
}
|
|
|
|
# no config file, fail early
|
|
if (not @configfilelist) {
|
|
_err "Error, no config file found!";
|
|
return 1;
|
|
}
|
|
|
|
# load config files in order
|
|
foreach my $configfile (@configfilelist) {
|
|
_log "Configuration: loading configfile $configfile...";
|
|
$fnret = OVH::Bastion::load_configuration_file(
|
|
file => $configfile,
|
|
rootonly => 1,
|
|
);
|
|
if (not $fnret) {
|
|
_err "Error while loading configuration from $configfile, aborting (" . $fnret->msg . ")";
|
|
return 1;
|
|
}
|
|
foreach my $key (keys %{$fnret->value}) {
|
|
$config{$key} = $fnret->value->{$key};
|
|
}
|
|
|
|
# we'll be using our own config file as a handy flock() backend
|
|
$config{'lockfile'} = $configfile if not defined $config{'lockfile'};
|
|
}
|
|
|
|
# set logging info as soon as we can, before vetting the rest of the config
|
|
$config{'syslog_facility'} //= 'local6';
|
|
if ($config{'syslog_facility'}) {
|
|
OVH::SimpleLog::setSyslog($config{'syslog_facility'});
|
|
}
|
|
else {
|
|
OVH::SimpleLog::closeSyslog();
|
|
}
|
|
OVH::SimpleLog::setLogFile($config{'logfile'}) if $config{'logfile'};
|
|
|
|
# normalize / define defaults / quick checks
|
|
if (not exists $config{'recipients'}) {
|
|
_err "config error: recipients must be defined";
|
|
return 1;
|
|
}
|
|
if (ref $config{'recipients'} ne 'ARRAY') {
|
|
_err "config error: recipients must be an array of array of GPG key IDs! (layer 1)";
|
|
return 1;
|
|
}
|
|
if (my @intruders = grep { ref $config{'recipients'}[$_] ne 'ARRAY' } 0 .. $#{$config{'recipients'}}) {
|
|
local $" = ', ';
|
|
_err "config error: recipients must be an array of array of GPG key IDs! (layer 2, indexes @intruders)";
|
|
return 1;
|
|
}
|
|
|
|
$config{'encrypt_and_move_to_directory'} //= '/home/.encrypt';
|
|
|
|
# new config option found
|
|
if (defined $config{'encrypt_and_move_ttyrec_delay_days'}) {
|
|
|
|
# check proper syntax
|
|
if ($config{'encrypt_and_move_ttyrec_delay_days'} !~ /^(?:\d+|-1)$/) {
|
|
_err "config error: encrypt_and_move_ttyrec_delay_days is not a positive integer nor -1!";
|
|
return 1;
|
|
}
|
|
|
|
# syntax is good but we also have the deprecated name, warn and proceed
|
|
if (defined $config{'encrypt_and_move_delay_days'}) {
|
|
_warn "config: deprecated option 'encrypt_and_move_delay_days' exists, but has been ignored as "
|
|
. "we also have the new option 'encrypt_and_move_ttyrec_delay_days' in the configuration";
|
|
}
|
|
}
|
|
|
|
# new config option not found
|
|
else {
|
|
# do we have the legacy option name ?
|
|
if (defined $config{'encrypt_and_move_delay_days'}) {
|
|
|
|
# yes, check proper syntax
|
|
if ($config{'encrypt_and_move_delay_days'} !~ /^(?:\d+|-1)$/) {
|
|
_err "config error: encrypt_and_move_delay_days is not an integer >= -1!";
|
|
return 1;
|
|
}
|
|
else {
|
|
# syntax ok, save it to the new name
|
|
$config{'encrypt_and_move_ttyrec_delay_days'} = delete $config{'encrypt_and_move_delay_days'};
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach my $key (qw{ encrypt_and_move_user_logs_delay_days encrypt_and_move_user_sqlites_delay_days }) {
|
|
$config{$key} //= 31;
|
|
if ($config{$key} !~ /^(?:\d+|-1)$/) {
|
|
_err "config error: $key is not an integer >= -1!";
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
$config{'rsync_delay_before_remove_days'} //= 0;
|
|
if ($config{'rsync_delay_before_remove_days'} !~ /^(?:\d+|-1)$/) {
|
|
_err "config error: rsync_delay_before_remove_days is not an integer >= -1!";
|
|
return 1;
|
|
}
|
|
|
|
$config{'rsync_destination'} //= '';
|
|
$config{'rsync_rsh'} //= '';
|
|
$config{'verbose'} //= 0;
|
|
|
|
if ($config{'verbose'} !~ /^\d+$/) {
|
|
_warn "config error: verbose is not an integer >= 0, defaulting to 0";
|
|
$config{'verbose'} = 0;
|
|
}
|
|
|
|
# if $verbose is 0, then no cmdline override has been done, so we use the config value:
|
|
$verbose ||= $config{'verbose'};
|
|
|
|
# ensure the various config files defined all the keywords we need
|
|
foreach my $keyword (qw{ signing_key signing_key_passphrase }) {
|
|
next if defined $config{$keyword};
|
|
_err "Missing mandatory configuration item '$keyword', aborting";
|
|
return 1;
|
|
}
|
|
|
|
_log "Config successfully loaded.";
|
|
|
|
if ($verbose) {
|
|
require Data::Dumper;
|
|
local $Data::Dumper::Sortkeys = 1;
|
|
local $Data::Dumper::Terse = 1;
|
|
|
|
# hide passphrase
|
|
my $passphrase = $config{'signing_key_passphrase'};
|
|
$config{'signing_key_passphrase'} = '***REDACTED***';
|
|
print Data::Dumper::Dumper({config => \%config});
|
|
$config{'signing_key_passphrase'} = $passphrase;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
sub config_test {
|
|
my $error;
|
|
|
|
# check if my gpg conf is good
|
|
my $infile = File::Temp->new(UNLINK => 1, TMPDIR => 1);
|
|
print {$infile} time();
|
|
close($infile);
|
|
|
|
_log "Testing signature with key $config{'signing_key'}... ";
|
|
my $outfile = File::Temp->new(UNLINK => 1, TMPDIR => 1);
|
|
|
|
# first, check we can sign
|
|
$error = gpg_sign(
|
|
infile => $infile,
|
|
outfile => $outfile,
|
|
signkey => $config{'signing_key'},
|
|
passphrase => $config{'signing_key_passphrase'}
|
|
);
|
|
if ($error) {
|
|
_err "Couldn't sign with the specified key $config{'signing_key'}, check your configuration";
|
|
return 1;
|
|
}
|
|
if (!-s $outfile) {
|
|
_err "Couldn't sign with the specified key $config{'signing_key'} (output file is empty), check your configuration";
|
|
return 1;
|
|
}
|
|
|
|
my %recipients_uniq;
|
|
foreach my $recipient_list (@{$config{'recipients'}}) {
|
|
foreach my $recipient (@$recipient_list) {
|
|
$recipients_uniq{$recipient}++;
|
|
}
|
|
}
|
|
|
|
foreach my $recipient (keys %recipients_uniq) {
|
|
_log "Testing encryption for recipient $recipient... ";
|
|
|
|
# then, check we can encrypt to each of the recipients
|
|
$outfile = File::Temp->new(UNLINK => 1, TMPDIR => 1);
|
|
$error = gpg_encrypt(
|
|
infile => $infile,
|
|
outfile => $outfile,
|
|
recipients => [$recipient]
|
|
);
|
|
if ($error) {
|
|
_err "Couldn't encrypt for the specified recipient <$recipient>, check your configuration";
|
|
return 1;
|
|
}
|
|
if (not -s $outfile) {
|
|
_err "Couldn't encrypt for the specified recipient <$recipient> (output file is empty), check your configuration";
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
_log "Testing encryption for all recipients + signature... ";
|
|
|
|
# then, encrypt to all the recipients, sign, and check the signature
|
|
$outfile = File::Temp->new(UNLINK => 1, TMPDIR => 1);
|
|
$error = gpg_encrypt(
|
|
infile => $infile,
|
|
outfile => $outfile,
|
|
recipients => [keys %recipients_uniq],
|
|
signkey => $config{'signing_key'},
|
|
passphrase => $config{'signing_key_passphrase'}
|
|
);
|
|
if ($error) {
|
|
_err "Couldn't encrypt and sign, check your configuration";
|
|
return 1;
|
|
}
|
|
if (not -s $outfile) {
|
|
_err "Couldn't encrypt and sign (output file is empty), check your configuration";
|
|
return 1;
|
|
}
|
|
|
|
_log "Config test passed";
|
|
return 0;
|
|
}
|
|
|
|
sub encrypt_multi {
|
|
my %params = @_;
|
|
my $source_file = $params{'source_file'};
|
|
my $destination_directory = $params{'destination_directory'};
|
|
my $remove_source_on_success = $params{'remove_source_on_success'} || 0;
|
|
|
|
my $outfile = $source_file;
|
|
$outfile =~ s!^/home/!$destination_directory/!;
|
|
my $outdir = File::Basename::dirname($outfile);
|
|
|
|
if (!-e $outdir) {
|
|
_log "Creating $outdir";
|
|
$dryRun or File::Path::mkpath(File::Basename::dirname($outfile), 0, oct(700));
|
|
}
|
|
|
|
my $layers = scalar(@{$config{'recipients'}});
|
|
_log "Encrypting $source_file to $outfile" . ".gpg" x $layers;
|
|
|
|
my $layer = 0;
|
|
my $current_source_file = $source_file;
|
|
my $current_destination_file = $outfile . '.gpg';
|
|
my $success = 1;
|
|
foreach my $recipients_array (@{$config{'recipients'}}) {
|
|
$layer++;
|
|
_log " ... encrypting $current_source_file to $current_destination_file" if $verbose;
|
|
my $error = encrypt_once(
|
|
source_file => $current_source_file,
|
|
destination_file => $current_destination_file,
|
|
recipients => $recipients_array,
|
|
);
|
|
if ($layer > 1 and $layer <= $layers) {
|
|
|
|
# transient file
|
|
_log " ... deleting transient file $current_source_file" if $verbose;
|
|
if (!$dryRun) {
|
|
if (!unlink $current_source_file) {
|
|
|
|
# maybe it is +a? try to -a it blindly and retry
|
|
system('chattr', '-a', $current_source_file);
|
|
if (!unlink $current_source_file) {
|
|
_warn "Couldn't delete transient file '$current_source_file' ($!)";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if ($error) {
|
|
$success = 0;
|
|
last;
|
|
}
|
|
$current_source_file = $current_destination_file;
|
|
$current_destination_file .= '.gpg';
|
|
}
|
|
if ($success and $remove_source_on_success) {
|
|
_log " ... removing source file $source_file" if $verbose;
|
|
if (!$dryRun) {
|
|
if (!unlink $source_file) {
|
|
|
|
# maybe it is +a? try to -a it blindly and retry
|
|
system('chattr', '-a', $source_file);
|
|
if (!unlink $source_file) {
|
|
_warn "Couldn't delete source file '$source_file' ($!)";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return !$success;
|
|
}
|
|
|
|
sub encrypt_once {
|
|
my %params = @_;
|
|
my $source_file = $params{'source_file'};
|
|
my $destination_file = $params{'destination_file'};
|
|
my $recipients = $params{'recipients'};
|
|
my $error;
|
|
|
|
if (not -f $source_file and not $dryRun) {
|
|
_err "encrypt_once: source file $source_file is not a file!";
|
|
return 1;
|
|
}
|
|
|
|
if (-f $destination_file) {
|
|
_log "encrypt_once: destination file $destination_file already exists, renaming!";
|
|
move($destination_file, "$destination_file.old." . time());
|
|
}
|
|
|
|
$error = gpg_encrypt(
|
|
infile => $source_file,
|
|
outfile => $destination_file,
|
|
recipients => $recipients,
|
|
signkey => $config{'signing_key'},
|
|
passphrase => $config{'signing_key_passphrase'},
|
|
);
|
|
if ($error) {
|
|
_err "encrypt_once: failed encrypting $source_file to $destination_file";
|
|
return 1;
|
|
}
|
|
if (!-s $destination_file) {
|
|
_err "encrypt_once: failed encrypting $source_file to $destination_file (destination is empty)";
|
|
return 1;
|
|
}
|
|
return 0; # no error
|
|
}
|
|
|
|
# this sub is called for each file found
|
|
sub potentially_work_on_this_file {
|
|
|
|
# file must be either:
|
|
# - a ttyrec file or an osh_http_proxy_ttyrec-ish file
|
|
# - a user sqlite file (possibly compressed)
|
|
# - a user log file (possibly compressed)
|
|
my ($filetype, $file_delay);
|
|
if (m{^/home/[^/]+/ttyrec/[^/]+/[^/]+(?:\.ttyrec(?:\.zst)?)?$}) {
|
|
$filetype = 'ttyrec';
|
|
$file_delay = $config{'encrypt_and_move_ttyrec_delay_days'};
|
|
}
|
|
elsif (m{^/home/[^/]+/ttyrec/[^/]+/\d+-\d+-\d+\.txt$}) {
|
|
$filetype = 'proxylog';
|
|
$file_delay = $config{'encrypt_and_move_ttyrec_delay_days'};
|
|
|
|
# never touch a file that's too recent because we might still write to it:
|
|
$file_delay = 1 if $file_delay < 1;
|
|
}
|
|
elsif (m{^/home/[^/]+/[^/]+\.log(?:\.gz|\.xz)?$}) {
|
|
$filetype = 'userlog';
|
|
$file_delay = $config{'encrypt_and_move_user_logs_delay_days'};
|
|
|
|
# never touch a file that's too recent because we might still write to it:
|
|
$file_delay = 31 if $file_delay < 31;
|
|
}
|
|
elsif (m{^/home/[^/]+/[^/]+\.sqlite(?:\.gz|\.xz)?$}) {
|
|
$filetype = 'usersqlite';
|
|
$file_delay = $config{'encrypt_and_move_user_sqlites_delay_days'};
|
|
|
|
# never touch a file that's too recent because we might still write to it:
|
|
$file_delay = 31 if $file_delay < 31;
|
|
}
|
|
else {
|
|
# ignore this file
|
|
_log "Ignoring file $_" if ($verbose >= 2);
|
|
return;
|
|
}
|
|
|
|
_log "Considering file $_" if ($verbose >= 2);
|
|
|
|
# we might not have the right to touch some filetypes, as per config
|
|
return if ($file_delay < 0);
|
|
|
|
# $_ must exist and be a file
|
|
-f or return;
|
|
my $file = $_;
|
|
|
|
# first, populate (once) the list of ttyrec files that are still opened by ttyrec
|
|
state $openedFiles;
|
|
if (ref $openedFiles ne 'HASH') {
|
|
$openedFiles = {};
|
|
if (open(my $fh_lsof, '-|', "lsof -a -n -c ttyrec -- /home/")) {
|
|
while (<$fh_lsof>) {
|
|
chomp;
|
|
m{\s(/home/[^/]+/ttyrec/\S+)$} and $openedFiles->{$1} = 1;
|
|
}
|
|
close($fh_lsof);
|
|
_log "Found " . (scalar keys %$openedFiles) . " opened ttyrec files we won't touch";
|
|
}
|
|
else {
|
|
_warn "Error trying to get the list of opened ttyrec files, we might rotate opened files!";
|
|
}
|
|
}
|
|
|
|
# still open? don't touch
|
|
if (exists $openedFiles->{$file}) {
|
|
_log "File $file is still opened by ttyrec, skipping";
|
|
return;
|
|
}
|
|
|
|
# ignore files that are too recent (as per config)
|
|
my $mtime = (stat($file))[9];
|
|
if ($mtime > time() - 86400 * $file_delay) {
|
|
_log "File $file is too recent ($filetype: $file_delay days), skipping" if $verbose;
|
|
return;
|
|
}
|
|
|
|
# ok, this file is eligible, go
|
|
my $error = encrypt_multi(source_file => $file, destination_directory => $config{'encrypt_and_move_to_directory'}, remove_source_on_success => not $noDelete);
|
|
if ($error) {
|
|
_err "Got an error for $file, skipping!";
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
sub directory_filter { ## no critic (RequireArgUnpacking)
|
|
|
|
# /home? check the subdirs and add them one by one if they are a bastion account's home
|
|
if ($File::Find::dir eq '/home') {
|
|
my @out = ();
|
|
foreach (@_) {
|
|
if (-e "/home/$_/lastlog" || -e "/home/$_/ttyrec") {
|
|
push @out, $_;
|
|
}
|
|
}
|
|
my @sorted = sort @out;
|
|
if ($verbose >= 2) {
|
|
_log "Filter: adding directory $_" for @sorted;
|
|
}
|
|
return @sorted;
|
|
}
|
|
|
|
# /home/*/ttyrec/*? check all subdirs/files up to infinite depth
|
|
if ($File::Find::dir =~ m{^/home/[^/]+($|/ttyrec)}) {
|
|
_log "Filter: adding all files of " . $File::Find::dir . ": " . join(", ", @_) if ($verbose >= 2);
|
|
return @_;
|
|
}
|
|
|
|
_log "Filter: not adding anything from " . $File::Find::dir if ($verbose >= 2);
|
|
return ();
|
|
}
|
|
|
|
sub print_usage {
|
|
print <<"EOF";
|
|
|
|
$0 [options]
|
|
|
|
--dry-run Don't actually compress/encrypt/rsync, just show what would be done
|
|
--config-test Test the validity of the config file and GPG setup
|
|
--verbose More logs, use twice to also get gpg raw output
|
|
|
|
encryption phase:
|
|
--encrypt-only Encrypt and move the files, but skip the rsync phase
|
|
--force-encrypt Don't wait for the configured number of days before encrypting & moving files, do it immediately.
|
|
Note that filetypes that have their amount of days set to -1 from the config file will still be ignored,
|
|
and the minimum configurable amount of time still applies per filetype (i.e. to avoid moving a file still in use).
|
|
|
|
rsync phase:
|
|
--rsync-only Skip the encryption phase, just rsync the already encrypted & moved files
|
|
--force-delete Don't wait for the configured number of days before deleting rsynced files,
|
|
do it as soon as they're transferred
|
|
--no-delete Don't delete local files after rsyncing, even if the configured amount of days has passed
|
|
|
|
EOF
|
|
return;
|
|
}
|
|
|
|
sub main {
|
|
_log "Starting...";
|
|
|
|
{
|
|
my $optwarn = 'Unknown error';
|
|
local $SIG{'__WARN__'} = sub { $optwarn = shift; };
|
|
if (
|
|
!GetOptions(
|
|
"dry-run" => \$dryRun,
|
|
"config-test" => \$configTest,
|
|
"no-delete" => \$noDelete,
|
|
"encrypt-only" => \$encryptOnly,
|
|
"rsync-only" => \$rsyncOnly,
|
|
"force-delete" => \$forceDelete,
|
|
"force-encrypt" => \$forceEncrypt,
|
|
"verbose+" => \$verbose,
|
|
"help" => \$help,
|
|
)
|
|
)
|
|
{
|
|
_err "Error while parsing command-line options: $optwarn";
|
|
print_usage();
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
if ($help) {
|
|
print_usage();
|
|
return 0;
|
|
}
|
|
|
|
if (config_load_and_lint() != 0) {
|
|
_err "Configuration is invalid, aborting";
|
|
return 1;
|
|
}
|
|
|
|
# ensure no other copy of myself is already running
|
|
# except if we are in rsync-only mode (concurrency is then not a problem)
|
|
my $lockfh;
|
|
if (not $rsyncOnly) {
|
|
if (!open($lockfh, '<', $config{'lockfile'})) {
|
|
|
|
# flock() needs a file handler
|
|
_log "Couldn't open config file, aborting";
|
|
return 1;
|
|
}
|
|
if (!flock($lockfh, LOCK_EX | LOCK_NB)) {
|
|
_log "Another instance is running, aborting this one!";
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
if ($forceDelete) {
|
|
$config{'rsync_delay_before_remove_days'} = 0;
|
|
}
|
|
if ($forceEncrypt) {
|
|
foreach my $type (qw{ ttyrec user_logs user_sqlites }) {
|
|
|
|
# keep config at -1 if it's set at -1 (i.e. filetype disabled)
|
|
$config{"encrypt_and_move_${type}_delay_days"} = 0 if $config{"encrypt_and_move_${type}_delay_days"} > 0;
|
|
}
|
|
}
|
|
|
|
if (config_test() != 0) {
|
|
_err "Config test failed, aborting";
|
|
return 1;
|
|
}
|
|
|
|
if ($configTest) {
|
|
return 0;
|
|
}
|
|
|
|
if ($dryRun) {
|
|
_log "Dry-run mode enabled, won't actually encrypt, move or delete files!";
|
|
}
|
|
|
|
if (not $rsyncOnly) {
|
|
_log "Looking for files in /home/ ...";
|
|
File::Find::find(
|
|
{
|
|
no_chdir => 1,
|
|
preprocess => \&directory_filter,
|
|
wanted => \&potentially_work_on_this_file
|
|
},
|
|
"/home/",
|
|
);
|
|
}
|
|
|
|
if (not($encryptOnly || $config{'encrypt_only'}) and $config{'rsync_destination'}) {
|
|
my @command;
|
|
my $sysret;
|
|
|
|
if (!-d $config{'encrypt_and_move_to_directory'} && $dryRun) {
|
|
_log "DRYRUN: source directory doesn't exist, substituting with another one (namely the config directory which we know exists), just to try the rsync in dry-run mode";
|
|
$config{'encrypt_and_move_to_directory'} = '/etc/cron.d/';
|
|
}
|
|
|
|
if (!-d $config{'encrypt_and_move_to_directory'}) {
|
|
_log "Nothing to rsync as the rsync source dir doesn't exist";
|
|
}
|
|
else {
|
|
_log "Now rsyncing files to remote host ...";
|
|
@command = qw{ rsync --prune-empty-dirs --one-file-system -a };
|
|
push @command, '-v' if $verbose;
|
|
if ($config{'rsync_rsh'}) {
|
|
push @command, '--rsh', $config{'rsync_rsh'};
|
|
}
|
|
if ($dryRun) {
|
|
push @command, '--dry-run';
|
|
}
|
|
|
|
push @command, $config{'encrypt_and_move_to_directory'} . '/';
|
|
push @command, $config{'rsync_destination'} . '/';
|
|
_log "Launching the following command: @command";
|
|
$sysret = system(@command);
|
|
|
|
if ($sysret != 0) {
|
|
_err "Error while rsyncing, stopping here";
|
|
return 1;
|
|
}
|
|
|
|
# now run rsync again BUT only with files having mtime +rsync_delay_before_remove_days AND specifying --remove-source-files
|
|
# this way only files old enough AND successfully transferred to the other side will be removed
|
|
|
|
if (!$dryRun) {
|
|
my $prevdir = $ENV{'PWD'};
|
|
if (not chdir $config{'encrypt_and_move_to_directory'}) {
|
|
_err "Error while trying to chdir to " . $config{'encrypt_and_move_to_directory'} . ", aborting";
|
|
return 1;
|
|
}
|
|
|
|
_log "Building a list of rsynced files to potentially delete (older than " . $config{'rsync_delay_before_remove_days'} . " days)";
|
|
my $cmdstr = "find . -xdev -type f -name '*.gpg' -mtime +" . ($config{'rsync_delay_before_remove_days'} - 1) . " -print0 | rsync -" . ($verbose ? 'v' : '') . "a ";
|
|
if ($config{'rsync_rsh'}) {
|
|
$cmdstr .= "--rsh '" . $config{'rsync_rsh'} . "' ";
|
|
}
|
|
if ($dryRun) {
|
|
$cmdstr .= "--dry-run ";
|
|
}
|
|
$cmdstr .= "--remove-source-files --files-from=- --from0 " . $config{'encrypt_and_move_to_directory'} . '/' . " " . $config{'rsync_destination'} . '/';
|
|
_log "Launching the following command: $cmdstr";
|
|
$sysret = system($cmdstr);
|
|
if ($sysret != 0) {
|
|
_err "Error while rsyncing for deletion, stopping here";
|
|
return 1;
|
|
}
|
|
|
|
# remove empty directories
|
|
_log "Removing now empty directories...";
|
|
|
|
# errors would be printed for non empty dirs, we don't care
|
|
system("find " . $config{'encrypt_and_move_to_directory'} . " -type d ! -wholename " . $config{'encrypt_and_move_to_directory'} . " -delete 2>/dev/null");
|
|
|
|
chdir $prevdir;
|
|
}
|
|
}
|
|
}
|
|
|
|
_log "Done, got " . (OVH::SimpleLog::nb_errors()) . " error(s) and " . (OVH::SimpleLog::nb_warnings()) . " warning(s).";
|
|
return 0;
|
|
}
|
|
|
|
exit main();
|