#!/usr/bin/perl -w

=head1 NAME
    
    downtime-checker

=head1 DESCRIPTION
    
    Для заданного списка хостов показывает, когда с ними случится/уже случилось отключение.
    Про API учений см. http://clubs.at.yandex-team.ru/4611686018427387924/replies.xml?item_no=150

    downtime-checker [options] [host1 host2 ...]

    Опции:

    -f|--file
        Читать список хостов из файла
    -d|--db-conf
        Читать список хостов из db-config в формате Директа 
    -c|--conductor-group
        Брать список хостов из кондукторной группы
    --dc

    Примеры: 

    можно велеть читать хосты с stdin:
        downtime-checker -

    Явный список хостов
        downtime-checker ppcdev1.yandex.ru ppcdev2.yandex.ru ppcdev3.yandex.ru

    Кондукторная группа
        downtime-checker -c direct_ng_databases_mysql_ppcrbac2_prod -c direct_soap
        downtime-checker -c direct_ng_databases_mysql_prod

    Датацентр
        downtime-checker -c direct -dc sas 

    db-config (полезно проверить перед учениями)
        downtime-checker -d /etc/yandex-direct/db-config.json 

    Можно все сразу
        downtime-checker -d /etc/yandex-direct/db-config.json -c direct_ng_databases_mysql_ppcmonitor_prod ppcdev{1,2,3,5}.yandex.ru


    Экзотические пайпы
    perl -MYandex::Conductor -le 'print for @{conductor_groups2hosts("direct")}' | downtime-checker -
    perl -MJSON -e 'sub p{if(ref $_[0] eq "ARRAY"){print map {"$_\n"} @{$_[0]}} else {print "$_[0]\n"} } sub f{if(ref $_[0] eq "ARRAY"){f($_) for @{$_[0]};return;} return unless ref $_[0] eq 'HASH'; p $_[0]->{host} if exists $_[0]->{host}; f($_) for values %{$_[0]}; };$c = from_json(join "", <>); f($c);' /etc/yandex-direct/db-config.json |xargs downtime-checker

    Пример вывода: 
    > downtime-checker -c direct_ng_databases_mysql_ppcdict_prod
                   -                   -    ppcdict01e.yandex.ru
                   -                   -    ppcdict01f.yandex.ru
                   -                   -    ppcstandby04e.yandex.ru
    2014-10-01 19:00    2014-10-01 20:00    ppcdict01d.yandex.ru

=cut

use strict;
use warnings;
use utf8;
use open ':std' => ':utf8';

use Data::Dumper;
use Getopt::Long;
use JSON;
use List::MoreUtils qw/uniq/;
use POSIX qw/strftime/;
use Socket qw/getaddrinfo AI_CANONNAME/; 

use Yandex::Conductor;
use Direct::ZKDowntime;

# используем c.y-t.ru/api-cached для не-риалтаймовый данных - задержка около часа, но на куче хостов работает быстрее
$Yandex::Conductor::CONDUCTOR_API_URL = $ENV{CONDUCTOR_API_URL} || "http://c.yandex-team.ru/api-cached";

run() unless caller();

sub run
{
    my $opt = parse_options();
    my $hosts = get_hosts_to_check(%$opt); 
    my $downtimes = get_downtimes($hosts);

    print_downtimes($downtimes);

    exit 0;
}



sub parse_options
{
    my %O;

    GetOptions(
        "help" => sub {system("podselect -section NAME -section DESCRIPTION $0 | pod2text-utf8"); exit 0;},
        "f|file=s@" => \$O{file},
        "d|db-conf=s@" => \$O{db_config},
        "c|conductor-group=s@" => \$O{conductor_group},
        "dc=s" => \$O{datacenter},
    ) || die "can't parse params, stop.\n";

    $O{hosts} = [ @ARGV ];

    return \%O;
}


sub get_hosts_to_check
{
    my %O = @_;

    my @hosts;

    if ( $O{hosts} && @{$O{hosts}} > 0 && $O{hosts}->[0] eq '-'){
        @hosts = <>;
        chomp for @hosts;
    } elsif ( $O{hosts} && @{$O{hosts}} > 0 ) {
        @hosts = @{$O{hosts}};
    }

    for my $f ( @{$O{file}||[]} ){
        open(my $fd, "<", $f) or die "can't open file '$f', stop.\n";
        my $text = join "", <$fd>;
        # у нас много не "yandex.ru" хостов
        push @hosts, uniq ($text =~ /(\b[.a-z0-9_-]+\.yandex-?[.a-z]*)/gsmi);
    }

    for my $f ( @{$O{db_config}||[]} ){
        open(my $fd, "<", $f) or die "can't open file '$f', stop.\n";
        my $text = join "", <$fd>;
        my $c = from_json($text); 
        push @hosts, uniq extract_hosts($c);
    }

    for my $gr ( @{$O{conductor_group}||[]} ){
        push @hosts, @{conductor_groups2hosts($gr)};
    }
    die "no hosts found in provided sources\n" if ! @hosts;

    if ($O{datacenter}) {
        my $hosts_info = conductor_hosts(\@hosts);
        @hosts = ();
        map { push(@hosts, $_->{fqdn}) if $_->{datacenter} && $_->{datacenter} =~ /$O{datacenter}/ } @$hosts_info;
        die "no single host is located in datacenter '$O{datacenter}'\n" if ! @hosts;
    }

    return \@hosts;
}


sub get_downtimes
{
    my ($hosts) = @_;

    my $now = time;

    my @downtimes;
    foreach my $host (@$hosts) {
        # определяем каноническое имя сервера -- иногда api учений знают только их
        my ($err, @res) = getaddrinfo($host, 0, {flags => AI_CANONNAME}); 
        my $host_cname = (map {$_->{canonname} || ()} @res)[0]; 

        # не нашлось канонического имени
        unless ($host_cname){
            push @downtimes, {
                host => $host, 
                host_cname => "can't get canonical name",
                start => 'error',
                end => 'error', 
            };
            next;
        }
        my @to_check = ($host);
        push @to_check, $host_cname if $host_cname ne $host;
        
        # спрашиваем api учений
        my $res;
        for my $host_name ( @to_check ){
            my $d = eval{ Direct::ZKDowntime::downtime_check($host_name) };
            if ($@){
                #print STDERR $@;
                # ошибка от api
                push @{$res->{errors}}, {
                    host => $host, 
                    host_cname => $host_name,
                    start => 'error',
                    end => 'error', 
                };
            } elsif(!$d){
                # нет известных даунтаймов
                push @{$res->{ok}}, {
                    host => $host, 
                    host_cname => $host_name,
                    start => '-', 
                    end => '-', 
                    no_downtime => 1,
                };
            } else {
                for my $dt (@$d){
                    # есть даунтаймы
                    my $start = strftime("%Y-%m-%d %H:%M", localtime($dt->[0]));
                    my $end;
                    if ( $dt->[1] =~ /^[0-9]+$/ ){
                        $end = strftime("%Y-%m-%d %H:%M", localtime($dt->[1]));
                    } else {
                        $end = "-";
                    }
                    my $h = {
                        host => $host,
                        host_cname => $host_name,
                        start => $start,
                        end => $end,
                    };
                    if ( $dt->[1] =~ /^[0-9]+$/ && $now > $dt->[1] ){
                        $h->{descr} = "PAST";
                    } elsif ( $now >= $dt->[0] && $now <= $dt->[1] ){
                        $h->{descr} = "NOW";
                    } elsif ( $dt->[0] - $now < 6*3600 ){
                        $h->{descr} = "COMING";
                    } elsif ( $dt->[0] - $now < 24*3600 && (localtime($now))[3] == (localtime($d->[0]))[3] ){
                        $h->{descr} = "TODAY";
                    } elsif ( $dt->[0] - $now < 2*24*3600 && (localtime($now))[3] != (localtime($d->[0]))[3] ){
                        # Определение "завтра неверное, захватывает лишний кусок предыдущего дня"
                        $h->{descr} = "TOMORROW";
                    }

                    push @{$res->{downtimes}}, $h;
                }
            }
        }
        $res->{$_} ||= [] for qw/downtimes ok errors/;
        push @downtimes, @{$res->{downtimes}}, @{$res->{ok}};

        if ( !@{$res->{downtimes}} && ! @{$res->{ok}} ){
            push @downtimes, @{$res->{errors}};
        }
    }

    $_->{descr} //= '' for @downtimes;
    @downtimes = sort { $a->{start} cmp $b->{start} } @downtimes;

    return \@downtimes;
}


sub print_downtimes
{
    my ($downtimes) = @_;

    for my $d (@$downtimes){
        print join "    ", sprintf("%16s", $d->{start}), sprintf("%16s", $d->{end}), sprintf("%16s", $d->{descr}), sprintf("%16s", $d->{host}), ( $d->{host_cname} eq $d->{host} ? () : "($d->{host_cname})" );
        print "\n";
    }

    return;
}


# получает сложную структуру из хешей и массивов, обходит все, выбирает все значения по ключам "host" на любом уровне
# предполагается, что по любому ключу host находится либо строка, либо массив строк
# возвращает плоский список строк
sub extract_hosts
{
    if( ref $_[0] eq "ARRAY" ){
        return ( map { extract_hosts($_) } @{$_[0]} );
    } 
    return () unless ref $_[0] eq 'HASH'; 
    # host может быть пустой строкой
    return ( ( $_[0]->{host} ? (to_arr($_[0]->{host})) : () ), map { extract_hosts($_) } values %{$_[0]} );
};


# получает либо простой скаляр, либо ссылку на массив, возвращает натуральный массив
sub to_arr
{
    if(ref $_[0] eq "ARRAY"){
        return ( @{$_[0]} );
    } else {
        return ( $_[0] );
    } 
} 

