#!/usr/bin/perl

#######################################################################
#
# argonaut-fai-monitor - read status of installation and send information
# to argonaut-server for FusionDirectory
#
# Copyright (C) 2014-2016 FusionDirectory project
#
# Using code from fai-monitor:
#   Copyright (C) 2003-2012 by Thomas Lange
#
# Author: Côme BERNIGAUD
#
#  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; either version 2 of the License, or
#  (at your option) any later version.
#
#  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, write to the Free Software
#  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
#
#######################################################################

use strict;
use warnings;

use 5.008;

use Argonaut::Libraries::Common qw(:ldap :file :config :string);

use if (USE_LEGACY_JSON_RPC),     'JSON::RPC::Legacy::Client';
use if not (USE_LEGACY_JSON_RPC), 'JSON::RPC::Client';
use Log::Handler;
use Socket;
use Getopt::Std;

use App::Daemon qw(daemonize);

my ($port, $timeout);
our ($opt_h,$opt_p,$opt_t);

my $logfile = "argonaut-fai-monitor.log";
my $piddir = "/var/run/argonaut";
my $pidfile = "argonaut-fai-monitor.pid";

my $config = argonaut_read_config;

my $settings        = argonaut_get_generic_settings(
  'argonautFAIMonitorConfig',
  {
    'logdir'      => "argonautFAIMonitorLogDir",
    'cacertfile'  => "argonautFAIMonitorCaCertPath",
    'port'        => "argonautFAIMonitorPort",
    'timeout'     => "argonautFAIMonitorTimeout"
  },
  $config,$config->{'client_ip'}
);

my $logdir          = $settings->{'logdir'};

my $server_settings = argonaut_get_server_settings($config,$config->{'server_ip'});

my $server_port   = $server_settings->{'port'};
my $protocol      = $server_settings->{'protocol'};

my %taskids;

my %progress_value = (
  "confdir"   =>  0,
  "setup"     =>  1,
  "defclass"  =>  2,
  "defvar"    =>  3,
  "action"    =>  4,
  "install"   =>  5,
  "partition" =>  6,
  "extrbase"  =>  7,
  "debconf"   =>  15,
  "prepareapt"=>  16,
  "updatebase"=>  17,
  "instsoft"  =>  18,
  "configure" =>  80,
  "savelog"   =>  90
);

$App::Daemon::pidfile = "$piddir/$pidfile";
$App::Daemon::logfile = "$logdir/$logfile";
$App::Daemon::as_user = "root";

argonaut_create_dir($logdir);

daemonize();

my $log = Log::Handler->create_logger("argonaut-fai-monitor");

$log->add(
  file => {
    filename => "$logdir/$logfile",
    maxlevel => "debug",
    minlevel => "emergency"
  }
);

sub get_id
{
  my $host = shift;
  unless (exists $taskids{$host}) {
    my $taskid = rpc_call(
      "get_host_id",
      [$host, '(objectClass=FAIobject)']
    );

    if($taskid) {
      if ($taskid->is_error) {
        die "Error for host '$host': ", $taskid->error_message."\n";
      } else {
        $taskids{$host} = $taskid->content->{result};
      }
    } else {
      die "Error while trying to contact Argonaut server\n";
    }
  }
  return $taskids{$host};
}

sub rpc_call
{
  my ($method, $params) = @_;

  my $client;
  if (USE_LEGACY_JSON_RPC) {
    $client = new JSON::RPC::Legacy::Client;
  } else {
    $client = new JSON::RPC::Client;
  }
  $client->version('1.0');
  if ($protocol eq 'https') {
    if ($client->ua->can('ssl_opts')) {
      $client->ua->ssl_opts(
        verify_hostname   => 1,
        SSL_ca_file       => $settings->{'cacertfile'},
        SSL_verifycn_name => $server_settings->{'certcn'}
      );
    }
    $client->ua->credentials($config->{'server_ip'}.":".$server_port, "JSONRPCRealm", "", argonaut_gen_ssha_token($server_settings->{'token'}));
  }

  my $callobj = {
    method  => "$method",
    params  => $params,
  };

  my $res = $client->call($protocol."://".$config->{'server_ip'}.":".$server_port, $callobj);

  if($res) {
    if ($res->is_error) {
      $log->error("Error : ".$res->error_message);
      print "Error : ", $res->error_message."\n";
    }
  } else {
    $log->error("Error while trying to contact Argonaut server : ".$client->status_line);
    print "Error while trying to contact Argonaut server : ".$client->status_line."\n";
  }

  return $res;
}

sub parse_line
{
  my ($line) = @_;

  chomp $line;

  my ($host,$keyword,$taskname,$errorcode) = split(/\s+/,$line);

  if(($keyword eq "TASKBEGIN") && ($taskname eq "confdir")) {
    # Clear cache as this is a new task
    delete $taskids{$host};
  }

  my $taskid = eval {get_id($host);};
  if ($@) {
    if (($keyword eq "TASKBEGIN") ||
        ($keyword eq "TASKERROR") ||
        (($keyword eq "TASKEND") && ($taskname eq "faiend"))) {
      print "Could not find taskid for line '$line':\n $@\n";
      $log->error($@);
      return;
    } else {
      $log->debug($@);
    }
  }

  if($keyword eq "TASKBEGIN") {
    print "[monitor:$host] Task $taskname begun\n";

    my $progress = undef;
    if(defined $progress_value{$taskname}) {
      $progress = $progress_value{$taskname};
    }

    rpc_call(
      "set_task_substatus",
      [$taskid,$taskname,$progress]
    );
  } elsif($keyword eq "TASKEND") {
    print "[monitor:$host] Task $taskname ended\n";

    if($taskname eq "faiend") {
      rpc_call(
        "set_task_substatus",
        [$taskid,$taskname,100]
      );
      delete $taskids{$host};
    }
  } elsif($keyword eq "TASKERROR") {
    print "[monitor:$host] Task error $taskname $errorcode\n";

    rpc_call(
      "set_error",
      [$taskid,$taskname." ".$errorcode],
    );
    delete $taskids{$host};
  } else {
    print "$line\n";
  }
}

sub server_init
{
  my ($port) = @_;
  $log->info("Argonaut FAI monitoring daemon starting..\n") or die "log: $!";

  # Listen
  my $proto = getprotobyname('tcp');
  socket(SERVER, PF_INET, SOCK_STREAM, $proto) or die "socket: $!";
  setsockopt(SERVER, SOL_SOCKET, SO_REUSEADDR, 1) or die "setsockopt: $!";

  my $paddr = sockaddr_in($port, INADDR_ANY);

  bind(SERVER, $paddr) or die "bind: $!";
  listen(SERVER, SOMAXCONN) or die "listen: $!";
  $log->info("Argonaut FAI monitoring daemon started on port $port with pid $$\n") or die "log: $!";
}

sub big_loop
{
  # accept a connection, print message received and close
  my ($client_addr);
  while ($client_addr = accept(CLIENT, SERVER)) {
    my ($port, $iaddr) = sockaddr_in($client_addr);
    my $ip = inet_ntoa($iaddr);

    my $inp = '';

    eval {
      local $SIG{__DIE__};
      local $SIG{__WARN__};
      local $SIG{'ALRM'} = sub { die("Timeout"); };

      alarm($timeout);
      $inp = <CLIENT>;
      alarm(0);
    };

    close CLIENT;

    if (!defined($inp) || $inp eq '') {
      # Client did not send anything, or alarm went off
      $log->info("$ip:$port: No data or timeout.\n") or die "log: $!";
      next;
    }

    parse_line($inp);
  }

  $log->error("accept returned: $!\n");
}

sub usage
{

  print << "EOF";
argonaut-fai-monitor, Argonaut FAI monitor daemon.

Usage: argonaut-fai-monitor [OPTIONS]

    -p PORT             Set port to listen to. Default is 4711.
    -t TIMEOUT          Timeout for bad clients. 0 to disable.

EOF
  exit 0;
}

getopts('hp:t:') || usage;
$opt_h && usage;
$port = $opt_p || $settings->{'port'};
if (defined $opt_t) {
  $timeout = $opt_t;
} else {
  $timeout = $settings->{'timeout'};
}

server_init($port);
big_loop;

__END__


=head1 NAME

argonaut-fai-monitor - read status of installation and send information to argonaut-server for FusionDirectory

=head1 SYNOPSIS

argonaut-fai-monitor [OPTIONS]

=head1 DESCRIPTION

argonaut-fai-monitor replaces fai-monitor and send information to argonaut-server for FusionDirectory to
show them in deployment queue

=head1 OPTIONS

=over 2

=item B<p port>

Set port to listen to. Default is 4711.

=item B<t timeout>

Timeout for bad clients. 0 to disable.

=back

=head1 BUGS

Please report any bugs, or post any suggestions, to the fusiondirectory mailing list fusiondirectory-users or to
<https://forge.fusiondirectory.org/projects/argonaut-agents/issues/new>

=head1 LICENCE AND COPYRIGHT

This code is part of Argonaut Project <https://www.argonaut-project.org>

=over 3

=item Copyright (C) 2011-2016 FusionDirectory project

=back

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.

=cut

# vim:ts=2:sw=2:expandtab:shiftwidth=2:syntax:paste

