#!/usr/bin/perl -w
# #
# Software subject to following license(s):
#   Apache 2 License (http://www.opensource.org/licenses/apache2.0)
#   Copyright (c) Responsible Organization
#

# #
# Current developer(s):
#   Luis Fernando Muñoz Mejías <Luis.Munoz@UGent.be>
#

# #
# Author(s): Germán Cancio Meliá, Marco Emilio Poleggi
#

# #
# cdp-listend, 17.12.0-rc2, rc2_1, Tue Dec 19 2017
#


use strict;
use warnings;

BEGIN {
  # use perl libs in /usr/lib/perl
  unshift(@INC, '/usr/lib/perl');
}


use IO::Socket;
use IO::Select;
use POSIX;
use Sys::Syslog qw( :DEFAULT setlogsock);
use AppConfig;
use Sys::Hostname;

#######################################################################
# configuration variables
#######################################################################

my $NULL = undef;
my $hostname = undef;
my $myaddr = undef;
my $my_ip_addr = undef;
my $port = undef;
my $s= undef;
my $pid = undef;
my $ip1 = undef;
my $r_ipaddr = undef;
my $r_host = undef;
my $newmsg = undef;
my $priority = undef;
my $msg = undef;
my $fetch = undef;
my $nch   = undef;
my $fetch_offset = undef;
my $fetch_smear = undef;
my $nch_smear = undef;
my $facility = undef;
my $pidfile = undef;
my $hostname;

my $fetch_def  = "/usr/sbin/ccm-fetch";
my $nch_def    = "/usr/sbin/cdbsync-nch";
my $config_def = "/etc/cdp-listend.conf";
my $port_def   = 7777;
my $fetch_offset_def = 0;
my $fetch_smear_def  = 300;
my $nch_smear_def    = 0;
my $hostname_def = hostname();


my $config = AppConfig->new({
    PEDANTIC => 1,
    CASE   => 1,
});

$config->define("help|h!",
        "config|c=s",{DEFAULT=>$config_def},
        "port|p=i",{DEFAULT=>$port_def},
        "fetch|f=s",{DEFAULT=>$fetch_def},
        "nch|n=s",{DEFAULT=>$nch_def},
        "fetch_offset=i",{DEFAULT=>$fetch_offset_def},
        "fetch_smear=i",{DEFAULT=>$fetch_smear_def},
        "nch_smear=i",{DEFAULT=>$nch_smear_def},
        "facility=s", {DEFAULT=>"daemon"},
        "pidfile=s", {DEFAULT=>"/var/run/cdp-listend.pid"},
        "hostname=s", {DEFAULT=>$hostname_def},
         );
if (!($config->getopt()) || $config->get("help")) {
print "usage: $0 [OPTIONS]
  -h, --help              print this message
  -c, --config=FILE       config file location
  -p, --port=PORT         listen port number
  -f, --fetch=PROG        ccm-fetch program
  -n, --nch=PROG          nch program
  --fetch_offset=SECONDS  fetch run offset time
  --fetch_smear=SECONDS   fetch run smearing time
  --nch_smear=SECONDS     nch run smearing time
  --pidfile=FILE          write PID to FILE
  --hostname=fqdn         hostname/IP to listen on\n";
  exit(1);
}

logit("info", "Starting $0 with configuration file " . $config->config());

if (-f $config->config()) {
    $config->file($config->config());
}

$port        = $config->port();
$fetch       = $config->fetch();
$nch         = $config->nch();
$fetch_offset = $config->fetch_offset();
$fetch_smear = $config->fetch_smear();
$nch_smear   = $config->nch_smear();
$facility   = $config->facility();
$pidfile   = $config->pidfile();
$hostname   = $config->hostname();


$s = new IO::Select;

if ($hostname =~ m/^((localhost)+\.*(localdomain)*|127(\.\d+){3})/) {
    logit('err',"Exiting: hostname set to $hostname: $!");
    die "Hostname incorrectly set as $hostname\n";
} elsif ($hostname =~ m/^\d+(\.\d+){3}$/) {
    # cannot be a hostname, and IO::Socket::INET will fail if it's not valid
    $my_ip_addr = $hostname;
} else {
    my ($name,$aliases,$addrtype,$length,@addrs) = gethostbyname($hostname);
    if(@addrs) {
        $myaddr = $addrs[0];
    } else {
        my $err_msg = "failed to retrieve addr from gethostbyname($hostname): ec $?";
        logit('err', "Exiting: $err_msg");
        die "$err_msg\n";
    }

    my @my_ip = unpack("C4", $myaddr);
    $my_ip_addr  = join(".", @my_ip);
}

unless ($ip1=IO::Socket::INET->new(LocalPort => $port, Proto=>'udp',
                                   LocalAddr => $my_ip_addr)) {
    logit('err',"Exiting: error creating UDP listener: $@");
    die "error creating UDP listener for $my_ip_addr  $@\n";
}
$s->add($ip1);


$pid = fork;
exit if $pid;
if(!(defined($pid))) {
    logit('err',"Exiting: daemon couldn't fork: $!");
    die "Daemon couldn't fork: $!";
}
POSIX::setsid();
exit if fork();

# Save the PID.
if ( $pidfile ) {
    if ( ! open(PIDFILE,">".$pidfile) ) {
        logit('err',"Cannot write PID to file \"".
             $pidfile."\": $! : Exiting");
        exit(-1);
    }
    print(PIDFILE "$$");
}

logit('info',"$0 daemon successfully started on port $port");

open(STDIN,  "+>/dev/null");
open(STDOUT, "+>&STDIN");
open(STDERR, "+>&STDIN");

#
# do an initial call of fetch and nch (if installed)
#
if (defined $fetch && -x $fetch) {
    logit('info', "Startup invocation of $fetch");
    system ("$fetch") == 0
        or logit('err',"[WARN] Startup invocation of ccm-fetch ($fetch) failed: $!");
}

if (defined $nch && -x $nch) {
    logit('info', "Startup invocation of $nch");
    system ("$nch") == 0
        or logit('err',"[WARN] Startup invocation of cdbsync-nch ($nch) failed: $!");
}

#
# now wait for notifications
#
while($ip1->recv($newmsg,1024)) {
    chomp($newmsg);
    ($NULL,$r_ipaddr) = sockaddr_in($ip1->peername);
    $r_host = inet_ntoa($r_ipaddr);
    $newmsg =~ /^(\w+).(\d+)$/;
    my $ntype = $1;
    my $time  = $2;
    logit('info',"Received UDP packet ($ntype|$time) from $r_host");
    # time extraction!
    if ($ntype eq"ccm") {
        my $smear = int(rand($fetch_smear));
        my $delay = $smear + $fetch_offset;
        logit('info', "$fetch will be called in $delay seconds (offset=$fetch_offset, smear=$smear)");
        sleep($delay);
        logit('info', "Calling $fetch with unix time $time (after $delay seconds)");
        system ("$fetch --profile-time=$time") == 0
            or logit('err',"[ERROR] call of ccm-fetch ($fetch) failed: $!");
    } elsif ($ntype eq "cdb") {
        my $smear = int(rand($nch_smear));
        logit('info', "$nch will be called in $smear seconds");
        sleep($smear);
        logit('info', "Calling $nch with unix time $time (after $smear seconds)");
        system ("$nch") == 0
            or logit('err',"[ERROR] call of cdbsync-nch ($nch) failed: $!");
    }
}

sub logit {
    my ($priority, $msg) = @_;
    return 0 unless ($priority =~ /info|err|debug/);
    setlogsock('unix');
    eval {
        openlog($0, 'pid', $facility);
        syslog($priority, $msg);
    }; warn $@ if $@;
    closelog();
    return 1;
}


__END__

=head1 NAME

cdp-listend - the CDP notfication daemon. listens for UDP packets
and launches ccm-fetch or nch program accordingly to the type
of notification

=head1 DESCRIPTION

This program is a daemon which runs on the local machine waiting to
receive the CDP notification. On receiving a UDP packet containing the
notification type and UNIX time, ccm-fetch is called with the
profile update time as its only argument or nch.

=head1 SYNOPSIS

cdp-listend [OPTIONS]

  -h, --help              print this message
  -c, --config=FILE       config file location
  -p, --port=PORT         listen port number
  -f, --fetch=PROG        ccm-fetch program
  -n, --nch=PROG          nch program
  --fetch_offset=SECONDS   fetch run offset time
  --fetch_smear=SECONDS   fetch run smearing time
  --nch_smear=SECONDS     nch run smearing time\n";
  --facility              syslog facility
  --hostname=fqdn         hostname/IP to listen on

Once initiated the daemon forks to the background. Information and
error messages are written to syslog 'daemon' facility (or to the facility
defined in config file or command line

=head1 AUTHOR

Michael George
University of Liverpool

Piotr Poznanski
CERN

=cut
