Code

Added ArpWatch component.
[gosa.git] / gosa-si / gosa-si-server
1 #!/usr/bin/perl
2 #===============================================================================
3 #
4 #         FILE:  gosa-sd
5 #
6 #        USAGE:  ./gosa-sd
7 #
8 #  DESCRIPTION:
9 #
10 #      OPTIONS:  ---
11 # REQUIREMENTS:  libconfig-inifiles-perl libcrypt-rijndael-perl libxml-simple-perl 
12 #                libdata-dumper-simple-perl libdbd-sqlite3-perl libnet-ldap-perl
13 #                libpoe-perl
14 #         BUGS:  ---
15 #        NOTES:
16 #       AUTHOR:   (Andreas Rettenberger), <rettenberger@gonicus.de>
17 #      COMPANY:
18 #      VERSION:  1.0
19 #      CREATED:  12.09.2007 08:54:41 CEST
20 #     REVISION:  ---
21 #===============================================================================
23 use strict;
24 use warnings;
25 use Getopt::Long;
26 use Config::IniFiles;
27 use POSIX;
28 use Time::HiRes qw( gettimeofday );
30 use Fcntl;
31 use IO::Socket::INET;
32 use IO::Handle;
33 use IO::Select;
34 use Symbol qw(qualify_to_ref);
35 use Crypt::Rijndael;
36 use MIME::Base64;
37 use Digest::MD5  qw(md5 md5_hex md5_base64);
38 use XML::Simple;
39 use Data::Dumper;
40 use Sys::Syslog qw( :DEFAULT setlogsock);
41 use Cwd;
42 use File::Spec;
43 use GOSA::GosaSupportDaemon;
44 use GOSA::DBsqlite;
45 use POE qw(Component::Server::TCP);
47 my $modules_path = "/usr/lib/gosa-si/modules";
48 use lib "/usr/lib/gosa-si/modules";
50 my (%cfg_defaults, $foreground, $verbose, $ping_timeout);
51 my ($bus, $msg_to_bus, $bus_cipher);
52 my ($server, $server_mac_address, $server_events);
53 my ($gosa_server, $job_queue_timeout, $job_queue_table_name, $job_queue_file_name,$job_queue_loop_delay);
54 my ($known_modules, $known_clients_file_name, $known_server_file_name);
55 my ($max_clients);
56 my ($pid_file, $procid, $pid, $log_file);
57 my (%free_child, %busy_child, $child_max, $child_min, %child_alive_time, $child_timeout);
58 my ($arp_activ, $arp_fifo, $arp_fifo_path);
60 # variables declared in config file are always set to 'our'
61 our (%cfg_defaults, $log_file, $pid_file, 
62     $bus_activ, $bus_passwd, $bus_ip, $bus_port,
63     $server_activ, $server_ip, $server_port, $server_passwd, $max_clients,
64     $arp_activ, $arp_fifo_path,
65     $gosa_activ, $gosa_passwd, $gosa_ip, $gosa_port, $gosa_timeout,
66 );
68 # additional variable which should be globaly accessable
69 our $xml;
70 our $server_address;
71 our $bus_address;
72 our $gosa_address;
73 our $no_bus;
74 our $no_arp;
75 our $verbose;
76 our $forground;
77 our $cfg_file;
79 # specifies the verbosity of the daemon_log
80 $verbose = 0 ;
82 # if foreground is not null, script will be not forked to background
83 $foreground = 0 ;
85 # specifies the timeout seconds while checking the online status of a registrating client
86 $ping_timeout = 5;
88 $no_bus = 0;
90 $no_arp = 0;
92 # name of table for storing gosa jobs
93 our $job_queue_table_name = 'jobs';
94 our $job_db;
96 # holds all other gosa-sd as well as the gosa-sd-bus
97 our $known_server_db;
99 # holds all registrated clients
100 our $known_clients_db;
102 %cfg_defaults =
103 ("general" =>
104     {"log_file" => [\$log_file, "/var/run/".$0.".log"],
105     "pid_file" => [\$pid_file, "/var/run/".$0.".pid"],
106     "child_max" => [\$child_max, 10],
107     "child_min" => [\$child_min, 3],
108     "child_timeout" => [\$child_timeout, 180],
109     "job_queue_timeout" => [\$job_queue_timeout, undef],
110     "job_queue_file_name" => [\$job_queue_file_name, '/var/lib/gosa-si/jobs.db'],
111     "job_queue_loop_delay" => [\$job_queue_loop_delay, 3],
112     "known_clients_file_name" => [\$known_clients_file_name, '/var/lib/gosa-si/known_clients.db' ],
113     "known_server_file_name" => [\$known_server_file_name, '/var/lib/gosa-si/known_server.db'],
114    },
115 "bus" =>
116     {"bus_activ" => [\$bus_activ, "on"],
117     "bus_passwd" => [\$bus_passwd, ""],
118     "bus_ip" => [\$bus_ip, "0.0.0.0"],
119     "bus_port" => [\$bus_port, "20080"],
120     },
121 "server" =>
122     {"server_activ" => [\$server_activ, "on"],
123     "server_ip" => [\$server_ip, "0.0.0.0"],
124     "server_port" => [\$server_port, "20081"],
125     "server_passwd" => [\$server_passwd, ""],
126     "max_clients" => [\$max_clients, 100],
127     },
128 "arp" =>
129     {"arp_activ" => [\$arp_activ, "on"],
130     "arp_fifo_path" => [\$arp_fifo_path, "/var/run/gosa-si/arp-notify"],
131     },
132 "gosa" =>
133     {"gosa_activ" => [\$gosa_activ, "on"],
134     "gosa_ip" => [\$gosa_ip, "0.0.0.0"],
135     "gosa_port" => [\$gosa_port, "20082"],
136     "gosa_passwd" => [\$gosa_passwd, "none"],
137     },
138     );
141 #===  FUNCTION  ================================================================
142 #         NAME:  usage
143 #   PARAMETERS:  nothing
144 #      RETURNS:  nothing
145 #  DESCRIPTION:  print out usage text to STDERR
146 #===============================================================================
147 sub usage {
148     print STDERR << "EOF" ;
149 usage: $0 [-hvf] [-c config]
151            -h        : this (help) message
152            -c <file> : config file
153            -f        : foreground, process will not be forked to background
154            -v        : be verbose (multiple to increase verbosity)
155            -no-bus   : starts $0 without connection to bus
156            -no-arp   : starts $0 without connection to arp module
157  
158 EOF
159     print "\n" ;
163 #===  FUNCTION  ================================================================
164 #         NAME:  read_configfile
165 #   PARAMETERS:  cfg_file - string -
166 #      RETURNS:  nothing
167 #  DESCRIPTION:  read cfg_file and set variables
168 #===============================================================================
169 sub read_configfile {
170     my $cfg;
171     if( defined( $cfg_file) && ( length($cfg_file) > 0 )) {
172         if( -r $cfg_file ) {
173             $cfg = Config::IniFiles->new( -file => $cfg_file );
174         } else {
175             print STDERR "Couldn't read config file!\n";
176         }
177     } else {
178         $cfg = Config::IniFiles->new() ;
179     }
180     foreach my $section (keys %cfg_defaults) {
181         foreach my $param (keys %{$cfg_defaults{ $section }}) {
182             my $pinfo = $cfg_defaults{ $section }{ $param };
183             ${@$pinfo[ 0 ]} = $cfg->val( $section, $param, @$pinfo[ 1 ] );
184         }
185     }
189 #===  FUNCTION  ================================================================
190 #         NAME:  logging
191 #   PARAMETERS:  level - string - default 'info'
192 #                msg - string -
193 #                facility - string - default 'LOG_DAEMON'
194 #      RETURNS:  nothing
195 #  DESCRIPTION:  function for logging
196 #===============================================================================
197 sub daemon_log {
198     # log into log_file
199     my( $msg, $level ) = @_;
200     if(not defined $msg) { return }
201     if(not defined $level) { $level = 1 }
202     if(defined $log_file){
203         open(LOG_HANDLE, ">>$log_file");
204         if(not defined open( LOG_HANDLE, ">>$log_file" )) {
205             print STDERR "cannot open $log_file: $!";
206             return }
207             chomp($msg);
208             if($level <= $verbose){
209                 my ($seconds, $minutes, $hours, $monthday, $month,
210                         $year, $weekday, $yearday, $sommertime) = localtime(time);
211                 $hours = $hours < 10 ? $hours = "0".$hours : $hours;
212                 $minutes = $minutes < 10 ? $minutes = "0".$minutes : $minutes;
213                 $seconds = $seconds < 10 ? $seconds = "0".$seconds : $seconds;
214                 my @monthnames = ("Jan", "Feb", "Mar", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec");
215                 $month = $monthnames[$month];
216                 $monthday = $monthday < 10 ? $monthday = "0".$monthday : $monthday;
217                 $year+=1900;
218                 my $name = $0;
219                 $name =~ s/\.\///;
221                 my $log_msg = "$month $monthday $hours:$minutes:$seconds $name $msg\n";
222                 print LOG_HANDLE $log_msg;
223                 if( $foreground ) { 
224                     print STDERR $log_msg;
225                 }
226             }
227         close( LOG_HANDLE );
228     }
229 #log into syslog
230 #    my ($msg, $level, $facility) = @_;
231 #    if(not defined $msg) {return}
232 #    if(not defined $level) {$level = "info"}
233 #    if(not defined $facility) {$facility = "LOG_DAEMON"}
234 #    openlog($0, "pid,cons,", $facility);
235 #    syslog($level, $msg);
236 #    closelog;
237 #    return;
241 #===  FUNCTION  ================================================================
242 #         NAME:  check_cmdline_param
243 #   PARAMETERS:  nothing
244 #      RETURNS:  nothing
245 #  DESCRIPTION:  validates commandline parameter
246 #===============================================================================
247 sub check_cmdline_param () {
248     my $err_config;
249     my $err_counter = 0;
250         if(not defined($cfg_file)) {
251                 $cfg_file = "/etc/gosa-si/server.conf";
252                 if(! -r $cfg_file) {
253                         $err_config = "please specify a config file";
254                         $err_counter += 1;
255                 }
256     }
257     if( $err_counter > 0 ) {
258         &usage( "", 1 );
259         if( defined( $err_config)) { print STDERR "$err_config\n"}
260         print STDERR "\n";
261         exit( -1 );
262     }
266 #===  FUNCTION  ================================================================
267 #         NAME:  check_pid
268 #   PARAMETERS:  nothing
269 #      RETURNS:  nothing
270 #  DESCRIPTION:  handels pid processing
271 #===============================================================================
272 sub check_pid {
273     $pid = -1;
274     # Check, if we are already running
275     if( open(LOCK_FILE, "<$pid_file") ) {
276         $pid = <LOCK_FILE>;
277         if( defined $pid ) {
278             chomp( $pid );
279             if( -f "/proc/$pid/stat" ) {
280                 my($stat) = `cat /proc/$pid/stat` =~ m/$pid \((.+)\).*/;
281                 if( $0 eq $stat ) {
282                     close( LOCK_FILE );
283                     exit -1;
284                 }
285             }
286         }
287         close( LOCK_FILE );
288         unlink( $pid_file );
289     }
291     # create a syslog msg if it is not to possible to open PID file
292     if (not sysopen(LOCK_FILE, $pid_file, O_WRONLY|O_CREAT|O_EXCL, 0644)) {
293         my($msg) = "Couldn't obtain lockfile '$pid_file' ";
294         if (open(LOCK_FILE, '<', $pid_file)
295                 && ($pid = <LOCK_FILE>))
296         {
297             chomp($pid);
298             $msg .= "(PID $pid)\n";
299         } else {
300             $msg .= "(unable to read PID)\n";
301         }
302         if( ! ($foreground) ) {
303             openlog( $0, "cons,pid", "daemon" );
304             syslog( "warning", $msg );
305             closelog();
306         }
307         else {
308             print( STDERR " $msg " );
309         }
310         exit( -1 );
311     }
314 #===  FUNCTION  ================================================================
315 #         NAME:  import_modules
316 #   PARAMETERS:  module_path - string - abs. path to the directory the modules 
317 #                are stored
318 #      RETURNS:  nothing
319 #  DESCRIPTION:  each file in module_path which ends with '.pm' is imported by 
320 #                "require 'file';"
321 #===============================================================================
322 sub import_modules {
323     daemon_log(" ", 1);
325     if (not -e $modules_path) {
326         daemon_log("ERROR: cannot find directory or directory is not readable: $modules_path", 1);   
327     }
329     opendir (DIR, $modules_path) or die "ERROR while loading modules from directory $modules_path : $!\n";
330     while (defined (my $file = readdir (DIR))) {
331         if (not $file =~ /(\S*?).pm$/) {
332             next;
333         }
335         if( $no_arp > 0 ) {
336             if( $file =~ /ArpHandler.pm/ ) {
337                 next;
338             }
339         } 
340         eval { require $file; };
341         if ($@) {
342             daemon_log("ERROR: gosa-si-server could not load module $file", 1);
343             daemon_log("$@", 5);
344         } else {
345             my $mod_name = $1;
346             my $info = eval($mod_name.'::get_module_info()');
347             my ($input_address, $input_key, $input, $input_active, $input_type) = @{$info};
348             $known_modules->{$mod_name} = $info;
350             daemon_log("module $mod_name loaded", 1);
351         }
352     }   
354     # for debugging
355     #while ( my ($module, $tag_hash) = each(%$known_modules)) {
356     #    print "\tmodule: $module"."\n";   
357     #    print "\ttags: ".join(", ", keys(%$tag_hash))."\n";
358     #}
359     close (DIR);
363 #===  FUNCTION  ================================================================
364 #         NAME:  sig_int_handler
365 #   PARAMETERS:  signal - string - signal arose from system
366 #      RETURNS:  noting
367 #  DESCRIPTION:  handels tasks to be done befor signal becomes active
368 #===============================================================================
369 sub sig_int_handler {
370     my ($signal) = @_;
372     daemon_log("shutting down gosa-si-server", 1);
373     exit(1);
375 $SIG{INT} = \&sig_int_handler;
378 #===  FUNCTION  ================================================================
379 #         NAME:  create_known_client
380 #   PARAMETERS:  hostname - string - key for the hash known_clients
381 #      RETURNS:  nothing
382 #  DESCRIPTION:  creates a dummy entry for hostname in known_clients
383 #===============================================================================
384 sub create_known_client {
385     my ($hostname) = @_;
387     my $entry = { table=>'known_clients',
388         hostname=>$hostname,
389         status=>'none',
390         hostkey=>'none',
391         timestamp=>'none',
392         macaddress=>'none',
393         events=>'none',
394     };
395     my $res = $known_clients_db->add_dbentry($entry);
396     if ($res > 0) {
397         daemon_log("ERROR: cannot add entry to known_clients.db: $res", 1);
398     }
400     return;  
403 sub client_input {
404         my ($heap,$input,$wheel) = @_[HEAP, ARG0, ARG1];
406     daemon_log("Incoming msg:\n$input\n", 8);
408         ######################################
409         # forward msg to all imported modules 
410         no strict "refs";
411         my $answer;
412         my %act_modules = %$known_modules;
413         while( my ($module, $info) = each(%act_modules)) {
414                 daemon_log("Processing module ".$module, 3);
415                 my $tmp = &{ $module."::process_incoming_msg" }($input.".".$heap->{remote_ip}."\n");
416                 if (defined $tmp) {
417                         $answer = $tmp;
418             daemon_log("Got answer from module ".$module.": \n".$answer,8);
419                 }
420         }        
421         daemon_log("processing of msg finished", 5);
423         if (defined $answer) {
424                 $heap->{client}->put($answer);
425         } else {
426                 $heap->{client}->put("done\n");
427         }
430 sub trigger_db_loop {
431         my ($kernel) = $_[KERNEL];
432         $kernel->delay_set('watch_for_new_jobs',3);
435 sub watch_for_new_jobs {
436         my ($kernel,$heap) = @_[KERNEL, HEAP];
438         # check gosa job queue for jobs with executable timestamp
439     my $timestamp = &get_time();
441     my $sql_statement = "SELECT * FROM ".$job_queue_table_name.
442         " WHERE status='waiting' AND timestamp<'$timestamp'";
444         my $res = $job_db->select_dbentry( $sql_statement );
446         while( my ($id, $hit) = each %{$res} ) {         
448                 my $jobdb_id = $hit->{id};
449                 my $macaddress = $hit->{macaddress};
450                 my $job_msg_hash = &transform_msg2hash($hit->{xmlmessage});
451                 my $out_msg_hash = $job_msg_hash;
452         my $sql_statement = "SELECT * FROM known_clients WHERE macaddress='$macaddress'";
453                 my $res_hash = $known_clients_db->select_dbentry( $sql_statement );
454                 # expect macaddress is unique!!!!!!
455                 my $target = $res_hash->{1}->{hostname};
457                 if (not defined $target) {
458                         &daemon_log("ERROR: no host found for mac address: $job_msg_hash->{mac}[0]", 1);
459                         &daemon_log("xml message: $hit->{xmlmessage}", 5);
460             my $sql_statement = "UPDATE $job_queue_table_name ".
461                 "SET status='error', result='no host found for mac address' ".
462                 "WHERE id='$jobdb_id'";
463                         my $res = $job_db->update_dbentry($sql_statement);
464                         next;
465                 }
467                 # add target
468                 &add_content2xml_hash($out_msg_hash, "target", $target);
470                 # add new header
471                 my $out_header = $job_msg_hash->{header}[0];
472                 $out_header =~ s/job_/gosa_/;
473                 delete $out_msg_hash->{header};
474                 &add_content2xml_hash($out_msg_hash, "header", $out_header);
476                 # add sqlite_id 
477                 &add_content2xml_hash($out_msg_hash, "jobdb_id", $jobdb_id); 
479                 my $out_msg = &create_xml_string($out_msg_hash);
481                 # encrypt msg as a GosaPackage module
482                 my $cipher = &create_ciphering($gosa_passwd);
483                 my $crypted_out_msg = &encrypt_msg($out_msg, $cipher);
485                 my $error = &send_msg_hash2address($out_msg_hash, "$gosa_ip:$gosa_port", $gosa_passwd);
487                 if ($error == 0) {
488                         my $sql_statement = "UPDATE $job_queue_table_name ".
489                 "SET status='processing', targettag='$target' ".
490                 "WHERE id='$jobdb_id'";
491                         my $res = $job_db->update_dbentry($sql_statement);
492                 } else {
493             my $sql_statement = "UPDATE $job_queue_table_name ".
494                 "SET status='error' ".
495                 "WHERE id='$jobdb_id'";
496                         my $res = $job_db->update_dbentry($sql_statement);
497                 }
498         }
500         $kernel->delay_set('watch_for_new_jobs',3);
504 #==== MAIN = main ==============================================================
505 #  parse commandline options
506 Getopt::Long::Configure( "bundling" );
507 GetOptions("h|help" => \&usage,
508         "c|config=s" => \$cfg_file,
509         "f|foreground" => \$foreground,
510         "v|verbose+" => \$verbose,
511         "no-bus+" => \$no_bus,
512         "no-arp+" => \$no_arp,
513            );
515 #  read and set config parameters
516 &check_cmdline_param ;
517 &read_configfile;
518 &check_pid;
520 $SIG{CHLD} = 'IGNORE';
522 # forward error messages to logfile
523 if( ! $foreground ) {
524     open(STDERR, '>>', $log_file);
525     open(STDOUT, '>>', $log_file);
528 # Just fork, if we are not in foreground mode
529 if( ! $foreground ) { 
530     chdir '/'                 or die "Can't chdir to /: $!";
531     $pid = fork;
532     setsid                    or die "Can't start a new session: $!";
533     umask 0;
534 } else { 
535     $pid = $$; 
538 # Do something useful - put our PID into the pid_file
539 if( 0 != $pid ) {
540     open( LOCK_FILE, ">$pid_file" );
541     print LOCK_FILE "$pid\n";
542     close( LOCK_FILE );
543     if( !$foreground ) { 
544         exit( 0 ) 
545     };
548 daemon_log(" ", 1);
549 daemon_log("$0 started!", 1);
551 # delete old DBsqlite lock files
552 system('rm -f /tmp/gosa_si_lock*');
554 # connect to gosa-si job queue
555 my @job_col_names = ("id", "timestamp", "status", "result", "headertag", "targettag", "xmlmessage", "macaddress");
556 $job_db = GOSA::DBsqlite->new($job_queue_file_name);
557 $job_db->create_table('jobs', \@job_col_names);
559 # connect to known_clients_db
560 my @clients_col_names = ('hostname', 'status', 'hostkey', 'timestamp', 'macaddress', 'events');
561 $known_clients_db = GOSA::DBsqlite->new($known_clients_file_name);
562 $known_clients_db->create_table('known_clients', \@clients_col_names);
564 # connect to known_server_db
565 my @server_col_names = ('hostname', 'status', 'hostkey', 'timestamp');
566 $known_server_db = GOSA::DBsqlite->new($known_server_file_name);
567 $known_server_db->create_table('known_server', \@server_col_names);
569 # import all modules
570 &import_modules;
572 # check wether all modules are gosa-si valid passwd check
574 # create session for repeatedly checking the job queue for jobs
575 POE::Session->create
577         inline_states => {
578                 _start => \&trigger_db_loop,
579                 watch_for_new_jobs => \&watch_for_new_jobs,
580         }
581 );
583 # create socket for incoming xml messages
584 POE::Component::Server::TCP->new
586         Port => $server_port,
587         ClientInput => \&client_input,
588     Concurrency => 10,
589 );
590 daemon_log("start socket for incoming xml messages at port '$server_port' ", 1);
592 POE::Kernel->run();
593 exit;