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