Code

351878bc6199f9107a3297ce2d34c318459df50c
[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         }
334         eval { require $file; };
335         if ($@) {
336             daemon_log("ERROR: gosa-si-server could not load module $file", 1);
337             daemon_log("$@", 5);
338         } else {
339             my $mod_name = $1;
340             my $info = eval($mod_name.'::get_module_info()');
341             my ($input_address, $input_key, $input, $input_active, $input_type) = @{$info};
342             $known_modules->{$mod_name} = $info;
344             daemon_log("module $mod_name loaded", 1);
345         }
346     }   
348     # for debugging
349     #while ( my ($module, $tag_hash) = each(%$known_modules)) {
350     #    print "\tmodule: $module"."\n";   
351     #    print "\ttags: ".join(", ", keys(%$tag_hash))."\n";
352     #}
353     close (DIR);
357 #===  FUNCTION  ================================================================
358 #         NAME:  sig_int_handler
359 #   PARAMETERS:  signal - string - signal arose from system
360 #      RETURNS:  noting
361 #  DESCRIPTION:  handels tasks to be done befor signal becomes active
362 #===============================================================================
363 sub sig_int_handler {
364     my ($signal) = @_;
366     daemon_log("shutting down gosa-si-server", 1);
367     exit(1);
369 $SIG{INT} = \&sig_int_handler;
372 #===  FUNCTION  ================================================================
373 #         NAME:  create_known_client
374 #   PARAMETERS:  hostname - string - key for the hash known_clients
375 #      RETURNS:  nothing
376 #  DESCRIPTION:  creates a dummy entry for hostname in known_clients
377 #===============================================================================
378 sub create_known_client {
379     my ($hostname) = @_;
381     my $entry = { table=>'known_clients',
382         hostname=>$hostname,
383         status=>'none',
384         hostkey=>'none',
385         timestamp=>'none',
386         macaddress=>'none',
387         events=>'none',
388     };
389     my $res = $known_clients_db->add_dbentry($entry);
390     if ($res > 0) {
391         daemon_log("ERROR: cannot add entry to known_clients.db: $res", 1);
392     }
394     return;  
397 sub client_input {
398         my ($heap,$input,$wheel) = @_[HEAP, ARG0, ARG1];
399         ######################################
400         # forward msg to all imported modules 
401         no strict "refs";
402         my $answer;
403         my %act_modules = %$known_modules;
404         while( my ($module, $info) = each(%act_modules)) {
405                 daemon_log("Processing module ".$module, 3);
406                 my $tmp = &{ $module."::process_incoming_msg" }($input.".".$heap->{remote_ip}."\n");
407                 if (defined $tmp) {
408                         $answer = $tmp;
409                 }
410                 daemon_log("Got answer from module ".$module.": ".$answer,8);
411         }        
412         daemon_log("processing of msg finished", 5);
414         if (defined $answer) {
415                 $heap->{client}->put($answer);
416         } else {
417                 $heap->{client}->put("done\n");
418         }
421 sub trigger_db_loop {
422         my ($kernel) = $_[KERNEL];
423         $kernel->delay_set('watch_for_new_jobs',3);
426 sub watch_for_new_jobs {
427         my ($kernel,$heap) = @_[KERNEL, HEAP];
429         # check gosa job queue for jobs with executable timestamp
430 #       my ($seconds, $minutes, $hours, $monthday, $month,
431 #               $year, $weekday, $yearday, $sommertime) = localtime(time);
432 #       $hours = $hours < 10 ? $hours = "0".$hours : $hours;
433 #       $minutes = $minutes < 10 ? $minutes = "0".$minutes : $minutes;
434 #       $seconds = $seconds < 10 ? $seconds = "0".$seconds : $seconds;
435 #       $month+=1;
436 #       $month = $month < 10 ? $month = "0".$month : $month;
437 #       $monthday = $monthday < 10 ? $monthday = "0".$monthday : $monthday;
438 #       $year+=1900;
439 #       my $timestamp = "$year$month$monthday$hours$minutes$seconds";
440     my $timestamp = &get_time();
442         my $res = $job_db->select_dbentry( { table=>$job_queue_table_name, status=>'waiting', timestamp=>$timestamp  } );
444         while( my ($id, $hit) = each %{$res} ) {         
446                 my $jobdb_id = $hit->{id};
447                 my $macaddress = $hit->{macaddress};
448                 my $job_msg_hash = &transform_msg2hash($hit->{xmlmessage});
449                 my $out_msg_hash = $job_msg_hash;
450                 my $res_hash = $known_clients_db->select_dbentry( {table=>'known_clients', macaddress=>$macaddress} );
451                 # expect macaddress is unique!!!!!!
452                 my $target = $res_hash->{1}->{hostname};
454                 if (not defined $target) {
455                         &daemon_log("ERROR: no host found for mac address: $job_msg_hash->{mac}[0]", 1);
456                         &daemon_log("xml message: $hit->{xmlmessage}", 5);
457                         my $update_hash = { table=>$job_queue_table_name,
458                                 update=> [ { status=>['error'], result=>["no host found for mac address"] } ],
459                                 where=> [ { id=>[$jobdb_id] } ],
460                         };
461                         my $res = $job_db->update_dbentry($update_hash);
463                         next;
464                 }
466                 # add target
467                 &add_content2xml_hash($out_msg_hash, "target", $target);
469                 # add new header
470                 my $out_header = $job_msg_hash->{header}[0];
471                 $out_header =~ s/job_/gosa_/;
472                 delete $out_msg_hash->{header};
473                 &add_content2xml_hash($out_msg_hash, "header", $out_header);
475                 # add sqlite_id 
476                 &add_content2xml_hash($out_msg_hash, "jobdb_id", $jobdb_id); 
478                 my $out_msg = &create_xml_string($out_msg_hash);
480                 # encrypt msg as a GosaPackage module
481                 my $cipher = &create_ciphering($gosa_passwd);
482                 my $crypted_out_msg = &encrypt_msg($out_msg, $cipher);
484                 my $error = &send_msg_hash2address($out_msg_hash, "$gosa_ip:$gosa_port", $gosa_passwd);
486                 if ($error == 0) {
487                         my $sql = "UPDATE '$job_queue_table_name' SET status='processing', targettag='$target' WHERE id='$jobdb_id'";
488                         my $res = $job_db->exec_statement($sql);
489                 } else {
490                         my $update_hash = { table=>$job_queue_table_name, 
491                                 update=> [ { status=>'error' } ],
492                                 where=> [ { id=>$jobdb_id } ],
493                         };
494                         my $res = $job_db->update_dbentry($update_hash);
495                 }
496         }
498         $kernel->delay_set('watch_for_new_jobs',3);
502 #==== MAIN = main ==============================================================
503 #  parse commandline options
504 Getopt::Long::Configure( "bundling" );
505 GetOptions("h|help" => \&usage,
506         "c|config=s" => \$cfg_file,
507         "f|foreground" => \$foreground,
508         "v|verbose+" => \$verbose,
509         "no-bus+" => \$no_bus,
510         "no-arp+" => \$no_arp,
511            );
513 #  read and set config parameters
514 &check_cmdline_param ;
515 &read_configfile;
516 &check_pid;
518 $SIG{CHLD} = 'IGNORE';
520 # forward error messages to logfile
521 if( ! $foreground ) {
522     open(STDERR, '>>', $log_file);
523     open(STDOUT, '>>', $log_file);
526 # Just fork, if we are not in foreground mode
527 if( ! $foreground ) { 
528     chdir '/'                 or die "Can't chdir to /: $!";
529     $pid = fork;
530     setsid                    or die "Can't start a new session: $!";
531     umask 0;
532 } else { 
533     $pid = $$; 
536 # Do something useful - put our PID into the pid_file
537 if( 0 != $pid ) {
538     open( LOCK_FILE, ">$pid_file" );
539     print LOCK_FILE "$pid\n";
540     close( LOCK_FILE );
541     if( !$foreground ) { 
542         exit( 0 ) 
543     };
546 daemon_log(" ", 1);
547 daemon_log("$0 started!", 1);
549 # delete old DBsqlite lock files
550 system('rm -f /tmp/gosa_si_lock*');
552 # connect to gosa-si job queue
553 my @job_col_names = ("id", "timestamp", "status", "result", "headertag", "targettag", "xmlmessage", "macaddress");
554 $job_db = GOSA::DBsqlite->new($job_queue_file_name);
555 $job_db->create_table('jobs', \@job_col_names);
557 # connect to known_clients_db
558 my @clients_col_names = ('hostname', 'status', 'hostkey', 'timestamp', 'macaddress', 'events');
559 $known_clients_db = GOSA::DBsqlite->new($known_clients_file_name);
560 $known_clients_db->create_table('known_clients', \@clients_col_names);
562 # connect to known_server_db
563 my @server_col_names = ('hostname', 'status', 'hostkey', 'timestamp');
564 $known_server_db = GOSA::DBsqlite->new($known_server_file_name);
565 $known_server_db->create_table('known_server', \@server_col_names);
567 # import all modules
568 &import_modules;
570 # check wether all modules are gosa-si valid passwd check
572 # create session for repeatedly checking the job queue for jobs
573 POE::Session->create
575         inline_states => {
576                 _start => \&trigger_db_loop,
577                 watch_for_new_jobs => \&watch_for_new_jobs,
578         }
579 );
581 # create socket for incoming xml messages
582 POE::Component::Server::TCP->new
584         Port => $server_port,
585         ClientInput => \&client_input,
586 );
587 daemon_log("start socket for incoming xml messages at port '$server_port' ", 1);
589 POE::Kernel->run();
590 exit;