1 #!/usr/bin/perl
2 #===============================================================================
3 #
4 # FILE: gosa-support-daemon.pl
5 #
6 # USAGE: ./.gosa-support-daemon.pl
7 #
8 # DESCRIPTION:
9 #
10 # OPTIONS: ---
11 # REQUIREMENTS: ---
12 # BUGS: ---
13 # NOTES: ---
14 # AUTHOR: Andreas Rettenberger, <rettenberger@gonicus.de>
15 # COMPANY: Gonicus GmbH, Arnsberg
16 # VERSION: 1.0
17 # CREATED: 21.08.2007 15:13:51 CEST
18 # REVISION: ---
19 #===============================================================================
21 use strict;
22 use warnings;
23 use Getopt::Long;
24 use Config::IniFiles;
25 use POSIX;
26 use Fcntl;
27 use Net::LDAP;
28 use Net::LDAP::LDIF;
29 use Net::LDAP::Entry;
30 use Switch;
33 my ($verbose, $cfg_file, $log_file, $pid_file, $foreground);
34 my ($timeout, $mailto, $mailfrom, $user, $group);
35 my ($procid, $pid, $loglevel);
36 my ($fifo_path, $max_process_timeout, $max_process );
37 my %daemon_children;
38 my ($ldap, $bind_phrase, $password, $ldap_base) ;
40 $procid = -1 ;
41 $foreground = 0 ;
42 $verbose = 0 ;
43 $max_process = 2 ;
44 $max_process_timeout = 1 ;
45 $ldap_base = "dc=gonicus,dc=de" ;
46 #$ldap_path = "/var/run/gosa-support-daemon.socket";
47 #$log_path = "/var/log/gosa-support-daemon.log";
48 #$pid_path = "/var/run/gosa-support-daemon/gosa-support-daemon.pid";
50 #---------------------------------------------------------------------------
51 # parse commandline options
52 #---------------------------------------------------------------------------
53 Getopt::Long::Configure( "bundling" );
54 GetOptions( "v|verbose+" => \$verbose,
55 "c|config=s" => \$cfg_file,
56 "h|help" => \&usage,
57 "l|logfile=s" => \$log_file,
58 "p|pid=s" => \$pid_file,
59 "f|foreground" => \$foreground);
61 #---------------------------------------------------------------------------
62 # read and set config parameters
63 #---------------------------------------------------------------------------
64 my %cfg_defaults =
65 ("Allgemein" =>
66 {"timeout" => [ \$timeout, 1000 ],
67 "mailto" => [ \$mailto, 'root@localhost' ],
68 "mailfrom" => [ \$mailfrom, 'sps-daemon@localhost' ],
69 "user" => [ \$user, "nobody" ],
70 "group" => [ \$group, "nogroup" ],
71 "fifo_path" => [ \$fifo_path, "/home/rettenbe/gonicus/gosa-support/tmp/fifo" ],
72 "log_file" => [ \$log_file, "/home/rettenbe/gonicus/gosa-support/tmp/gosa-support.log" ],
73 "pid_file" => [ \$pid_file, "/home/rettenbe/gonicus/gosa-support/tmp/gosa-support.pid" ],
74 "loglevel" => [ \$loglevel, 1]
75 },
76 "LDAP" =>
77 {"bind" => [ \$bind_phrase, "cn=ldapadmin,dc=gonicus,dc=de" ],
78 "password" => [ \$password, "tester" ],
79 }
80 );
81 &read_configfile;
84 #=== FUNCTION ================================================================
85 # NAME: check_cmdline_param
86 # PURPOSE: checks all commandline parameters to validity
87 # PARAMETERS: none
88 # RETURNS: none
89 # DESCRIPTION: ????
90 # THROWS: no exceptions
91 # COMMENTS: none
92 # SEE ALSO: n/a
93 #===============================================================================
94 sub check_cmdline_param () {
95 my $err_config;
96 my $err_log;
97 my $err_pid;
98 my $err_counter = 0;
99 if( not defined( $cfg_file)) {
100 $err_config = "please specify a config file";
101 $err_counter += 1;
102 }
103 if( not defined( $log_file)) {
104 $err_log = "please specify a log file";
105 $err_counter += 1;
106 }
107 if( not defined( $pid_file)) {
108 $err_pid = "please specify a pid file";
109 $err_counter += 1;
110 }
111 if( $err_counter > 0 ) {
112 &usage( "", 1 );
113 if( defined( $err_config)) { print STDERR "$err_config\n"}
114 if( defined( $err_log)) { print STDERR "$err_log\n" }
115 if( defined( $err_pid)) { print STDERR "$err_pid\n"}
116 print STDERR "\n";
117 exit( -1 );
118 }
119 }
121 #=== FUNCTION ================================================================
122 # NAME: check_pid
123 # PURPOSE:
124 # PARAMETERS: none
125 # RETURNS: none
126 # DESCRIPTION: ????
127 # THROWS: no exceptions
128 # COMMENTS: none
129 # SEE ALSO: n/a
130 #===============================================================================
131 sub check_pid {
132 if( open( LOCK_FILE, "<$pid_file") ) {
133 $procid = <LOCK_FILE>;
134 if( defined $procid ) {
135 chomp( $procid );
136 if( -f "/proc/$procid/stat" ) {
137 my($stat) = `cat /proc/$procid/stat` =~ m/$procid \((.+)\).*/;
138 print "\t".$stat."\n";
139 if( "sps-daemon.pl" eq $stat ) {
140 close( LOCK_FILE );
141 exit -1;
142 }
143 }
144 }
145 close( LOCK_FILE );
146 unlink( $pid_file );
147 }
149 # Try to open PID file
150 if (!sysopen(LOCK_FILE, $pid_file, O_WRONLY|O_CREAT|O_EXCL, 0644)) {
151 my($msg) = "Couldn't obtain lockfile '$pid_file' ";
152 if (open(LOCK_FILE, "<", $pid_file) && ($pid = <LOCK_FILE>)) {
153 chomp($pid);
154 $msg .= "(PID $pid)\n";
155 } else {
156 $msg .= "(unable to read PID)\n";
157 }
158 if ( ! $foreground ) {
159 daemon_log( $msg."\n");
160 } else {
161 print( STDERR " $msg " );
162 }
163 exit( -1 );
164 }
165 }
167 #=== FUNCTION ================================================================
168 # NAME: read_configfile
169 # PURPOSE: read the configuration file and provide the programm with
170 # parameters
171 # PARAMETERS: none
172 # RETURNS: none
173 # DESCRIPTION: ????
174 # THROWS: no exceptions
175 # COMMENTS: none
176 # SEE ALSO: n/a
177 #===============================================================================
178 sub read_configfile {
179 my $log_time = localtime(time);
180 my $cfg;
181 if( defined( $cfg_file) && ( length($cfg_file) > 0 )) {
182 if( -r $cfg_file ) {
183 $cfg = Config::IniFiles->new( -file => $cfg_file );
184 } else {
185 usage( "Couldn't read config file: $cfg_file \n" );
186 }
187 } else {
188 $cfg = Config::IniFiles->new() ;
189 }
191 foreach my $section (keys %cfg_defaults) { # "Parse" config into values
192 foreach my $param (keys %{$cfg_defaults{ $section }}) {
193 my $pinfo = $cfg_defaults{ $section }{ $param };
194 ${@$pinfo[ 0 ]} = $cfg->val( $section, $param, @$pinfo[ 1 ] );
195 }
196 }
198 if(-e $log_file ) { unlink $log_file }
199 daemon_log("$log_time: config file read\n");
200 }
202 #=== FUNCTION ================================================================
203 # NAME: daemon_log
204 # PURPOSE: log messages to specified logfile
205 # PARAMETERS: $msg, $level
206 # RETURNS: ????
207 # DESCRIPTION: Takes a message ($msg) and append it to the logfile. The
208 # standard log-level ($level) is 1. Messages whith higher level
209 # than the verbosity-level (defined by commandline) are printed
210 # out to commandline. Messages with log-level lower than 2 are
211 # not logged to logfile!
212 # THROWS: no exceptions
213 # COMMENTS: none
214 # SEE ALSO: n/a
215 #===============================================================================
216 sub daemon_log {
217 my( $msg, $level ) = @_;
218 if(not defined $msg) { return }
219 if(not defined $level) { $level = 1 }
220 open(LOG_HANDLE, ">>$log_file");
221 if(not defined open( LOG_HANDLE, ">>$log_file" ) ) { return }
222 chomp($msg);
223 #if( $verbose >= $level ) { print "$msg"."\n" }
224 if( $level <= 1 ) { print LOG_HANDLE $msg."\n" }
225 if( $foreground ) { print $msg."\n" }
226 close( LOG_HANDLE );
227 }
229 #=== FUNCTION ================================================================
230 # NAME: signal handler
231 # PURPOSE: catches signals from the programm and do diffrent things
232 # than default
233 # PARAMETERS: none
234 # RETURNS: none
235 # DESCRIPTION: sighandler
236 # THROWS: no exceptions
237 # COMMENTS: none
238 # SEE ALSO: n/a
239 #===============================================================================
240 sub sigINT {
241 my $log_time = localtime(time);
242 print "INT\n";
243 if( -p $fifo_path ) {
244 close FIFO ;
245 unlink($fifo_path) ;
246 daemon_log( "$log_time: FIFO closed after signal INT!\n") ;
247 }
248 if(defined($ldap)) {
249 $ldap->unbind;
250 }
251 $SIG{INT} = "DEFAULT" ;
252 kill INT => $$ ;
253 }
254 $SIG{INT} = \&sigINT ;
256 #=== FUNCTION ================================================================
257 # NAME: usage
258 # PURPOSE:
259 # PARAMETERS: none
260 # RETURNS: none
261 # DESCRIPTION: print out the usage of the program
262 # THROWS: no exceptions
263 # COMMENTS: none
264 # SEE ALSO: n/a
265 #===============================================================================
266 sub usage {
267 my( $text, $help ) = @_;
268 $text = undef if( "h" eq $text );
269 (defined $text) && print STDERR "\n$text\n";
270 if( (defined $help && $help) || (!defined $help && !defined $text) ) {
271 print STDERR << "EOF" ;
272 usage: $0 [-hvf] [-c config, -l logfile, -p pidfile]
274 -h : this (help) message
275 -c <file> : config file
276 -l <file> : log file (example: /var/log/sps/sps.log)
277 -p <file> : pid file (example: /var/run/sps/sps.pid)
278 -f : foreground (don"t fork)
279 -v : be verbose (multiple to increase verbosity)
280 EOF
281 }
282 print "\n" ;
283 }
286 #=== FUNCTION ================================================================
287 # NAME: open_fifo
288 # PURPOSE:
289 # PARAMETERS: $fifo_path
290 # RETURNS: 0: FIFO couldn"t be setup, 1: FIFO setup correctly
291 # DESCRIPTION: creates a FIFO at $fifo_path
292 # THROWS: no exceptions
293 # COMMENTS: none
294 # SEE ALSO: n/a
295 #===============================================================================
296 sub open_fifo {
297 my ($fifo_path) = @_ ;
298 my $log_time = localtime( time );
299 if( -p $fifo_path ) {
300 daemon_log("$log_time: FIFO at $fifo_path already exists\n");
301 return 0;
302 }
303 POSIX::mkfifo($fifo_path, 0666) or die "can't mkfifo $fifo_path: $!";
304 daemon_log( "$log_time: FIFO started at $fifo_path\n" ) ;
305 return 1;
306 }
309 #=== FUNCTION ================================================================
310 # NAME: add_ldap_entry
311 # PURPOSE: adds an element to ldap-tree
312 # PARAMETERS:
313 # RETURNS: none
314 # DESCRIPTION: ????
315 # THROWS: no exceptions
316 # COMMENTS: none
317 # SEE ALSO: n/a
318 #===============================================================================
319 sub add_ldap_entry {
320 my ($ldap_tree, $ldap_base, $mac, $gotoSysStatus, $ip, $interface, $desc) = @_;
321 my $dn = "cn=$mac,ou=incoming,$ldap_base";
322 my $s_res = &search_ldap_entry($ldap_tree, $ldap_base, "(|(macAddress=$mac)(dhcpHWAddress=ethernet $mac))");
323 my $c_res = $s_res->count;
324 if($c_res == 1) {
325 daemon_log("WARNING: macAddress $mac already in LDAP", 1);
326 return;
327 } elsif($c_res > 0) {
328 daemon_log("ERROR: macAddress $mac exists $c_res times in LDAP", 1);
329 return;
330 }
332 # create LDAP entry
333 my $entry = Net::LDAP::Entry->new( $dn );
334 $entry->dn($dn);
335 $entry->add("objectClass" => "goHard");
336 $entry->add("cn" => $mac);
337 $entry->add("macAddress" => $mac);
338 if(defined $gotoSysStatus) {$entry->add("gotoSysStatus" => $gotoSysStatus)}
339 if(defined $ip) {$entry->add("ipHostNumber" => $ip) }
340 #if(defined $interface) { }
341 if(defined $desc) {$entry->add("description" => $desc) }
343 # submit entry to LDAP
344 my $result = $entry->update ($ldap_tree);
346 # for $result->code constants please look at Net::LDAP::Constant
347 my $log_time = localtime( time );
348 if($result->code == 68) { # entry already exists
349 daemon_log("WARNING: $log_time: $dn ".$result->error, 3);
350 } elsif($result->code == 0) { # everything went fine
351 daemon_log("$log_time: add entry $dn to ldap", 1);
352 } else { # if any other error occur
353 daemon_log("ERROR: $log_time: $dn, ".$result->code.", ".$result->error, 1);
354 }
355 return;
356 }
359 #=== FUNCTION ================================================================
360 # NAME: change_ldap_entry
361 # PURPOSE: ????
362 # PARAMETERS: ????
363 # RETURNS: ????
364 # DESCRIPTION: ????
365 # THROWS: no exceptions
366 # COMMENTS: none
367 # SEE ALSO: n/a
368 #===============================================================================
369 sub change_ldap_entry {
370 my ($ldap_tree, $ldap_base, $mac, $gotoSysStatus ) = @_;
372 # check if ldap_entry exists or not
373 my $s_res = &search_ldap_entry($ldap_tree, $ldap_base, "(|(macAddress=$mac)(dhcpHWAddress=ethernet $mac))");
374 my $c_res = $s_res->count;
375 if($c_res == 0) {
376 daemon_log("WARNING: macAddress $mac not in LDAP", 1);
377 return;
378 } elsif($c_res > 1) {
379 daemon_log("ERROR: macAddress $mac exists $c_res times in LDAP", 1);
380 return;
381 }
383 my $s_res_entry = $s_res->pop_entry();
384 my $dn = $s_res_entry->dn();
385 my $result = $ldap->modify( $dn, replace => {'gotoSysStatus' => $gotoSysStatus } );
387 # for $result->code constants please look at Net::LDAP::Constant
388 my $log_time = localtime( time );
389 if($result->code == 32) { # entry doesnt exists
390 &add_ldap_entry($mac, $gotoSysStatus);
391 } elsif($result->code == 0) { # everything went fine
392 daemon_log("$log_time: entry $dn changed successful", 1);
393 } else { # if any other error occur
394 daemon_log("ERROR: $log_time: $dn, ".$result->code.", ".$result->error, 1);
395 }
397 return;
398 }
400 #=== FUNCTION ================================================================
401 # NAME: search_ldap_entry
402 # PURPOSE: ????
403 # PARAMETERS: [Net::LDAP] $ldap_tree - object of an ldap-tree
404 # string $sub_tree - dn of the subtree the search is performed
405 # string $search_string - either a string or a Net::LDAP::Filter object
406 # RETURNS: [Net::LDAP::Search] $msg - result object of the performed search
407 # DESCRIPTION: ????
408 # THROWS: no exceptions
409 # COMMENTS: none
410 # SEE ALSO: n/a
411 #===============================================================================
412 sub search_ldap_entry {
413 my ($ldap_tree, $sub_tree, $search_string) = @_;
414 my $msg = $ldap_tree->search( # perform a search
415 base => $sub_tree,
416 filter => $search_string,
417 ) or daemon_log("cannot perform search at ldap: $@", 1);
418 # if(defined $msg) {
419 # print $sub_tree."\t".$search_string."\t";
420 # print $msg->count."\n";
421 # foreach my $entry ($msg->entries) { $entry->dump; };
422 # }
424 return $msg;
425 }
429 #========= MAIN = main ========================================================
430 daemon_log( "####### START DAEMON ######\n", 1 );
431 &check_cmdline_param ;
432 &check_pid;
433 &open_fifo($fifo_path);
435 # Just fork, if we"re not in foreground mode
436 if( ! $foreground ) { $pid = fork(); }
437 else { $pid = $$; }
439 # Do something useful - put our PID into the pid_file
440 if( 0 != $pid ) {
441 open( LOCK_FILE, ">$pid_file" );
442 print LOCK_FILE "$pid\n";
443 close( LOCK_FILE );
444 if( !$foreground ) { exit( 0 ) };
445 }
448 if( not -p $fifo_path ) { die "fifo file disappeared\n" }
449 sysopen(FIFO, $fifo_path, O_RDONLY) or die "can't read from $fifo_path: $!" ;
451 while( 1 ) {
452 # checke alle prozesse im hash daemon_children ob sie noch aktiv sind, wenn
453 # nicht, dann entferne prozess aus hash
454 while( (my $key, my $val) = each( %daemon_children) ) {
455 my $status = waitpid( $key, &WNOHANG) ;
456 if( $status == -1 ) {
457 delete $daemon_children{$key} ;
458 daemon_log("childprocess finished: $key", 3) ;
459 }
460 }
462 # ist die max_process anzahl von prozesskindern erreicht, dann warte und
463 # prüfe erneut, ob in der zwischenzeit prozesse fertig geworden sind
464 if( keys( %daemon_children ) >= $max_process ) {
465 sleep($max_process_timeout) ;
466 next ;
467 }
469 my $msg = <FIFO>;
470 if( not defined( $msg )) { next ; }
472 chomp( $msg );
473 if( length( $msg ) == 0 ) { next ; }
475 my $forked_pid = fork();
476 #=== PARENT = parent ==========================================================
477 if ( $forked_pid != 0 ) {
478 daemon_log("childprocess forked: $forked_pid", 3) ;
479 $daemon_children{$forked_pid} = 0 ;
480 }
481 #=== CHILD = child ============================================================
482 else {
483 # parse the incoming message from arp, split the message and return
484 # the values in an array. not defined values are set to "none"
485 #my ($mac, $ip, $interface, $arp_sig, $desc) = &parse_input( $msg ) ;
486 daemon_log( "childprocess read from arp: $fifo_path\nline: $msg", 3);
487 my ($mac, $ip, $interface, $arp_sig, $desc) = split('\s', $msg, 5);
489 # create connection to LDAP
490 $ldap = Net::LDAP->new( "localhost" ) or die "$@";
491 $ldap->bind($bind_phrase,
492 password => $password,
493 ) ;
495 switch($arp_sig) {
496 case 0 {&change_ldap_entry($ldap, $ldap_base,
497 $mac, "ip-changed",
498 )}
499 case 1 {&change_ldap_entry($ldap, $ldap_base,
500 $mac, "mac-not-whitelisted",
501 )}
502 case 2 {&change_ldap_entry($ldap, $ldap_base,
503 $mac, "mac-in-blacklist",
504 )}
505 case 3 {&add_ldap_entry($ldap, $ldap_base,
506 $mac, "new-mac-address", $ip,
507 $interface, $desc,
508 )}
509 case 4 {&change_ldap_entry($ldap, $ldap_base,
510 $mac, "unauthorized-arp-request",
511 )}
512 case 5 {&change_ldap_entry($ldap, $ldap_base,
513 $mac, "abusive-number-of-arp-requests",
514 )}
515 case 6 {&change_ldap_entry($ldap, $ldap_base,
516 $mac, "ether-and-arp-mac-differs",
517 )}
518 case 7 {&change_ldap_entry($ldap, $ldap_base,
519 $mac, "flood-detected",
520 )}
521 case 8 {&add_ldap_entry($ldap, $ldap_base,
522 $mac, $ip, "new-system",
523 )}
524 case 9 {&change_ldap_entry($ldap, $ldap_base,
525 $mac, "mac-changed",
526 )}
527 }
530 # ldap search
531 # my $base_phrase = "dc=gonicus,dc=de";
532 # my $filter_phrase = "cn=keinesorge";
533 # my $attrs_phrase = "cn macAdress";
534 # my $msg_search = $ldap->search( base => $base_phrase,
535 # filter => $filter_phrase,
536 # attrs => $attrs_phrase,
537 # );
538 # $msg_search->code && die $msg_search->error;
539 #
540 # my @entries = $msg_search->entries;
541 # my $max = $msg_search->count;
542 # print "anzahl der entries: $max\n";
543 # my $i;
544 # for ( $i = 0 ; $i < $max ; $i++ ) {
545 # my $entry = $msg_search->entry ( $i );
546 # foreach my $attr ( $entry->attributes ) {
547 # if( not $attr eq "cn") {
548 # next;
549 # }
550 # print join( "\n ", $attr, $entry->get_value( $attr ) ), "\n\n";
551 # }
552 # }
554 # ldap add
557 $ldap->unbind;
558 exit;
559 }
561 }