package HostInfo;

use POSIX;
use Sys::Hostname;
use Time::Local;

use strict;

BEGIN {
    eval {require Time::HiRes; import Time::HiRes "time"};
}

use Sysinfo;
use LogUtils;
use InfoChecker;

our $host_options_schema = {
        x509_host_cert => '*',
        x509_cert_dir  => '*',
        wakeupperiod   => '*',
        processes      => [ '' ],
        ports => {
           '*' => [ '*' ] #process name, ports
        },
        localusers     => [ '' ],
        control => {
            '*' => {
                sessiondir => [ '' ],
                cachedir => [ '*' ],
                cachesize => '*'
            }
        },
        remotegmdirs   => [ '*' ]
};

our $host_info_schema = {
        hostname      => '',
        osname        => '*', # see OSName_t, GFD.147
        osversion     => '*', # see OSName_t, GFD.147
        sysname       => '',  # uname -s
        release       => '',  # uname -r
        machine       => '',  # uname -m (what would it be on a linux machine)
        cpuvendor     => '*',
        cpumodel      => '*',
        cpufreq       => '*', # unit: MHz
        cpustepping   => '*',
        pmem          => '*', # unit: MB
        vmem          => '*', # unit: MB
        cputhreadcount=> '*',
        cpucorecount  => '*',
        cpusocketcount=> '*',
        issuerca      => '',
        issuerca_hash => '',
        issuerca_enddate => '*',
        issuerca_expired => '*',
        hostcert_enddate => '*',
        hostcert_expired => '*',
        trustedcas    => [ '' ],
        session_free  => '', # unit: MB
        session_total => '', # unit: MB
        cache_free    => '', # unit: MB
        cache_total   => '', # unit: MB
        globusversion => '*',
        processes  => { '*' => '' },
        ports      => {
             '*' => {  # process name
                 '*' => [ '' ]    # port -> [port status, error message ]
             }
        },
        gm_alive      => '',
        localusers => {
            '*' => {
                gridareas => [ '' ],
                diskfree => '' # unit: MB
            }
        },
};

our $log = LogUtils->getLogger(__PACKAGE__);

{   my ($t0, $descr);
    sub timer_start($) { $descr = shift; $t0 = time(); }
    sub timer_stop() {
        my $dt = sprintf("%.3f", time() - $t0);
        $log->debug("Time spent $descr: ${dt}s");
}   }


sub collect($) {
    my ($options) = @_;
    my ($checker, @messages);

    $checker = InfoChecker->new($host_options_schema);
    @messages = $checker->verify($options);
    $log->warning("config key options->$_") foreach @messages;
    $log->fatal("Some required options are missing") if @messages;

    my $result = get_host_info($options);

    $checker = InfoChecker->new($host_info_schema);
    @messages = $checker->verify($result);
    $log->debug("SelfCheck: result key hostinfo->$_") foreach @messages;

    return $result;
}


# private subroutines

# Obtain the end date of a certificate (in seconds since the epoch)
sub enddate {
    my ($openssl, $certfile) = @_;
    # assuming here that the file exists and is a well-formed certificate.
    my $stdout =`$openssl x509 -noout -enddate -in '$certfile' 2>&1`;
    if ($?) {
       $log->info("openssl error: $stdout");
       return undef;
    }
    chomp ($stdout);


    my %mon = (Jan=>0,Feb=>1,Mar=>2,Apr=>3,May=>4,Jun=>5,Jul=>6,Aug=>7,Sep=>8,Oct=>9,Nov=>10,Dec=>11);
    if ($stdout =~ m/notAfter=(\w{3})  ?(\d\d?) (\d\d):(\d\d):(\d\d) (\d{4}) GMT/ and exists $mon{$1}) {
        return timegm($5,$4,$3,$2,$mon{$1},$6);
    } else {
        $log->warning("Unexpected -enddate from openssl for $certfile");
        return undef;
    }
}

sub get_ports_info {
    my ($processes, $ports) = @_;

    my $portsstatus = {};

    my $errormessage = '';
    
    # Assume user is root
    my $userisroot = 1;    
    if ($> != 0) {
        $userisroot = 0;
        $errormessage = "Checking if ARC ports are open: user ".getpwuid($>)." cannot access process names. Infosys will assume AREX interfaces are running properly;";
        $log->verbose($errormessage);
    }
    
    my $netcommand = '';
    my $stdout = '';
    
    # check if to use either netstat or ss
    if ($userisroot) {
        for my $path (split ':', "$ENV{PATH}") {
            $netcommand = "$path/netstat" and last if -x "$path/netstat";
            $netcommand = "$path/ss" and last if -x "$path/ss";
        }
        
        
        if ($netcommand eq '') {
           $errormessage = $errormessage." Could not find neither netstat nor ss command, cannot probe open ports, assuming services are up;";
           $log->verbose("Could not find neither netstat nor ss command, cannot probe open ports, assuming services are up");
        } else {
            # run net command
            $stdout = `$netcommand -antup 2>&1`;
            if ($?) {
               $errormessage = $errormessage." $netcommand error: $stdout";
               $log->info("$netcommand error: $stdout");
               return undef;
            }
        }
        chomp ($stdout);
    }
    
    foreach my $process (@$processes) {
       my $procports = $ports->{$process};
       foreach my $port (@$procports) {
           if ( $stdout =~ m/$port.*$process/ or $netcommand eq '' or $userisroot == 0 ) {
                $portsstatus->{$process}{$port} = ['ok', $errormessage ]; 
           } else {
                my $porterrormessage = $errormessage. " $netcommand: process $process is not listening on port $port;";
                $portsstatus->{$process}{$port} = ['critical', $porterrormessage ]; 
           }
              
        }
    } 
    return $portsstatus;
}

# Hostcert, issuer CA, trustedca, issuercahash, enddate ...
sub get_cert_info {
    my ($options, $globusloc) = @_;

    my $host_info = {};

    if (not $options->{x509_host_cert}) {
        $log->info("x509_host_cert not configured");
        return $host_info;
    }

    # find an openssl
    my $openssl = '';
    for my $path (split ':', "$ENV{PATH}:$globusloc/bin") {
        $openssl = "$path/openssl" and last if -x "$path/openssl";
    }
    $log->error("Could not find openssl command") unless $openssl;

    # Inspect host certificate
    my $hostcert = $options->{x509_host_cert};
    chomp (my $issuerca = `$openssl x509 -noout -issuer -nameopt oneline -in '$hostcert' 2>&1`);
    if ($?) {
        $log->warning("Failed processing host certificate file: $hostcert, openssl error: $issuerca") if $?;
    } else {
        $issuerca =~ s/, /\//g;
        $issuerca =~ s/ = /=/g;
        $issuerca =~ s/^[^=]*= */\//;
        $host_info->{issuerca} = $issuerca;
        $host_info->{hostcert_enddate} = enddate($openssl, $hostcert);
        system("$openssl x509 -noout -checkend 3600 -in '$hostcert' >/dev/null 2>&1");
        $host_info->{hostcert_expired} = $? ? 1 : 0;
        $log->warning("Host certificate is expired in file: $hostcert") if $?;
    }

    if (not $options->{x509_cert_dir}) {
        $log->info("x509_cert_dir not configured");
        return $host_info;
    }

    # List certs and elliminate duplication in case 2 soft links point to the same file.
    my %certfiles;
    my $certdir = $options->{x509_cert_dir};
    opendir(CERTDIR, $certdir) or $log->error("Failed listing certificates directory $certdir: $!");
    for (readdir CERTDIR) {
        next unless m/\.\d$/;
        my $file = $certdir."/".$_;
        my $link = -l $file ? readlink $file : $_;
        $certfiles{$link} = $file;
    }
    closedir CERTDIR;

    my %trustedca;
    foreach my $cert ( sort values %certfiles ) {
       chomp (my $ca_sn = `$openssl x509 -checkend 3600 -noout -subject -nameopt oneline -in '$cert'`);
       my $is_expired = $?;
       $ca_sn = (split(/\n/, $ca_sn))[0];
       $ca_sn =~ s/, /\//g;
       $ca_sn =~ s/ = /=/g;
       $ca_sn =~ s/^[^=]*= */\//;
       if ($ca_sn eq $issuerca) {
           chomp (my $issuerca_hash = `$openssl x509 -noout -hash -in '$cert'`);
           if ($?) {
               $log->warning("Failed processing issuer CA certificate file: $cert");
           } else {
               $host_info->{issuerca_hash} = $issuerca_hash || undef;
               $host_info->{issuerca_enddate} = enddate($openssl, $cert);
               $host_info->{issuerca_expired} = $is_expired ? 1 : 0;
               $log->warning("Issuer CA certificate is expired in file: $cert") if $is_expired;
           }
       }
       $log->warning("Certificate is expired for CA: $ca_sn") if $is_expired;
       $trustedca{$ca_sn} = 1 unless $is_expired;
    }
    $host_info->{trustedcas} = [ sort keys %trustedca ];
    $log->warning("Issuer CA certificate file not found") unless exists $host_info->{issuerca_hash};

    return $host_info;
}


# Returns 'all'  if all grid-managers are up
#         'some' if one or more grid-managers are down
#         'none' if all grid-managers are down
sub gm_alive {
    my ($timeout, @controldirs) = @_;

    my $up = 0;
    my $down = 0;
    for my $dir (@controldirs) {
        my @stat = stat("$dir/gm-heartbeat");
        if (@stat and time() - $stat[9] < $timeout) {
            $up++;
        } else {
            $down++;
        }
    }
    return 'none' if not $up;
    return $down ? 'some' : 'all';
}


sub get_host_info {
    my $options = shift;

    my $host_info = {};

    $host_info->{hostname} = hostname();

    my $osinfo = Sysinfo::osinfo() || {};
    my $cpuinfo = Sysinfo::cpuinfo() || {};
    my $meminfo = Sysinfo::meminfo() || {};
    $log->error("Failed querying CPU info") unless %$cpuinfo;
    $log->error("Failed querying OS info") unless %$osinfo;

    # Globus location
    my $globusloc = $ENV{GLOBUS_LOCATION} || "/usr";
    if ($ENV{GLOBUS_LOCATION}) {
        if ($ENV{LD_LIBRARY_PATH}) {
            $ENV{LD_LIBRARY_PATH} .= ":$ENV{GLOBUS_LOCATION}/lib";
        } else {
            $ENV{LD_LIBRARY_PATH} = "$ENV{GLOBUS_LOCATION}/lib";
        }
    }

    timer_start("collecting certificates info");
    my $certinfo = get_cert_info($options, $globusloc);
    timer_stop();

    $host_info = {%$host_info, %$osinfo, %$cpuinfo, %$meminfo, %$certinfo};

    my @controldirs;
    my $control = $options->{control};
    push @controldirs, $_->{controldir} for values %$control;

    # Considering only common session disk space (not including per-user session directoires)
    my (%commongridareas, $commonfree);
    if ($control->{'.'}) {
        $commongridareas{$_} = 1 for map { my ($path, $drain) = split /\s+/, $_; $path; } @{$control->{'.'}{sessiondir}};
    }
    # Also include remote session directoires.
    if (my $remotes = $options->{remotegmdirs}) {
        for my $remote (@$remotes) {
            my ($ctrldir, @sessions) = split ' ', $remote;
            $commongridareas{$_} = 1 for grep { $_ ne 'drain' } @sessions;
            push @controldirs, $ctrldir;
        }
    }
    if (%commongridareas) {
        my %res = Sysinfo::diskspaces(keys %commongridareas);
        if ($res{errors}) {
            $log->warning("Failed checking disk space available in session directories");
        } else {
            $host_info->{session_free} = $commonfree = $res{freesum};
            $host_info->{session_total} = $res{totalsum};
        }
    }

    # calculate free space on the sessionsirs of each local user.
    my $user = $host_info->{localusers} = {};

    foreach my $u (@{$options->{localusers}}) {

        # Are there grid-manager settings applying for this local user?
        if ($control->{$u}) {
            my $sessiondirs = [ map { my ($path, $drain) = split /\s+/, $_; $path; } @{$control->{$u}{sessiondir}} ];
            my %res = Sysinfo::diskspaces(@$sessiondirs);
            if ($res{errors}) {
                $log->warning("Failed checking disk space available in session directories of user $u")
            } else {
                $user->{$u}{gridareas} = $sessiondirs;
                $user->{$u}{diskfree} = $res{freesum};
            }
        } elsif (defined $commonfree) {
            # default for other users
            $user->{$u}{gridareas} = [ keys %commongridareas ];
            $user->{$u}{diskfree} = $commonfree;
        }
    }

    # Considering only common cache disk space (not including per-user caches)
    if ($control->{'.'}) {
        my $cachedirs = $control->{'.'}{cachedir} || [];
        my ($cachemax, $cachemin) = split " ", $control->{'.'}{cachesize} if defined $control->{'.'}{cachesize};
        my @paths = map { my @pair = split " ", $_; $pair[0] } @$cachedirs;
        if (@paths) {
            my %res = Sysinfo::diskspaces(@paths);
            if ($res{errors}) {
                $log->warning("Failed checking disk space available in common cache directories")
            } else {
                # What to publish as CacheFree if there are multiple cache disks?
                # HighWatermark is factored in
                # Only accurate if caches are on filesystems of their own
                $host_info->{cache_total} = (defined $cachemax) ? $res{totalsum}*$cachemax/100 : $res{totalsum};
                $host_info->{cache_total} = int $host_info->{cache_total};
                # Opting to publish the least free space on any of the cache
                # disks -- at least this has a simple meaning and is useful to
                # diagnose if a disk gets full -- but upper limit is 
                # the max space usable calculated above, for consistency
                $host_info->{cache_free} = ($res{freemin} >= $host_info->{cache_total}) ? $host_info->{cache_total} : $res{freemin};
                $host_info->{cache_free} = int $host_info->{cache_free};
            }
        }
    }

    my $gm_timeout = $options->{wakeupperiod}
                   ? $options->{wakeupperiod} * 10
                   : 1800;
    $host_info->{gm_alive} = gm_alive($gm_timeout, @controldirs);

    #Globus Toolkit version
    #globuslocation/share/doc/VERSION
    my $globusversion;
    if (-r "$globusloc/share/doc/VERSION" ) {
       chomp ( $globusversion =  `cat $globusloc/share/doc/VERSION 2>/dev/null`);
       if ($?) { $log->warning("Failed reading the globus version file")}
    }
    #globuslocation/bin/globus-version
    elsif (-x "$globusloc/bin/globus-version" ) {
       chomp ( $globusversion =  `$globusloc/bin/globus-version 2>/dev/null`);
       if ($?) { $log->warning("Failed running $globusloc/bin/globus-version command")}
    }
    $host_info->{globusversion} = $globusversion if $globusversion;

    $host_info->{processes} = Sysinfo::processid(@{$options->{processes}});

    $host_info->{ports} = get_ports_info($options->{processes},$options->{ports});

    return $host_info;
}


#### TEST ##### TEST ##### TEST ##### TEST ##### TEST ##### TEST ##### TEST ####

sub test {
    my $options = { x509_host_cert => '/etc/grid-security/testCA-hostcert.pem',
                    x509_cert_dir => '/etc/grid-security/certificates',
                    control => {
                        '.' => {
                            sessiondir => [ '/home', '/boot' ],
                            cachedir => [ '/home' ],
                            cachesize => '60 80',
                        },
                        'daemon' => {
                            sessiondir => [ '/home', '/tmp' ],
                        }
                    },
                    remotegmdirs => [ '/dummy/control /home',
                                      '/dummy/control /boot' ],
                    libexecdir => '/usr/libexec/arc',
                    runtimedir => '/home/grid/runtime',
                    processes => [ qw(bash ps init grid-manager bogous cupsd slapd) ],
                    ports => {
                        cupsd => ['631'],
                        gridftpd => ['3811'],
                        slapd => ['133','389','2135']
                    },
                    localusers => [ qw(root bin daemon) ] };
    require Data::Dumper; import Data::Dumper qw(Dumper);
    LogUtils::level('DEBUG');
    $log->debug("Options:\n" . Dumper($options));
    my $results = HostInfo::collect($options);
    $log->debug("Results:\n" . Dumper($results));
}

#test;

1;