#!/usr/bin/perl
#
# ***** BEGIN LICENSE BLOCK *****
# Zimbra Collaboration Suite Server
# Copyright (C) 2010, 2012, 2013, 2014, 2015, 2016 Synacor, Inc.
#
# 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,
# version 2 of the License.
#
# 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, see <https://www.gnu.org/licenses/>.
# ***** END LICENSE BLOCK *****
#
# Must be run on a system where the ldap_url key is set to contain all of the
# replicas, as that is how the script determines what replicas exist.

use strict;
use lib '/opt/zimbra/common/lib/perl5';
use Net::LDAP;
use Date::Manip;

my (%c,%loaded,%mstatus,%rstatus);
$c{zmlocalconfig}="/opt/zimbra/bin/zmlocalconfig";

my $ldap_starttls_supported=getLocalConfig("ldap_starttls_supported");
my $ldap_master=getLocalConfig("ldap_master_url");
my $ldap_urls=getLocalConfig("ldap_url");
my $ldap_pass=getLocalConfig("ldap_master_root_password") || getLocalConfig("ldap_root_password");
my ($mesgp, $entry);

my @masters = split / /, $ldap_master;
my @replicas = split / /, $ldap_urls;

for my $master (@masters) {
  my $index=0;
  chomp($master);
  for my $rep (@replicas) {
    chomp($rep);
    if ($master eq $rep) {
      splice @replicas,$index,1;
    }
    $index++;
  }
}  

foreach my $master (@masters) {
  my @csns;
  my $ldapp;
  chomp($master);
  if ($ldapp = Net::LDAP->new( $master, async=> 1 ) ) { 
    if ($master !~ /^ldaps/i) {
      if ($ldap_starttls_supported) {
        $mesgp = $ldapp->start_tls(
                   verify => 'none',
                   capath => "/opt/zimbra/conf/ca",
                 ) or die "start_tls: $@";
        if($mesgp->code) {
          $mstatus{$master}[0]=3;
          $mstatus{$master}[1]="Could not execute StartTLS ($master)";
        }
      }
    }
  }
  if (!defined($ldapp)) {
    $mstatus{$master}[0]=4;
    $mstatus{$master}[1]="Server down";
    next;
  }
  $mesgp=$ldapp->bind("cn=config", password => $ldap_pass);
  $mesgp->code && die "Unable to bind to master ($master): " . (($ldap_pass) ? $mesgp->error : "Please set ldap_master_root_password") . "\n";
 
  $mesgp = $ldapp->search(
             base    =>  'cn=config',
             scope   =>  'sub',
             filter  =>  '(olcServerID=*)',
             attrs   =>  ['olcServerID']
           );

  my $serverid=0;
  $entry = $mesgp->entry(0);
  if ($entry) {
    $serverid=$entry->get_value('olcServerID'); 
  }

  $mesgp = $ldapp->search(
             base    =>  '',
             scope   =>  'base',
             filter  =>  '(objectclass=*)',
             attrs   =>  ['contextCSN']
           );
  if ($mesgp->code) {
    $mstatus{$master}[0]=5;
    $mstatus{$master}[1]="Unable to search";
    next;
  }
  $entry = $mesgp->entry(0);
  @csns = $entry->get_value('contextCSN');
  
  if (!@csns) {
    $mstatus{$master}[0]=5;
    $mstatus{$master}[1]="Not a replicated master ($master)\n";
  }
  $mstatus{$master}[0]=0;
  $mstatus{$master}[1]="CSNs Retrieved";
  my $pcsn;
  my $csnsize = $#csns;
  for (my $i=0; $i < $csnsize; $i++) {
    if (scalar(@masters) > 1 && $csns[$i] =~ /#000#/) { next; }
    $pcsn.=$csns[$i].":";
  }
  $pcsn.=$csns[$csnsize];
  $mstatus{$master}[2]=$pcsn;
  $mstatus{$master}[3]=$serverid;
  $ldapp->unbind;
}

if (scalar(@masters) > 1) {

  my $melements = $#masters;
  my $primcurstatus = $mstatus{$masters[0]}[0];
  my $localcsns = $mstatus{$masters[0]}[2];
  my @lcsns = split /:/, $localcsns;
  my $index=$#lcsns;

  for (my $j=1; $j<=$melements; $j++) {
    my $curstatus = $mstatus{$masters[$j]}[0];
    my $nextcsns = $mstatus{$masters[$j]}[2];
    my $status=0;
    my @ncsns = split /:/, $nextcsns;
    my $delta;

    for (my $i=0; $i <= $index; $i++) {
      if ($curstatus || $primcurstatus) {
        next;
      } elsif ($lcsns[$i] eq $ncsns[$i]) {
        next;
      } else {
        $delta = &compareCSN($lcsns[$i], $ncsns[$i]);
        $status = 6;
      }
    }
    if ($status == 6) {
      $mstatus{$masters[0]}[0]=$status;
      $mstatus{$masters[0]}[1]=$delta;
      $mstatus{$masters[$j]}[0]=$status;
      $mstatus{$masters[$j]}[1]=$delta;
    } else {
      if ($primcurstatus == 0) {
        $mstatus{$masters[0]}[1]="In Sync";
      }
      if ($curstatus == 0) {
        $mstatus{$masters[$j]}[1]="In Sync";
      }
    }
  }

  foreach my $master (@masters) {
    chomp($master);
    print "Master: $master";
    print " ServerID: ".$mstatus{$master}[3];
    print " Code: ".$mstatus{$master}[0];
    print " Status: ".$mstatus{$master}[1];
    if(defined($mstatus{$master}[2])) {
      my $pcsn = $mstatus{$master}[2];
      $pcsn =~ s/:/\n/g;
      print " CSNs:\n".$pcsn;
    }
    print "\n";
  }
}

foreach my $replica (@replicas) {
  my ($pcsn, @rcsns, $mesgr, $ldapr);
  chomp($replica);

  if ($ldapr = Net::LDAP->new( $replica, async=> 1 ) ) {
    if ($replica !~ /^ldaps/i) {
      if ($ldap_starttls_supported) {
        $mesgr = $ldapr->start_tls(
                   verify => 'none',
                   capath => "/opt/zimbra/conf/ca",
                 ) or die "start_tls: $@";
        if($mesgr->code) {
          $rstatus{$replica}[0]=3;
          $rstatus{$replica}[1]="Could not execute StartTLS";
        }
      }
    }
  }
  if (!defined($ldapr)) {
    $rstatus{$replica}[0]=4;
    $rstatus{$replica}[1]="Server down";
    next;
  }

  $mesgr=$ldapr->bind;
  $mesgr = $ldapr->search(
             base    =>  "",
             scope   =>  'base',
             filter  =>  '(objectclass=*)',
             attrs   =>  ['contextCSN']
           );
  if ($mesgr->code) {
    $rstatus{$replica}[0]=5;
    $rstatus{$replica}[1]="Unable to search";
    next;
  }
  $entry = $mesgr->entry(0);
  @rcsns=$entry->get_value('contextCSN');

  my $rcsn;
  my $rcsnsize = $#rcsns;
  for (my $i=0; $i < $rcsnsize; $i++) {
    if (scalar(@masters) > 1 && $rcsns[$i] =~ /#000#/) { next; }
    $rcsn.=$rcsns[$i].":";
  }
  $rcsn.=$rcsns[$rcsnsize];
  $rstatus{$replica}[2]=$rcsn;
  @rcsns = split /:/, $rcsn;
  $rcsnsize = $#rcsns;

  my $master=$masters[0];
  chomp($master);
  $pcsn = $mstatus{$master}[2];
  my @pcsns = split /:/, $pcsn;

  for (my $i=0; $i <= $rcsnsize; $i++) {
    if ($rcsns[$i] eq $pcsns[$i]) {
      if($rstatus{$replica}[0] != 6) {
        $rstatus{$replica}[0]=0;
        $rstatus{$replica}[1]="In Sync";
      }
    } else {
      my $delta=&compareCSN($rcsns[$i], $pcsns[$i]);
      $rstatus{$replica}[0]=6;
      $rstatus{$replica}[1]="$delta";
    }
  }
}

foreach my $replica (@replicas) {
  print "Replica: $replica";
  print " Code: ".$rstatus{$replica}[0];
  print " Status: ".$rstatus{$replica}[1];
  if(defined($rstatus{$replica}[2])) {
    my $rcsn = $rstatus{$replica}[2];
    $rcsn =~ s/:/\n/g;
    print " CSNs:\n".$rcsn;
  }
  print "\n";
}

sub getLocalConfig {
  my ($key,$force) = @_;

  return $loaded{lc}{$key}
    if (exists $loaded{lc}{$key} && !$force);
  my $val=qx($c{zmlocalconfig} -x -s -m nokey ${key} 2> /dev/null);
  chomp($val);
  $loaded{lc}{$key} = $val;
  return $val;
}

sub compareCSN($$) {
  my ($x, $y) = @_;
    $x =~ s/#.*//g;
    $y =~ s/#.*//g;
    $x =~ s/\..*//g;
    $y =~ s/\..*//g;
    $x =~ s/$/Z/;
    $y =~ s/$/Z/;
    my $tdelta = DateCalc($y,$x);
    if ($tdelta =~ /\-/) {
      $tdelta=~ s/\-//;
      $tdelta.="s behind";
    } elsif ($tdelta =~ /\+/) {
      $tdelta=~ s/\+//;
      $tdelta.="s ahead";
    }
    foreach ("y", "M", "w","d", "h", "m") { $tdelta =~ s/:/$_ /;}
    return $tdelta;
}
