#!/usr/bin/perl

# Copyright 2005 - 2020 Centreon (https://www.centreon.com/)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# For more information : contact@centreon.com
#

package centreon::script::centreon_central_sync;

use strict;
use warnings;

use Linux::Inotify2;
use File::Basename;
use centreon::script;
use centreon::common::misc;

use base qw(centreon::script);
use vars qw(%centreon_central_sync_config);

my %handlers = ('TERM' => {}, 'HUP' => {}, 'DIE' => {});
my $object_cb;

sub new {
    my $class = shift;
    my $self = $class->SUPER::new("centreon_central_sync",
        centreon_db_conn => 0,
        centstorage_db_conn => 0
    );

    bless $self, $class;
    $self->add_options(
        "config-extra=s" => \$self->{opt_extra},
    );

    $self->{watches} = {};
    $self->{rsync_dirs_cache} = ();
    $self->{special_rsync_dirs_cache} = ();
    $self->{last_time} = time();
    $self->{timetoreload} = 0;
    $self->{running} = 1;

    %{$self->{centreon_central_sync_default_config}} =
      (
       ssh_options => '-o ConnectTimeout=10 -o StrictHostKeyChecking=yes -o PreferredAuthentications=publickey -o ServerAliveInterval=10 -o ServerAliveCountMax=3 -o Compression=yes ',
       rsync_exclude => '--exclude=".*" --exclude="centengine.cmd"',
       rsync_dir => ["/etc/centreon-broker", "/etc/centreon-engine", "/var/log/centreon-engine", "/var/lib/centreon/centplugins",
                     "/etc/centreon/license.d", "/usr/share/centreon-broker/lua"],
       special_rsync_dir => ["/usr/share/centreon/www/img/media", "/usr/share/centreon/www/sounds", "/etc/snmp/centreon_traps"],
       global_sync_time => '600'
    );

    # redefine to avoid out when we try modules
    $SIG{__DIE__} = 'IGNORE';
    return $self;
}

sub init {
    my $self = shift;
    $self->SUPER::init();

    if (!defined($self->{opt_extra})) {
        $self->{opt_extra} = "/etc/centreon-ha/centreon_central_sync.pm";
    }
    if (-f $self->{opt_extra}) {
        require $self->{opt_extra};
    } else {
        $self->{logger}->writeLogInfo("Can't find extra config file $self->{opt_extra}");
    }

    $self->{centreon_central_sync_config} = {%{$self->{centreon_central_sync_default_config}}, %centreon_central_sync_config};

    $self->set_signal_handlers;
}

sub set_signal_handlers {
    my $self = shift;

    $SIG{TERM} = \&class_handle_TERM;
    $handlers{TERM}->{$self} = sub { $self->handle_TERM() };
    $SIG{HUP} = \&class_handle_HUP;
    $handlers{HUP}->{$self} = sub { $self->handle_HUP() };
    $SIG{__DIE__} = \&class_handle_DIE;
    $handlers{DIE}->{$self} = sub { $self->handle_DIE($_[0]) };

}

sub class_handle_TERM {
    foreach (keys %{$handlers{TERM}}) {
        &{$handlers{TERM}->{$_}}();
    }
}

sub class_handle_HUP {
    foreach (keys %{$handlers{HUP}}) {
        &{$handlers{HUP}->{$_}}();
    }
}

sub class_handle_DIE {
    my ($msg) = @_;

    foreach (keys %{$handlers{DIE}}) {
        &{$handlers{DIE}->{$_}}($msg);
    }
}

sub handle_TERM {
    my $self = shift;

    $self->{logger}->writeLogInfo("$$ Receiving order to stop...");
    $self->{running} = 0;
}

sub handle_HUP {
    my $self = shift;
    $self->{logger}->writeLogInfo("$$ Receiving order to reload...");
    $self->{timetoreload} = 1;
}

sub handle_DIE {
    my $self = shift;
    my $msg = @_;

    $self->{logger}->writeLogInfo("Receiving die: $msg");
    $self->{running} = 0;
}

sub reload_config {
    my $self = shift;
    my $file = $_[0];

    unless (my $return = do $file) {
        $self->{logger}->writeLogError("couldn't parse $file: $@") if $@;
        $self->{logger}->writeLogError("couldn't do $file: $!") unless defined $return;
        $self->{logger}->writeLogError("couldn't run $file") unless $return;
    }
}

sub reload {
    my $self = shift;

    $self->{logger}->writeLogInfo("Reload in progress...");

    if ($self->{logger}->is_file_mode()) {
        $self->{logger}->file_mode($self->{logger}->{file_name});
    }
    $self->{logger}->redirect_output();

    $self->reload_config($self->{opt_extra});

    $self->{logger}->writeLogInfo("Clearing watches...");
    $self->{inotify}->poll;
    foreach my $watch (keys %{$self->{watches}}) {
        $self->{watches}{$watch}->cancel;
    }

    $self->{timetoreload} = 0;

    $self->{logger}->writeLogInfo("Reload done...");
}

sub add_watch {
    my ($self, %options) = @_;

    $self->{logger}->writeLogDebug("add_watch: Trying to init watch on dir '$options{watch}'");
    if (!($self->{watches}{$options{watch}} = $self->{inotify}->watch($options{watch}, IN_CREATE|IN_CLOSE_WRITE|IN_DELETE, \&mywatch))) {
        $self->{logger}->writeLogError("add_watch: Observor error $!");
        die("Exiting");
    }

    my $cmd = 'rsync -rlptgD --omit-dir-times --chmod=g+w --update --delete --log-format="\%o \%m \%f \%l" --timeout=30 ' . $self->{centreon_central_sync_config}->{rsync_exclude} . ' -e "ssh ' .
        $self->{centreon_central_sync_config}->{ssh_options} . '" "' . $options{watch} . '/" "' . $self->{centreon_central_sync_config}->{peer_addr} .  ':' . $options{watch} . '/" 2>&1';

    my ($lerror, $stdout, $retcode) = centreon::common::misc::backtick(
        command => $cmd,
        logger => $self->{logger},
        wait_exit => 1,
        timeout => 30
    );

    if (defined($retcode) and $retcode != 0) {
        $self->{logger}->writeLogError("rsync code: " . $retcode . " output:\n$stdout ");
    } else {
        $self->{logger}->writeLogDebug("rsync code: " . $retcode . " output: '$options{watch}' = \n$stdout ");
    }
}

sub watch_and_sync {
    my $self = shift;

    foreach my $directory (@{$self->{centreon_central_sync_config}->{rsync_dir}}) {
        $self->add_watch(
            watch => $directory,
            special => 0
        );
    }

    foreach my $special_directory (@{$self->{centreon_central_sync_config}->{special_rsync_dir}}) {
        my $dh;

        if (!opendir($dh, $special_directory)) {
            $self->{logger}->writeLogError("Can't opendir '$special_directory': $!");
            die("Exiting");
        }
        while((my $filename = readdir $dh)) {
            if ($filename !~ /^\./ && -d "$special_directory/$filename") {
                $self->{special_dirs_cache}->{$special_directory . "/" . $filename} = 1;
                $self->add_watch(
                    watch => $special_directory . "/" . $filename,
                    special => 1
                );
            }
        }
        closedir $dh;
    }

    return 0
}

sub mywatch {
    my $event = $_[0];

    my $name = Linux::Inotify2::Event::fullname($event);
    my $dirname = dirname($name);
    my $file = basename($name);

    if ($file !~ /^\./) {
        if (!$event->IN_ISDIR && $event->IN_CREATE) {
            return 1;
        }
        if ($event->IN_Q_OVERFLOW) {
            return 1;
        }

        $object_cb->{rsync_dirs_cache}{$dirname} = 1;
        $object_cb->{logger}->writeLogDebug("$name was created") if $event->IN_CREATE;
        $object_cb->{logger}->writeLogDebug("$name was closed write") if $event->IN_CLOSE_WRITE;
        $object_cb->{logger}->writeLogDebug("$name is deleted") if $event->IN_DELETE;
    }
    return 0;
}


sub run {
    my $self = shift;

    $self->SUPER::run();

    $self->{logger}->writeLogInfo("centreon_central_sync launched....");
    $self->{logger}->writeLogInfo("PID: $$");

    my ($cmd, $lerror, $stdout, $retcode);

    $object_cb = $self;
    if (!defined($self->{centreon_central_sync_config}->{peer_addr}) || $self->{centreon_central_sync_config}->{peer_addr} eq '') {
        $self->{logger}->writeLogError("Please set peer_addr directive into config file");
        die("Exiting");
    }

    $self->{inotify} = new Linux::Inotify2 or die("Can't create inotify instance:  $!");
    $self->{inotify}->blocking(0);

    $self->watch_and_sync();

    while ($self->{running}) {
        $self->{inotify}->poll;
        foreach my $dir (keys %{$object_cb->{rsync_dirs_cache}}) {
            delete $object_cb->{rsync_dirs_cache}->{$dir};
            $cmd = 'rsync -rlptgD --omit-dir-times --chmod=g+w --update --delete --log-format="\%o \%m \%f \%l" --timeout=30 ' . $self->{centreon_central_sync_config}->{rsync_exclude} . ' -e "ssh ' .
                $self->{centreon_central_sync_config}->{ssh_options} . '" "' . $dir . '/" "' . $self->{centreon_central_sync_config}->{peer_addr} .  ':' . $dir . '/" 2>&1';

            ($lerror, $stdout, $retcode) = centreon::common::misc::backtick(
                command => $cmd,
                logger => $self->{logger},
                wait_exit => 1,
                timeout => 30
            );

            if (defined($retcode) and $retcode != 0) {
                $self->{logger}->writeLogError("rsync code: " . $retcode . " output:\n$stdout ");
            } else {
                $self->{logger}->writeLogDebug("rsync code: " . $retcode . " output: = \n$stdout ");
            }
        }

        sleep(2);

        if (time() - $self->{last_time} > $self->{centreon_central_sync_config}->{global_sync_time}) {
            foreach my $dir (@{$self->{centreon_central_sync_config}->{rsync_dir}}) {
                $cmd = 'rsync -rlptgD --omit-dir-times --chmod=g+w --update --delete --log-format="\%o \%m \%f \%l" --timeout=30 ' . $self->{centreon_central_sync_config}->{rsync_exclude} . ' -e "ssh ' .
                    $self->{centreon_central_sync_config}->{ssh_options} . '" "' . $dir . '/" "' . $self->{centreon_central_sync_config}->{peer_addr} .  ':' . $dir . '/" 2>&1';

                ($lerror, $stdout, $retcode) = centreon::common::misc::backtick(
                    command => $cmd,
                    logger => $self->{logger},
                    wait_exit => 1,
                    timeout => 30
                );

                if (defined($retcode) and $retcode != 0) {
                    $self->{logger}->writeLogError("rsync code: " . $retcode . " output:\n$stdout ");
                } else {
                    $self->{logger}->writeLogDebug("rsync code: " . $retcode . " output: = \n$stdout ");
                }
            }

            $self->{last_time} = time();

            # Add dir for special if needed
            foreach my $special_dir (@{$self->{centreon_central_sync_config}->{special_rsync_dir}}) {
                my $dh;
                if (!opendir($dh, $special_dir)) {
                    $self->{logger}->writeLogError("Can't opendir '$special_dir': $!");
                    next;
                }
                $cmd = 'rsync -rlptgD --omit-dir-times --chmod=g+w --update --delete --log-format="\%o \%m \%f \%l" --timeout=30 ' . $self->{centreon_central_sync_config}->{rsync_exclude} . ' -e "ssh ' .
                    $self->{centreon_central_sync_config}->{ssh_options} . '" "' . $special_dir . '/" "' . $self->{centreon_central_sync_config}->{peer_addr} .  ':' . $special_dir . '/" 2>&1';

                ($lerror, $stdout, $retcode) = centreon::common::misc::backtick(
                    command => $cmd,
                    logger => $self->{logger},
                    wait_exit => 1,
                    timeout => 30
                );

                if (defined($retcode) and $retcode != 0) {
                    $self->{logger}->writeLogError("rsync code: " . $retcode . " output:\n$stdout ");
                } else {
                    $self->{logger}->writeLogDebug("rsync code: " . $retcode . " output: = \n$stdout ");
                }

                while ((my $filename = readdir $dh)) {
                    if ($filename !~ /^\./ && -d "$special_dir/$filename") {
                        if (!defined($self->{special_dirs_cache}{$special_dir . "/" . $filename})) {
                            $self->{special_dirs_cache}{$special_dir . "/" . $filename} = 1;
                            $self->add_watch(directory => $special_dir . "/" . $filename, special => 1);
                        } else {
                            $cmd = 'rsync -rlptgD --omit-dir-times --chmod=g+w --update --delete --log-format="\%o \%m \%f \%l" --timeout=30 ' . $self->{centreon_central_sync_config}->{rsync_exclude} . ' -e "ssh ' .
                                $self->{centreon_central_sync_config}->{ssh_options} . '" "' . $special_dir . '/' . $filename . '/" "' . $self->{centreon_central_sync_config}->{peer_addr} .  ':' . $special_dir . '/' . $filename . '/" 2>&1';

                            ($lerror, $stdout, $retcode) = centreon::common::misc::backtick(
                                command => $cmd,
                                logger => $self->{logger},
                                wait_exit => 1,
                                timeout => 30
                            );

                            if (defined($retcode) and $retcode != 0) {
                                $self->{logger}->writeLogError("rsync code: " . $retcode . " output:\n$stdout ");
                            } else {
                                $self->{logger}->writeLogDebug("rsync code: " . $retcode . " output: = \n$stdout ");
                            }
                        }
                    }
                }
                closedir $dh;
            }
        }

        if ($self->{timetoreload} == 1) {
            $self->reload();
        }
    }

    $self->{inotify}->poll;

    foreach my $watch (keys %{$self->{watches}}) {
        $self->{watches}->{$watch}->cancel;
    }
}

1;

# For more information : contact@centreon.com
#

use strict;
use warnings;

centreon::script::centreon_central_sync->new()->run();

=head1 NAME

centreon_central_sync - a script to sync files over an HA setup

=head1 SYNOPSIS

centreon_central_sync [options]

=head1 OPTIONS

=over 8

=item B<--config>

Specify the path to the main configuration file (default: /etc/centreon-ha/centreon_central_sync.pm).

=item B<--help>

Print a brief help message and exits.

=back

=head1 DESCRIPTION

B<centreon_central_sync> will watch interesting directory and sync files across central servers

=cut

__END__
