#!/usr/bin/perl

=head1 NAME

Script to synchronize cluster either as the master node or as a slave node

=head1 SYNOPSIS

  To push out the configuration to the other nodes :
   sync --as-master

  Options :
   --file : use with --as-master to push out only a single file
   --with-fingerbank-db : use with --as-master to push the Fingerbank
                          database during the sync (can be long)

  To sync the configuration from a master server to this server :
   sync --from=<master-ip> --api-user=<master-api-user> --api-password=<master-api-password>

Add files to /usr/local/pf/conf/cluster-files.txt (one per line) to sync additional files during the cluster synchronization.


=head1 DESCRIPTION

Made for keepalived so the pf services can be adjusted depending of the running node

=cut

use strict;
use warnings;

use constant INSTALL_DIR => '/usr/local/pf';
use lib (INSTALL_DIR . "/lib", INSTALL_DIR . "/lib_perl/lib/perl5");


BEGIN {
  use Log::Log4perl qw(get_logger);
  my $log_conf = q(
  log4perl.rootLogger              = INFO, SCREEN
  log4perl.appender.SCREEN         = Log::Log4perl::Appender::Screen
  log4perl.appender.SCREEN.stderr  = 0
  log4perl.appender.SCREEN.layout  = Log::Log4perl::Layout::PatternLayout
  log4perl.appender.SCREEN.layout.ConversionPattern = %p : %m %n
  );
  Log::Log4perl::init(\$log_conf);
}

use pf::api::jsonrpcclient;
use Getopt::Long;
use pf::api::jsonrpcclient;
use pfconfig::constants;
use pf::file_paths qw(
    $config_version_file
);
use pf::constants::cluster qw(@FILES_TO_SYNC);
use pf::cluster;
use pf::util qw(safe_pf_run);
use fingerbank::FilePath;
use Pod::Usage;


my ($master_server, $as_master, $api_user, $api_password, $with_fingerbank_db, $file, $storage, $help);
GetOptions (
    "from=s"   => \$master_server,
    "api-user=s" => \$api_user,
    "api-password=s" => \$api_password,
    "as-master" => \$as_master,
    "with-fingerbank-db" => \$with_fingerbank_db,
    "file=s" => \$file,
    "storage=s" => \$storage,
    "help|h" => \$help,
);

if($help){
  pod2usage( -verbose => 1 );
}

unless($master_server || $as_master){
    get_logger->error("You should either set --from or --as-master");
    exit;
}

my @stores = @{pf::cluster::stores_to_sync()};
my @files = @FILES_TO_SYNC;
my @added_files;

my $logger = get_logger();

if (-e '/usr/local/pf/conf/cluster-files.txt') {
    open(my $fh, '<', '/usr/local/pf/conf/cluster-files.txt')
      or get_logger->warn("Couldn't open the list of additionnal files to sync.");

    while (my $row = <$fh>) {
        chomp $row;
        push @files, $row;
        push @added_files, $row;
    }
    close($fh);
}

if($as_master){
    pf::config::cluster::increment_config_version();
    if($storage) {
        pf::cluster::sync_storages([$storage]);
    }
    elsif($file){
        unless($file =~ /^\//){
            get_logger->fatal("File path has to be absolute.");
            exit 1;
        }
        unless(-e $file){
            get_logger->fatal("File does not exist.");
            exit 1;
        }
        get_logger->info("Synching file $file with other nodes in the cluster");
        pf::cluster::sync_files([$file]);
    }
    else {
        get_logger->info("Synching cluster with this node as the configuration master");
        pf::cluster::sync_config_as_master();
        pf::cluster::sync_files(\@added_files);
        if($with_fingerbank_db) {
            pf::cluster::sync_files([$fingerbank::FilePath::UPSTREAM_DB_FILE], no_total_timeout => 1);
        }
    }

}

if($master_server){
    get_logger->info("Synching this server from node $master_server");

    my %client_args = (host => $master_server, proto => 'https', username => $api_user, password => $api_password);
    # The downloads can be long (big files, slow link): only the connection is bounded
    my $apiclient = pf::api::jsonrpcclient->new(%client_args, connect_timeout_ms => 5_000);

    # Check that the webservices of the master can be used before trying to download each file,
    # bounded so a server that accepts the connection but never answers can't hang here
    eval {
        pf::api::jsonrpcclient->new(%client_args, connect_timeout_ms => 5_000, timeout_ms => 60_000)->call('echo', 'cluster sync');
    };
    if (my $error = $@) {
        $error =~ s/\s+$//;
        my $port = $apiclient->port;
        get_logger->error("Unable to use the webservices of $master_server on port $port: $error");
        get_logger->error("Make sure that:");
        get_logger->error(" - packetfence-httpd.webservices is running and healthy on $master_server (an SSL/TLS error usually means it isn't serving, check 'docker logs httpd.webservices' there)");
        get_logger->error(" - $master_server is the management IP of the server to sync from and its port $port is reachable from this server");
        get_logger->error(" - --api-user and --api-password match the [webservices] user and pass of $master_server");
        exit 1;
    }

    my $failed = 0;
    foreach my $store (@stores) {
        my $cs = $store->new;
        my $config_file = $cs->configFile;
        push @files, $config_file if $config_file;
    }

    push @files, $config_version_file;

    foreach my $file (@files){
        eval {
            get_logger->info("Synching file : $file");
            my %data = ( conf_file => $file );
            my ($result, $mode) = $apiclient->call( 'download_configfile', %data );
            open(my $fh, '>', $file) or die "Unable to write $file: $!\n";
            print $fh $result or die "Unable to write $file: $!\n";
            close($fh) or die "Unable to write $file: $!\n";
            safe_pf_run("chown", "pf:pf", $file);
            # apply the mode returned by the master; log if chmod fails
            if (defined $mode) {
                chmod($mode, $file)
                  or get_logger->warn("Couldn't set mode on $file: $!");
            }
        };
        if($@){
            get_logger->error("Failed to sync file : $file . $@");
            $failed++;
        }
    }

    if ($failed) {
        get_logger->error("Failed to sync $failed file(s) from $master_server");
        exit 1;
    }

}

=back

=head1 AUTHOR

Inverse inc. <info@inverse.ca>

=head1 COPYRIGHT

Copyright (C) 2005-2026 Inverse inc.

=head1 LICENSE

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
USA.

=cut

1;

# vim: set shiftwidth=4:
# vim: set expandtab:
# vim: set backspace=indent,eol,start:

