1#!/usr/local/bin/perl 2# 3# tenshi 0.17 2017/10/19 4# Copyright 2004-2017 Andrea Barisani <andrea@inversepath.com> 5# 6# Permission to use, copy, modify, and distribute this software for any 7# purpose with or without fee is hereby granted, provided that the above 8# copyright notice and this permission notice appear in all copies. 9# 10# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 13# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 15# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 16# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 18use strict; 19use warnings; 20use Net::SMTP; 21use File::Temp; 22use Sys::Hostname; 23use IO::Socket::INET; 24use filetest 'access'; 25use IO::BufferedSelect; 26use Term::ANSIColor qw(:constants); 27use Getopt::Long qw(:config no_ignore_case); 28use POSIX qw(locale_h setsid setuid setgid strftime floor); 29 30setlocale(LC_TIME, "C"); 31File::Temp->safe_level(File::Temp::HIGH); 32$Term::ANSIColor::AUTORESET = 1; 33 34my $version = '0.17'; 35 36my %opts; 37GetOptions('configuration=s' => \$opts{'c'}, 'Check' => \$opts{'C'}, 38 'debug:i' => \$opts{'d'}, 'foreground' => \$opts{'f'}, 39 'profile' => \$opts{'p'}, 'Pid=s' => \$opts{'P'}, 40 'help' => \$opts{'h'}); 41if ($opts{'h'}) { usage(); } 42 43my $our_hostname = hostname(); 44my @startup_time = localtime(); 45 46my ($uid, $gid); 47 48my $last_check = 0; 49my $last_minute = 0; 50my $sleep = 5; 51my $mailtimeout = 10; 52my $select_timeout = 1; 53 54my $config_reinit = 1; 55 56my ($mailserver, $mailhelo, $limit, $pager_limit, $hidepid, $status, $listen, $resolve); 57my (%main, %last_match, %last_queue, %filter_file, %filter_args, %hostnames, %regexp_matches); 58my ($config_read, $queue_flush_needed, $queue_check_needed, $time_to_die); 59my (@log_files, @fifo_files, @log_prefix, @regexp, @queues, @skip, @group_stack, @queues_escalation); 60my (@redis_queues, $redisserver, $redis); 61my ($syslog_sender, $syslog_listen_socket); 62 63my $profile = $opts{'p'} || 0; 64my $foreground = $opts{'f'} || 0; 65my $config_file = $opts{'c'} || '/usr/local/etc/tenshi/tenshi.conf'; 66my $pid_file = $opts{'P'} || '/var/run/tenshi.pid'; 67 68my ($debug, $debug_smtp); 69 70$debug = (defined($opts{'d'}) && $opts{'d'} == 0) ? 1 : $opts{'d'} || 0; 71$debug_smtp = ($debug > 1) ? 1 : 0; 72 73my $tail_file = '/usr/bin/tail'; 74my $tail_args = '-q -F -n 0'; 75my $tail_multiple = 'off'; 76my @tail_pids; 77 78my @fhs; 79 80my %days = ( 'mon' => 1, 'tue' => 2, 'wed' => 3, 81 'thu' => 4, 'fri' => 5, 'sat' => 6, 82 'sun' => 0 ); 83 84my %months = ( 'jan' => 1, 'feb' => 2, 'mar' => 3, 85 'apr' => 4, 'may' => 5, 'jun' => 6, 86 'jul' => 7, 'aug' => 8, 'sep' => 9, 87 'oct' => 10, 'nov' => 11, 'dec' => 12 ); 88 89my @cron_specs = ( 90 { 'min' => 0, 'max' => 59, 'shift' => 0, 'wrap' => 0, 'localtime_field' => 1 }, 91 { 'min' => 0, 'max' => 23, 'shift' => 0, 'wrap' => 0, 'localtime_field' => 2 }, 92 { 'min' => 1, 'max' => 31, 'shift' => 0, 'wrap' => 0, 'localtime_field' => 3 }, 93 { 'min' => 1, 'max' => 12, 'shift' => -1, 'wrap' => 0, 'localtime_field' => 4, 'strings' => \%months }, 94 { 'min' => 0, 'max' => 7, 'shift' => 0, 'wrap' => 1, 'localtime_field' => 6, 'strings' => \%days }, 95); 96 97my $mask = '______'; 98my $mask_length = length $mask; 99my $subject = 'tenshi report'; 100my $sort_order = 'descending'; 101 102# prototype for clean_up so we don't need parens 103sub clean_up; 104 105config_read($config_file); 106$config_read = 1; 107 108if ($opts{'C'}) { exit 0; } 109 110if (not defined($uid)) { $uid = getpwnam('tenshi') or clean_up and die RED "[ERROR] no such user: tenshi\n"; } 111if (not defined($gid)) { $gid = getgrnam('tenshi') or clean_up and die RED "[ERROR] no such group: tenshi\n"; } 112 113if ($listen) { 114 $syslog_listen_socket = IO::Socket::INET->new( 115 LocalAddr => $listen, 116 Proto => 'udp' 117 ) or clean_up and die "[ERROR] can't bind UDP socket: $!\n"; 118 push @fhs, $syslog_listen_socket; 119} 120 121$SIG{'CHLD'} = sub { $debug && debug(5,'CHLD') ; print RED "[ERROR] child died, bailing out\n"; $time_to_die = 1; }; 122 123prepare_process(); 124 125# 126# sanity checks 127# 128 129if (!$profile) { 130 foreach my $queue (@queues) { 131 if ($filter_file{$queue} and ! -x $filter_file{$queue}) { 132 clean_up and die RED "[ERROR] $filter_file{$queue}: not executable\n"; 133 } 134 } 135 136 if ($main{'csv'} and ! -x $main{'csv'}{'path'}) { 137 clean_up and die RED "[ERROR] $main{'csv'}{'path'}: not executable\n"; 138 } 139 140 if (scalar(@redis_queues) > 0) { 141 $debug && debug(23, 'testing Redis module availability'); 142 require Redis; 143 } 144 145 my @readable_log_files; 146 147 foreach my $log (@log_files) { 148 unless (-f $log) { 149 print STDERR RED "[WARNING] $log: no such file\n"; 150 next; 151 } 152 153 unless (-r $log) { 154 print STDERR RED "[WARNING] $log: file not readable\n"; 155 next; 156 } 157 push @readable_log_files, $log; 158 } 159 160 @readable_log_files > 0 || @fifo_files > 0 || @redis_queues > 0 || $listen 161 or clean_up and die RED "[ERROR] no readable log files\n"; 162 163 @log_files = @readable_log_files; 164} 165 166# 167# log file parsing 168# 169 170if ($profile) { 171 open(my $fh, "-") or 172 clean_up and die RED "[ERROR] could not open standard input: $!\n"; 173 push @fhs, $fh; 174} else { 175 if (scalar(@log_files) > 0) { 176 clean_up and die RED "[ERROR] $tail_file: $!\n" if (! -f $tail_file); 177 my @remaining = @log_files; 178 179 do { 180 my $log; 181 182 if ($tail_multiple eq 'on') { 183 $log = shift @remaining; 184 } else { 185 $log = join(' ', @remaining); 186 @remaining = (); 187 } 188 189 $debug && debug(20, "$tail_file $tail_args $log"); 190 191 pipe(my $r, my $w) or clean_up and die RED "[ERROR] could not open pipe for tail: $!\n"; 192 my $pid = fork(); 193 defined($pid) or clean_up and die RED "[ERROR] failed first fork for tail: $!\n"; 194 195 if ($pid) { 196 close $w; 197 push @fhs, $r; 198 push @tail_pids, $pid; 199 } else { 200 # this is child, no clean_up 201 setuid($<) or die RED "[ERROR] can't setuid to $<: $!\n"; 202 setuid($uid) or die RED "[ERROR] can't setuid to $uid: $!\n"; 203 open(STDOUT, ">&", $w) or die RED "[ERROR] can't re-open pipe as STDOUT: $!\n"; 204 close $r; 205 open(STDERR, ">/dev/null") or die RED "[ERROR] can't open STDERR as /dev/null: $!\n"; 206 exec("$tail_file $tail_args $log") or die RED "[ERROR] failed to exec tail command: $!\n"; 207 } 208 } while (@remaining); 209 } 210 211 if (scalar(@fifo_files) > 0) { 212 foreach my $fifo_file (@fifo_files) { 213 $debug && debug(19, "$fifo_file"); 214 open(my $fh, "+<$fifo_file") or 215 clean_up and die RED "[ERROR] could not open $fifo_file: $!\n"; 216 push @fhs, $fh; 217 } 218 } 219 220 if (scalar(@redis_queues) > 0) { 221 $debug && debug(23, 'opening Redis connection'); 222 223 if (defined $redisserver) { 224 $redis = Redis->new(server => $redisserver); 225 } else { 226 $redis = Redis->new; 227 } 228 229 $redis->ping || die RED "[ERROR] could not open connection to redis on $redisserver: $!\n"; 230 231 $debug && debug(23, "created redis socket $redis"); 232 } 233} 234 235unless (scalar(@fhs) > 0 or scalar(@redis_queues) > 0) { 236 clean_up and die RED "[ERROR] no log file has been specified\n"; 237} 238 239$debug && debug(3); 240 241$SIG{'TERM'} = sub { $debug && debug(5,'TERM') ; $status = 'terminating'; $queue_flush_needed = 1; $time_to_die = 1; }; 242$SIG{'INT'} = sub { $debug && debug(5, 'INT') ; $status = 'terminating'; $queue_flush_needed = 1; $time_to_die = 1; }; 243$SIG{'HUP'} = sub { $debug && debug(5, 'HUP') ; $status = 'reloading' ; $queue_flush_needed = 1; $config_reinit = 1; }; 244$SIG{'USR1'} = sub { $debug && debug(5,'USR1') ; $status = 'queue check'; $queue_check_needed = 1; }; 245$SIG{'USR2'} = sub { $debug && debug(5,'USR2') ; $status = 'flushing' ; $queue_flush_needed = 1; }; 246 247my $bs = IO::BufferedSelect->new(@fhs); 248 249if (!($debug || $profile || $foreground)) { 250 daemonize(); 251} else { # Need to drop privs even if not daemonizing 252 setuid($<) or clean_up and die RED "[ERROR] can't setuid to $<: $!\n"; 253 setuid($uid) or clean_up and die RED "[ERROR] can't setuid to $uid: $!\n"; 254} 255 256while (!$time_to_die) { 257 my $now = time; 258 my ($fh, $line); 259 260 if ($now > ($last_check + $sleep)) { 261 $queue_check_needed = 1; 262 } 263 264 if ($queue_flush_needed) { queues_flush(); $queue_flush_needed = 0; $queue_check_needed = 0; } 265 if ($queue_check_needed) { queues_check($now); $queue_check_needed = 0; } 266 if ($config_reinit) { config_read($config_file); $config_reinit = 0; } 267 if ($time_to_die) { last; } 268 269 my @ready = $bs->read_line($select_timeout); 270 271 foreach (@ready) { 272 ($fh, $line) = @$_; 273 274 if ($listen and $fh == $syslog_listen_socket) { 275 $line =~ s/^<\d+>//; 276 277 if (! ($line =~ /^[A-Z][a-z]{2}\s(?:\s|\d)\d\s\d{2}:\d{2}:\d{2}\s(\S+)\s/)) { 278 my ($port, $ipaddr) = sockaddr_in(getpeername($syslog_listen_socket)); 279 280 if (not defined $hostnames{$ipaddr}) { 281 $hostnames{$ipaddr} = gethostbyaddr($ipaddr, AF_INET); 282 } 283 284 my $time = strftime "%b %e %H:%M:%S", localtime; 285 $line = sprintf("%s %s %s", $time, $hostnames{$ipaddr}, $line); 286 } 287 } 288 289 if ($profile and not defined($line)) { print BLUE, "[PROFILE] reached end of file\n"; $time_to_die = 1; next; } 290 291 parse_line($line); 292 } 293 294 foreach my $redis_queue (@redis_queues) { 295 while($redis->llen($redis_queue) > 0) { 296 $line = $redis->lpop($redis_queue); 297 parse_line($line); 298 } 299 } 300 301 # throttle down loop on void reads 302 unless ($line) { sleep(1) }; 303} 304 305queues_flush(); 306 307clean_up(); 308 309exit 0; 310 311# 312# subs 313# 314 315sub config_read { 316 my $config_file = shift; 317 318 $debug && debug(0,$config_file); 319 320 if ($config_reinit) { 321 %main = (); 322 %hostnames = (); 323 @regexp = (); 324 %regexp_matches = (); 325 @queues = (); 326 @queues_escalation = (); 327 @skip = (); 328 @log_prefix = (); 329 $main{'group'} = {}; 330 $main{'trash'} = {}; 331 $main{'repeat'} = {}; 332 $main{'group_host'} = {}; 333 334 $hidepid = 0; 335 $resolve = 0; 336 337 push @log_prefix, qr/^[A-Z][a-z]{2}\s(?:\s|\d)\d\s\d{2}:\d{2}:\d{2}\s(\S+)\s/; 338 339 $config_reinit = 0; 340 } 341 342 # 343 # configuration file parsing 344 # 345 346 open(my $CONF,$config_file) or clean_up and die RED "[ERROR] could not open configuration file $config_file: $!\n"; 347 348 while (<$CONF>) { 349 s/^\s+//; 350 next if (/^#|^$/); 351 chomp; 352 353 if (/^include\s+(\S+)/) { $debug && debug(1,$_) ; config_read($1); next; } 354 355 if (/^includedir\s+(\S+)/) { 356 $debug && debug(1,$_); 357 opendir(my $DIR, $1) or clean_up and die RED "[ERROR] could not open directory $1: $!\n"; 358 foreach my $file (sort readdir($DIR)) { 359 next if ($file =~ /^\./); 360 next unless -f "$1/$file"; 361 config_read("$1/$file"); 362 } 363 next; 364 } 365 366 if (/^set\s+logfile\s+(\S+)/) { 367 if ($config_read and (!grep(/^$1$/, @log_files))) { 368 clean_up and die debug(100,'logfile'); 369 } elsif (!$config_read) { 370 $debug && debug(1,$_); 371 } 372 push @log_files, $1; 373 } 374 elsif (/^set\s+redisqueue\s+(\S+)/) { 375 if ($config_read and (!grep(/^$1$/, @redis_queues))) { 376 clean_up and die debug(100,'redisqueue'); 377 } elsif (!$config_read) { 378 $debug && debug(1, $_); 379 } 380 push @redis_queues, $1; 381 } 382 elsif (/^set\s+fifo\s+(\S+)/) { 383 if ($config_read and (!grep(/^$1$/, @fifo_files))) { 384 clean_up and die debug(100,'fifo'); 385 } elsif (!$config_read) { 386 $debug && debug(1, $_); 387 } 388 push @fifo_files, $1; 389 } 390 elsif (/^set\s+pidfile\s+(\S+)/) { 391 next if $opts{'P'}; 392 if ($config_read and ($1 ne $pid_file)) { 393 clean_up and die debug(100,'pidfile'); 394 } elsif (!$config_read and (!$opts{'P'})) { 395 $debug && debug(1,$_); 396 } 397 $pid_file = $1; 398 } 399 elsif (/^set\s+tail\s+(\S+)\s*(\S+.*)?/) { 400 if ($config_read and ($1 ne $tail_file)) { 401 clean_up and die debug(100,'tail'); 402 } elsif (!$config_read) { 403 $debug && debug(1,$_); 404 } 405 $tail_file = $1; 406 $tail_args = $2 || $tail_args; 407 } 408 elsif (/^set\s+tail_multiple\s+(off|on)/) { 409 if ($config_read and ($1 ne $tail_multiple)) { 410 clean_up and die debug(100,'tail_multiple'); 411 } elsif (!$config_read) { 412 $debug && debug(1,$_); 413 } 414 if ($1 eq 'on') { $tail_multiple = 'on'; } 415 else { $tail_multiple = 'off'; } 416 } 417 elsif (/^set\s+uid\s+(.+)/) { 418 if ($config_read and (getpwnam($1) ne $uid)) { 419 clean_up and die debug(100,'uid'); 420 } elsif (!$config_read) { 421 $debug && debug(1,$_); 422 } 423 $uid = getpwnam($1); 424 clean_up and die RED "[ERROR] no such user: $1\n" if not defined $uid; 425 if (not defined $uid) { clean_up and die RED "[ERROR] no such user: $1\n"; } 426 } 427 elsif (/^set\s+gid\s+(.+)/) { 428 if ($config_read and (getgrnam($1) ne $gid)) { 429 clean_up and die debug(100,'gid'); 430 } elsif (!$config_read) { 431 $debug && debug(1,$_); 432 } 433 $gid = getgrnam($1); 434 if (not defined $gid) { clean_up and die RED "[ERROR] no such group: $1\n"; } 435 } 436 elsif (/^set\s+listen\s+(\d+\.\d+\.\d+\.\d+:\d+)/) { 437 if ($config_read and ($1 ne $listen)) { 438 clean_up and die debug(100,'listen'); 439 } elsif (!$config_read) { 440 $debug && debug(1,$_); 441 } 442 $listen = $1; 443 } 444 elsif (/^set\s+sort_order\s+(\S+)/) { 445 if ($1 =~ /^(ascending|descending)$/) { 446 $sort_order = $1; 447 $debug && debug(1,$_); next; } 448 else { 449 clean_up and die RED "[ERROR] sort_order is invalid"; 450 } 451 } 452 elsif (/^set\s+limit\s+(\d+)/) { $limit = $1; $debug && debug(1,$_); next; } 453 elsif (/^set\s+subject\s+(.+)/) { $subject = $1; $debug && debug(1,$_); next; } 454 elsif (/^set\s+mailserver\s+(.+)/) { $mailserver = $1; $debug && debug(1,$_); next; } 455 elsif (/^set\s+mailhelo\s+(.+)/) { $mailhelo = $1; $debug && debug(1,$_); next; } 456 elsif (/^set\s+pager_limit\s+(\d+)/) { $pager_limit = $1; $debug && debug(1,$_); next; } 457 elsif (/^set\s+mailtimeout\s+(\d+)/) { $mailtimeout = $1; $debug && debug(1,$_); next; } 458 elsif (/^set\s+redisserver\s+(.+)/) { $redisserver = $1; $debug && debug(1,$_); next; } 459 elsif (/^set\s+filter\s+(\S+)\s+(\S+)\s*(\S+.*)?/) { 460 $filter_file{$1} = $2; 461 $filter_args{$1} = $3 || ""; 462 $debug && debug(1,$_); next; 463 } 464 elsif (/^set\s+sleep\s+(\d+)/) { 465 if ($sleep > 60) { clean_up and die RED "[ERROR] sleep time should be <= 60 seconds\n"; } else { 466 $sleep = $1; $debug && debug(1,$_); next; 467 } 468 } 469 elsif (/^set\s+logprefix\s+(.+)/) { 470 push @log_prefix, qr/$1/; 471 $debug && debug(1,$_); 472 } 473 elsif (/^set\s+mask(\s+(\S+))?/) { 474 $mask = ($1 ? $2: ''); 475 $mask_length = length $mask; 476 $debug && debug(1,$_); 477 } 478 elsif (/^set\s+hidepid\s+(off|on)/) { 479 if ($1 eq 'on') { $hidepid = 1; } 480 else { $hidepid = 0; } 481 $debug && debug(1,$_); 482 } 483 elsif (/^set\s+resolve\s+(off|on)/) { 484 if ($1 eq 'on') { $resolve = 1; } 485 else { $resolve = 0; } 486 $debug && debug(1,$_); 487 } 488 elsif 489 (/^set\s+queue\s+(\S+)\s+(\S+(?:\@\S+)?)\s+(pager:)?(\S+(?:\@\S+)?)\s+\[((?:\S+(?:\s+)?){5}|now)\]\s*(\S+.*)?/o) { 490 491 my ($queue, $mail_from, $pager, $mail_to, $cron_spec, $subject) = ($1, $2, $3, $4, $5, $6); 492 493 if (queue_is_builtin($queue)) { 494 clean_up and die RED "[ERROR] '$queue' is a built-in queue\n"; 495 } 496 497 if ($queue eq 'csv') { 498 clean_up and die RED "[ERROR] '$queue' is a special queue and must be defined with 'set csv' option\n"; 499 } 500 501 if ($cron_spec eq 'now') { 502 $main{$queue}{'now'} = 1; 503 } else { 504 $main{$queue}{'cron_mask'} = cron_spec_to_mask($cron_spec); 505 } 506 507 if ($pager) { $main{$queue}{'pager'} = 1; } 508 509 $main{$queue}{'mailfrom'} = $mail_from; $debug && debug(1,"queue: $queue - mail_from => $mail_from"); 510 $main{$queue}{'mailto'} = $mail_to; $debug && debug(1,"queue: $queue - mailto => $mail_to"); 511 512 if ($subject) { 513 $main{$queue}{'subject'} = $subject; $debug && debug(1,"queue: $queue - subject => $subject"); 514 } 515 516 } 517 elsif 518 (/^set\s+csv\s+\[((?:\S+(?:\s+)?){5}|now)\]\s+(\S+)\s*(.*)?/o) { 519 520 my $cron_spec = $1; 521 ($main{'csv'}{'path'}, $main{'csv'}{'args'}) = ($2, $3); 522 523 if ($cron_spec eq 'now') { 524 $main{'csv'}{'now'} = 1; 525 } else { 526 $main{'csv'}{'cron_mask'} = cron_spec_to_mask($cron_spec); 527 } 528 529 } 530 elsif (/^set\s+threshold\s+(\S+)\s+(\d+)\s+(.+$)/) { 531 my ($queue, $count, $re) = ($1, $2, $3); 532 533 unless (defined $main{$queue}) { 534 clean_up and die RED "[ERROR] invalid queue in threshold set directive: $_\n" 535 } 536 537 if ($count <= 0) { 538 clean_up and die RED "[ERROR] invalid count in threshold set directive: $_\n" 539 } 540 541 unless(defined $main{$queue}{'threshold'}) { 542 $main{$queue}{'threshold'} = [] 543 } 544 545 # store $re, $count pair in the array 546 push @{$main{$queue}{'threshold'}}, qr/$re/, $count; 547 548 } 549 elsif (/^set\s+/) { 550 clean_up and die RED "[ERROR] invalid set directive: $_\n"; 551 } 552 elsif (my ($queue, $reg) = $_ =~ /(^\S+)\s+(.+$)/) { 553 554 $debug && debug(1,"queue: $queue regexp: $reg"); 555 556 my @queue = split(/,/, $queue); 557 my %queue_escalation; 558 559 my $max_escalation = 0; 560 foreach my $q (@queue) { 561 my ($queue, $escalation) = split(/:/, $q); 562 563 if (!($main{$queue})) { 564 clean_up and die RED "[ERROR] invalid configuration directive: queue $queue not defined\n"; 565 } 566 567 if ((scalar(@queue) > 1) and queue_is_builtin($queue)) { 568 clean_up and die RED "[ERROR] built-in queue not allowed in multiple queues declaration\n"; 569 } 570 571 if (($queue =~ /:/) && queue_is_builtin($queue)) { 572 clean_up and die RED "[ERROR] built-in queues are not allowed to have an escalation number\n"; 573 } 574 575 if (defined($escalation)) { 576 if ($escalation !~ m/^[1-9]\d*$/) { 577 clean_up and die RED "[ERROR] escalation number must be a positive integer greater than zero\n"; 578 } 579 580 if ($escalation >= $max_escalation) { 581 $max_escalation = $escalation; 582 } else { 583 clean_up and die RED "[ERROR] escalation numbers must increase from left to right in the queue list\n"; 584 } 585 586 $queue_escalation{$queue} = $escalation; 587 } else { 588 if ($max_escalation) { 589 clean_up and die RED "[ERROR] all queues without escalation numbers must be listed more left than the queues with escalation numbers\n"; 590 } 591 } 592 } 593 594 if (@queue > 1 and $queue[0] =~ /:/) { 595 clean_up and die RED "[ERROR] left most queue in a multiple queue declaration can not have an escalation number\n"; 596 } 597 598 if ($queue eq 'group') { push @group_stack, scalar(@regexp); } 599 if ($queue eq 'group_host') { push @group_stack, scalar(@regexp); } 600 push @regexp, qr/$reg/; 601 $queue =~ s/:\d*//g; 602 push @queues, $queue; 603 push @queues_escalation, \%queue_escalation; 604 push @skip, 0; 605 606 } 607 elsif (/^group_end/) { 608 609 if (scalar(@group_stack) < 1) { 610 clean_up and die RED "[ERROR] tried to close a group when there are non open\n"; 611 } 612 613 $skip[pop @group_stack] = scalar(@regexp) || 0; 614 615 } else { 616 clean_up and die RED "[ERROR] invalid configuration directive: $_\n"; 617 } 618 } 619 620 close $CONF; 621 622 clean_up and die RED "[ERROR] no smtp server specified" if (!$mailserver); 623 624 $debug && debug(2,$config_file); 625 626 if ($debug) { 627 for (my $i = 0; $i < scalar(@regexp); $i++) { 628 debug(18, $i, $regexp[$i]); 629 } 630 } 631} 632 633sub parse_line { 634 my $line = $_[0]; 635 636 if (defined $line) { 637 638 if ($time_to_die) { next; } 639 640 my $hostname; 641 my $has_prefix; 642 643 chomp($line); $debug && debug(6,$line); 644 645 foreach my $log_prefix (@log_prefix) { 646 if ($line =~ s/$log_prefix//) { 647 $has_prefix = 1; 648 $hostname = $1; last; 649 } 650 } 651 652 if (!$has_prefix && $main{'noprefix'}) { 653 $debug && debug(7,'noprefix',$line); 654 $main{'noprefix'}{'logs'}{'[unprefixed logs]'}{$line}++; 655 } 656 657 return unless defined($hostname); 658 659 if ($hidepid) { $line =~ s/^(\S+)\[\d+\]: /$1: /o; } 660 661 for (my $index = 0; $index <= $#regexp; $index++) { 662 my $regexp = $regexp[$index]; 663 my $queue = $queues[$index]; 664 my $queue_escalation = $queues_escalation[$index]; 665 my @queue = split(/,/, $queues[$index]); 666 667 if ($queue eq 'group_host') { 668 if ($hostname =~ /$regexp/) { 669 next; 670 } elsif ($skip[$index] > 0) { 671 $debug && debug(9,$skip[$index],$line); 672 $index = ($skip[$index] - 1); 673 next; 674 } 675 } 676 677 if ($line =~ /$regexp/) { 678 $debug && debug(7,$queue,$line); 679 680 if ($queue eq 'trash') { 681 $last_queue{$hostname} = $queue; 682 $last_match{$hostname} = $line; 683 last; 684 } 685 686 next if ($queue eq 'group'); 687 688 if ($queue eq 'repeat' and $last_match{$hostname}) { 689 unless (defined $1) { 690 $debug && debug(22); 691 last; 692 } 693 694 my @last_queue = split(/,/, $last_queue{$hostname}); 695 696 foreach my $last_queue (@last_queue) { 697 next if $last_queue eq 'trash'; 698 $main{$last_queue}{'logs'}{$hostname}{$last_match{$hostname}} += $1; 699 } 700 701 last; 702 } 703 704 my $offset = 0; 705 my ($begin, $end); 706 707 foreach my $i (1 .. $#-) { 708 next unless defined($-[$i]); 709 710 $begin = $-[$i] + $offset; 711 $end = $+[$i] + $offset; 712 my $length = ($end - $begin); 713 714 substr($line, $begin, $length, $mask); 715 716 $offset += ($mask_length - $length); 717 } 718 719 $debug && debug(8,$line); 720 721 $last_queue{$hostname} = $queue; 722 $last_match{$hostname} = $line; 723 724 $regexp_matches{$hostname}[$index]++; 725 726 my $max_escalation = $queue_escalation->{$queue[$#queue]}; 727 728 foreach my $queue (@queue) { 729 my $escalation = $queue_escalation->{$queue}; 730 731 if ($escalation) { 732 # If the regexp has matched enough lines to escalate, put the line in the queue. 733 # When the queue with the largest escalation number receives a message, then we 734 # want the escalation count to essentially reset, looping escalation back around 735 # through the other escalation queues, but without actually resetting the number 736 # of matches. 737 my $lines = $regexp_matches{$hostname}[$index] % $max_escalation; 738 if ($escalation == $lines || ($escalation == $max_escalation && $lines == 0)) { 739 $main{$queue}{'logs'}{$hostname}{$line} = $regexp_matches{$hostname}[$index]; 740 } 741 } else { 742 $main{$queue}{'logs'}{$hostname}{$line}++; 743 } 744 } 745 746 last; 747 } 748 elsif ($skip[$index] > 0) { 749 $debug && debug(9,$skip[$index],$line); 750 $index = ($skip[$index] - 1); 751 } 752 } 753 } 754} 755 756sub queue_is_builtin { 757 my $queue = shift; 758 759 if (($queue eq 'trash') or ($queue eq 'repeat') or ($queue eq 'group') or ($queue eq 'group_host')) { 760 return 1; 761 } 762 763 return 0; 764} 765 766sub queues_check { 767 my $now = shift; 768 my @time = localtime($now); 769 my $check_crons = 0; 770 771 $last_check = $now; 772 773 my $current_minute = floor($now / 60); 774 775 if ($current_minute > $last_minute) { 776 $check_crons = 1; 777 $last_minute = $current_minute; 778 } 779 780 $debug && debug(12); 781 782 foreach my $queue (keys %main) { 783 784 next if queue_is_builtin($queue); 785 786 if ($main{$queue}{'now'} || ($check_crons && cron_mask_match(\@time, $main{$queue}{'cron_mask'}))) { 787 if ($queue eq 'csv') { 788 csv_out(); 789 next; 790 } 791 792 queue_mail($queue) if (!$profile); 793 } 794 } 795} 796 797sub queues_flush { 798 $debug && debug(13); 799 800 foreach my $queue (keys %main) { 801 next if queue_is_builtin($queue); 802 803 if ($queue eq 'csv') { 804 csv_out(); 805 next; 806 } 807 808 queue_mail($queue) if (!$profile); 809 } 810 if ($status) { $status = 0; } 811} 812 813sub queue_mail { 814 my $queue = shift; 815 my @lines; 816 817 # evaluate threshold first to prevent report if none of the lines appeared often enough 818 if ($main{$queue}{'threshold'}) { 819 my @t = @{$main{$queue}{'threshold'}}; 820 821 foreach my $hostname (keys %{$main{$queue}{'logs'}}) { 822 foreach my $key (keys %{$main{$queue}{'logs'}{$hostname}}) { 823 for (my $x = 0; $x < @t; $x += 2) { 824 # compare count first then regex 825 if ($main{$queue}{'logs'}{$hostname}{$key} < $t[$x+1] && $key =~ $t[$x]) { 826 delete $main{$queue}{'logs'}{$hostname}{$key}; 827 last; 828 } 829 } 830 } 831 832 delete $main{$queue}{'logs'}{$hostname} unless(keys %{$main{$queue}{'logs'}{$hostname}}); 833 } 834 835 delete $main{$queue}{'logs'} unless(keys %{$main{$queue}{'logs'}}); 836 } 837 838 return unless (keys %{$main{$queue}{'logs'}}); 839 $debug && debug(11,$queue); 840 841 my $tmp = new File::Temp(UNLINK => 1, DIR => '/tmp/', SUFFIX => '.tenshi') 842 or clean_up and die RED "[ERROR] could not open temporary file: $!\n"; 843 $debug && debug(19,$tmp); 844 845 if ($status and (!$main{$queue}{'pager'})) { 846 print $tmp "*** Status: $status ***\n"; 847 } 848 849 foreach my $hostname (keys %{$main{$queue}{'logs'}}) { 850 my $index = 0; 851 852 next unless (keys %{$main{$queue}{'logs'}{$hostname}}); 853 854 if ($resolve && !$main{$queue}{'pager'}) { 855 my $ipaddr = inet_aton($hostname); 856 857 if ($ipaddr && !defined $hostnames{$ipaddr}) { 858 $hostnames{$ipaddr} = gethostbyaddr($ipaddr, AF_INET); 859 } 860 861 if (defined $ipaddr && defined $hostnames{$ipaddr}) { 862 print $tmp "\n$hostname ($hostnames{$ipaddr}): \n"; 863 } else { 864 print $tmp "\n$hostname: \n"; 865 } 866 } else { 867 print $tmp "\n$hostname: \n" if (!$main{$queue}{'pager'}); 868 } 869 870 my @sorted_keys; 871 872 if ($sort_order eq 'descending') { 873 @sorted_keys = reverse sort { $main{$queue}{'logs'}{$hostname}{$a} <=> $main{$queue}{'logs'}{$hostname}{$b} } keys %{$main{$queue}{'logs'}{$hostname}}; 874 } elsif ($sort_order eq 'ascending') { 875 @sorted_keys = sort { $main{$queue}{'logs'}{$hostname}{$a} <=> $main{$queue}{'logs'}{$hostname}{$b} } keys %{$main{$queue}{'logs'}{$hostname}}; 876 } 877 878 foreach my $key (@sorted_keys) { 879 if ($main{$queue}{'pager'}) { 880 last if ($pager_limit and ($index >= $pager_limit)); 881 print $tmp "$hostname,$main{$queue}{'logs'}{$hostname}{$key},$key\n"; 882 $index++; 883 } else { 884 last if ($limit and ($index >= $limit)); 885 print $tmp " $main{$queue}{'logs'}{$hostname}{$key}: $key\n"; 886 $index++; 887 } 888 } 889 890 print $tmp "\n *** Too many alerts (limit: $limit) ***\n" 891 if ($limit and ($index >= $limit)); 892 893 # clear regexp match count for every regexp whose left most queue is the one being mailed 894 for (my $index = 0; $index <= $#regexp; $index++) { 895 my @q = split(/,/, $queues[$index]); 896 $regexp_matches{$hostname}[$index] = 0 if $q[0] eq $queue; 897 } 898 } 899 900 seek($tmp, 0, 0) or clean_up and die RED "[ERROR] can't rewind $tmp->filename: $!\n"; 901 902 if ($filter_file{$queue}) { 903 local $SIG{CHLD} = 'IGNORE'; # FIXME it's ugly I know, need something smarter here 904 905 $debug && debug(20,"$filter_file{$queue} $filter_args{$queue} < $tmp"); 906 907 open(my $filter, "$filter_file{$queue} $filter_args{$queue} < $tmp|") or 908 clean_up and die RED "[ERROR] '$filter_file{$queue} $filter_args{$queue} < $tmp' failed: $!\n"; 909 910 while (<$filter>) { push @lines, $_; } 911 } else { 912 while (<$tmp>) { push @lines, $_; } 913 } 914 915 return unless (scalar(@lines) > 0); 916 917 my $smtp = Net::SMTP->new($mailserver, Hello => $mailhelo, Timeout => $mailtimeout, Debug => $debug_smtp); 918 919 if (!$smtp) { 920 print RED "[ERROR] could not contact $mailserver:25\n"; 921 return; 922 } 923 if (!$smtp->mail($main{$queue}{'mailfrom'})) { 924 print RED "[ERROR] mail from: $main{$queue}{'mailfrom'} rejected\n"; 925 return; 926 } 927 if (!$smtp->to(split(/,/, $main{$queue}{'mailto'}))) { 928 print RED "[ERROR] rcpt to: $main{$queue}{'mailto'} rejected\n"; 929 return; 930 } 931 if (!$smtp->data()) { 932 print RED "[ERROR] data rejected\n"; 933 return; 934 } 935 936 my $timezone = get_timezone(); 937 my $subject = $main{$queue}{'subject'} || $subject; 938 939 $smtp->datasend("From: $main{$queue}{'mailfrom'}\n"); 940 $smtp->datasend("To: $main{$queue}{'mailto'}\n"); 941 $smtp->datasend("Date: " . strftime("%a, %d %b %Y %H:%M:%S $timezone", localtime()) . "\n"); 942 $smtp->datasend("X-tenshi-version: $version\n"); 943 $smtp->datasend("X-tenshi-hostname: $our_hostname\n"); 944 945 if (!$main{$queue}{'now'}) { 946 my @now = localtime(); 947 948 $main{$queue}{'report_time'} = [ @startup_time ] if (!$main{$queue}{'report_time'}); 949 $smtp->datasend("X-tenshi-report-start: " . strftime("%a %b %d %H:%M:%S $timezone %Y", @{$main{$queue}{'report_time'}}) . "\n"); 950 $main{$queue}{'report_time'} = [ @now ]; 951 } 952 953 $smtp->datasend("Subject: $subject [$queue]\n\n"); 954 $smtp->datasend(@lines); 955 $smtp->dataend(); 956 $smtp->quit; 957 $main{$queue}{'logs'} = {}; 958} 959 960sub csv_out { 961 # FIXME: too much code duplication here, need better functions 962 963 return unless (keys %{$main{'csv'}{'logs'}}); 964 965 if (!$main{'csv'}{'now'}) { 966 my @now = localtime(); 967 968 $main{'csv'}{'report_time'} = [ @startup_time ] if (!$main{'csv'}{'report_time'}); 969 $main{'csv'}{'report_time'} = [ @now ]; 970 } 971 972 my $tmp = new File::Temp(UNLINK => 1, DIR => '/tmp/', SUFFIX => '.tenshi') 973 or clean_up and die RED "[ERROR] could not open temporary file: $!\n"; 974 $debug && debug(19,$tmp); 975 976 foreach my $hostname (keys %{$main{'csv'}{'logs'}}) { 977 my $index = 0; 978 979 next unless (keys %{$main{'csv'}{'logs'}{$hostname}}); 980 981 my @sorted_keys = sort { $main{'csv'}{'logs'}{$hostname}{$a} <=> $main{'csv'}{'logs'}{$hostname}{$b} } keys %{$main{'csv'}{'logs'}{$hostname}}; 982 983 foreach my $key (@sorted_keys) { 984 print $tmp "$hostname,\"$key\",$main{'csv'}{'logs'}{$hostname}{$key}\n"; 985 $index++; 986 } 987 988 # clear regexp match count for every regexp whose left most queue is the one being mailed 989 for (my $index = 0; $index <= $#regexp; $index++) { 990 my @q = split(/,/, $queues[$index]); 991 $regexp_matches{$hostname}[$index] = 0 if $q[0] eq 'csv'; 992 } 993 } 994 995 seek($tmp, 0, 0) or clean_up and die RED "[ERROR] can't rewind $tmp->filename: $!\n"; 996 997 local $SIG{CHLD} = 'IGNORE'; # FIXME it's ugly I know, need something smarter here 998 999 $debug && debug(20,"$main{'csv'}{'path'} $main{'csv'}{'args'} < $tmp"); 1000 1001 open(my $filter, "$main{'csv'}{'path'} $main{'csv'}{'args'} < $tmp|") or 1002 die RED "[ERROR] '$main{'csv'}{'path'} $main{'csv'}{'args'} < $tmp' failed: $!\n"; 1003 1004 $main{'csv'}{'logs'} = {}; 1005} 1006 1007sub prepare_process { 1008 $0 = 'tenshi'; 1009 chdir '/' or clean_up and die RED "[ERROR] can't chdir to /: $!\n"; 1010 if($> == 0) { # only works when root 1011 $) = "$gid $gid"; (!$!) or clean_up and die RED "[ERROR] can't reset supplementary groups: $!\n"; 1012 } 1013 setgid($gid) or clean_up and die RED "[ERROR] can't setgid to $gid: $!\n"; 1014 $> = $uid; (!$!) or clean_up and die RED "[ERROR] can't seteuid to $uid: $!\n"; 1015 close STDIN or clean_up and die RED "[ERROR] can't close STDIN: $!\n"; 1016 open(STDIN, "/dev/null") or clean_up and die RED "[ERROR] can't open STDIN as /dev/null: $!\n"; 1017} 1018 1019sub daemonize { 1020 defined(my $pid = fork) or clean_up and die RED "[ERROR] can't fork: $!\n"; 1021 exit if $pid; 1022 setsid() or clean_up and die RED "[ERROR] can't start a new session: $!\n"; 1023 setuid($<) or clean_up and die RED "[ERROR] can't setuid to $<: $!\n"; 1024 save_pid(); 1025 setuid($uid) or clean_up and die RED "[ERROR] can't setuid to $uid: $!\n"; 1026 1027 close STDOUT or clean_up and die RED "[ERROR] can't close STDOUT: $!\n"; 1028 open(STDOUT, ">/dev/null") or clean_up and die RED "[ERROR] can't open STDOUT as /dev/null: $!\n"; 1029 close STDERR or clean_up and die RED "[ERROR] can't close STDERR: $!\n"; 1030 open(STDERR, ">/dev/null") or clean_up and die RED "[ERROR] can't open STDERR as /dev/null: $!\n"; 1031} 1032 1033sub save_pid { 1034 open (PIDFILE,">$pid_file") or clean_up and die RED "[ERROR] could not open pid file $pid_file: $!\n"; 1035 print PIDFILE $$; $debug && debug(4,$$); 1036 close PIDFILE; 1037} 1038 1039sub clean_up { 1040 my $save = $!; # preserve $! for the call to die 1041 local $SIG{CHLD} = 'IGNORE'; 1042 1043 if (scalar(@tail_pids) > 0) { 1044 $debug && debug(21, join(' ', @tail_pids)); 1045 kill("SIGTERM", @tail_pids); 1046 } 1047 1048 foreach my $fh (@fhs) { 1049 close $fh; 1050 if ($listen and $fh == $syslog_listen_socket) { 1051 $syslog_listen_socket->close(); 1052 } 1053 } 1054 1055 $redis->quit if (defined $redis); 1056 1057 $! = $save; 1058 return 1; 1059} 1060 1061sub get_timezone { 1062 use Time::Local; 1063 1064 my @time = localtime(); 1065 my $timediff = (timegm(@time) - timelocal(@time)); 1066 return sprintf("%+03d%02d", $timediff/3600 , $timediff%3600/60); 1067} 1068 1069sub cron_field_resolve { 1070 my $field = lc(shift); 1071 my $strings_ref = shift; 1072 1073 if (ref($strings_ref) && $strings_ref->{$field}) { 1074 return $strings_ref->{$field}; 1075 } 1076 else { 1077 return $field; 1078 } 1079} 1080 1081sub cron_spec_to_mask { 1082 my $string = shift; 1083 my @mask; 1084 1085 for (my $i = 0; $i < scalar(@cron_specs); $i++) { 1086 $string =~ s/^(\S+)\s*//o 1087 or clean_up and die RED "[ERROR] unable to parse cron string: $string\n"; 1088 1089 my $cron_spec = $cron_specs[$i]; 1090 1091 my @mask_fields; 1092 $#mask_fields = $cron_spec->{'max'} + $cron_spec->{'shift'}; 1093 @mask_fields = map { 0 } @mask_fields; 1094 1095 foreach my $field (split(/,/, $1)) { 1096 my $start = 0; 1097 my $end = 0; 1098 my $skip = 1; 1099 if ($field =~ /\*(?:\/([0-9]+))?/o) { 1100 $start = $cron_spec->{'min'}; 1101 $end = $cron_spec->{'max'}; 1102 if ($1) { $skip = $1 } 1103 } 1104 else { 1105 1106 if (!($field =~ /(\w+)(?:-(\w+)(?:\/([0-9]+))?)?/o)) { 1107 clean_up and die RED "[ERROR] error in field syntax: $field\n"; 1108 } 1109 1110 if ($#- == 1) { 1111 $start = cron_field_resolve($1, $cron_spec->{'strings'}); 1112 $end = cron_field_resolve($1, $cron_spec->{'strings'}); 1113 } 1114 elsif ($#- == 2) { 1115 $start = cron_field_resolve($1, $cron_spec->{'strings'}); 1116 $end = cron_field_resolve($2, $cron_spec->{'strings'}); 1117 } 1118 elsif ($#- == 3) { 1119 $start = cron_field_resolve($1, $cron_spec->{'strings'}); 1120 $end = cron_field_resolve($2, $cron_spec->{'strings'}); 1121 $skip = $3; 1122 } 1123 1124 if ($start > $end) { 1125 clean_up and die RED "[ERROR] error in field syntax. Ranges should be <lower>-<higher>: $field\n" 1126 } 1127 } 1128 1129 if ($start < $cron_spec->{'min'}) { 1130 clean_up and die RED "[ERROR] $start is below minimum value for field in: $field\n"; 1131 } 1132 1133 if ($end > $cron_spec->{'max'}) { 1134 clean_up and die RED "[ERROR] $end is above maximum value for field in: $field\n"; 1135 } 1136 1137 if ($cron_spec->{'shift'}) { 1138 $start += $cron_spec->{'shift'}; 1139 $end += $cron_spec->{'shift'}; 1140 } 1141 1142 for (my $j = $start; $j <= $end; $j += $skip) { 1143 if (($j == $end) && $cron_spec->{'wrap'} && ($j == $cron_spec->{'max'})) { 1144 $mask_fields[$cron_spec->{'min'}] = 1; 1145 last; 1146 } 1147 $mask_fields[$j] = 1; 1148 } 1149 1150 $mask[$i] = \@mask_fields; 1151 } 1152 } 1153 1154 return \@mask; 1155} 1156 1157sub cron_mask_match { 1158 my @time = @{shift()}; 1159 my @mask = @{shift()}; 1160 1161 $debug && debug(15, join(' - ', map { join(',', @{$_}) } @mask), join(',', @time)); 1162 1163 for (my $i = 0; $i < scalar(@mask); $i++) { 1164 if (!$mask[$i]->[$time[$cron_specs[$i]->{'localtime_field'}]]) { 1165 $debug && debug(16); 1166 return 0; 1167 } 1168 } 1169 1170 $debug && debug(17); 1171 return 1; 1172} 1173 1174sub debug { 1175 if (!defined($_[1])) { $_[1] = 'foo'; } 1176 if (!defined($_[2])) { $_[2] = 'foo'; } 1177 1178 my (%debug_msg); 1179 1180 $debug_msg{'0'}{'msg'} = "[CONF] reading config file $_[1]\n"; 1181 $debug_msg{'0'}{'col'} = CYAN; 1182 1183 $debug_msg{'1'}{'msg'} = "[CONF] parsing conf directive - $_[1]\n"; 1184 $debug_msg{'1'}{'col'} = CYAN; 1185 1186 $debug_msg{'2'}{'msg'} = "[CONF] configuration file $_[1] successfully parsed\n"; 1187 $debug_msg{'2'}{'col'} = WHITE; 1188 1189 $debug_msg{'3'}{'msg'} = "[INIT] entering monitoring loop\n"; 1190 $debug_msg{'3'}{'col'} = BLUE; 1191 1192 $debug_msg{'4'}{'msg'} = "[INIT] saving pid $$ in $pid_file\n"; 1193 $debug_msg{'4'}{'col'} = MAGENTA; 1194 1195 $debug_msg{'5'}{'msg'} = "[MAIN] trapped $_[1] signal!\n"; 1196 $debug_msg{'5'}{'col'} = RED; 1197 1198 $debug_msg{'6'}{'msg'} = "[MAIN] got message: $_[1]\n"; 1199 $debug_msg{'6'}{'col'} = WHITE; 1200 1201 $debug_msg{'7'}{'msg'} = "[MAIN] matched message for queue $_[1]: $_[2]\n"; 1202 $debug_msg{'7'}{'col'} = GREEN; 1203 1204 $debug_msg{'8'}{'msg'} = "[MAIN] masked message: $_[1]\n"; 1205 $debug_msg{'8'}{'col'} = RED; 1206 1207 $debug_msg{'9'}{'msg'} = "[MAIN] skipping to regex: $_[1] after failed match for group regex on line: $_[2]\n"; 1208 $debug_msg{'9'}{'col'} = YELLOW; 1209 1210 $debug_msg{'11'}{'msg'} = "[QUEUE] flushing queue $_[1]\n"; 1211 $debug_msg{'11'}{'col'} = RED; 1212 1213 $debug_msg{'12'}{'msg'} = "[QUEUE] checking queues\n"; 1214 $debug_msg{'12'}{'col'} = CYAN; 1215 1216 $debug_msg{'13'}{'msg'} = "[QUEUE] flushing all queues\n"; 1217 $debug_msg{'13'}{'col'} = RED; 1218 1219 $debug_msg{'14'}{'msg'} = "[CRON] creating cron mask from: $_[1]\n"; 1220 $debug_msg{'14'}{'col'} = GREEN; 1221 1222 $debug_msg{'15'}{'msg'} = "[CRON] testing mask: $_[1] against current time: $_[2]\n"; 1223 $debug_msg{'15'}{'col'} = GREEN; 1224 1225 $debug_msg{'16'}{'msg'} = "[CRON] test returned negative\n"; 1226 $debug_msg{'16'}{'col'} = GREEN; 1227 1228 $debug_msg{'17'}{'msg'} = "[CRON] test returned positive\n"; 1229 $debug_msg{'17'}{'col'} = GREEN; 1230 1231 $debug_msg{'18'}{'msg'} = "[REGEX] set regex: $_[1] to: $_[2]\n"; 1232 $debug_msg{'18'}{'col'} = YELLOW; 1233 1234 $debug_msg{'19'}{'msg'} = "[FILE] opening $_[1]\n"; 1235 $debug_msg{'19'}{'col'} = BLUE; 1236 1237 $debug_msg{'20'}{'msg'} = "[EXEC] executing $_[1]\n"; 1238 $debug_msg{'20'}{'col'} = CYAN; 1239 1240 $debug_msg{'21'}{'msg'} = "[EXEC] killing child processes [pids: $_[1]]\n"; 1241 $debug_msg{'21'}{'col'} = CYAN; 1242 1243 $debug_msg{'22'}{'msg'} = "[REGEX] repeat queue matched without capturing number of lines repeated; skipping\n"; 1244 $debug_msg{'22'}{'col'} = RED; 1245 1246 $debug_msg{'23'}{'msg'} = "[REDIS] $_[1]\n"; 1247 $debug_msg{'23'}{'col'} = RED; 1248 1249 $debug_msg{'100'}{'msg'} = "[ERROR] tried to change a protected setting: $_[1], please restart tenshi for this change to take effect\n"; 1250 $debug_msg{'100'}{'col'} = RED; 1251 1252 print $debug_msg{$_[0]}{'col'}, $debug_msg{$_[0]}{'msg'}, RESET; 1253} 1254 1255sub usage { 1256 die "tenshi $version https://github.com/inversepath/tenshi 1257Copyright 2004-2017 Andrea Barisani || <andrea\@inversepath.com>\n 1258Usage: $0 [-c <conf file>] [-C|-f|-p] [-d <debug level>] [-P <pid file>] 1259 -c configuration file 1260 -C test configuration syntax 1261 -d debug level 1262 -f foreground mode 1263 -p profile mode 1264 -P pid file 1265 -h this help\n\n"; 1266} 1267 1268# vim: set ts=4 sw=4 expandtab: 1269