#!/usr/bin/perl -w -d # Copyright (C) 2000 Daniel J. Urist # Contact: Daniel J. Urist # Portions of this code are Copyright (C) Jim Trocki # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. # FIXME # - all options supported from latest mon release? # - hostgroup crashes (still!); right now we preload the debugger as a kluge # - path validations should test for regular file vs. directory # RECONSIDER # - current validation scheme for globals warns but sets them anyway? # # TODO: # - Edit help doco from mon to make it more relevant to the GUI # - More validation # - Watches that don't have any periods should be grayed out # - More use should be made of the Watches HList; it should come up # with just a list of hostgroups; double-clicking on a hostgroup should # bring up a list of watches. Ideally, we should be able to drag and # drop watches between hostgroups. # - Add dialog for config file reopen # Tell the debugger to go nonstop BEGIN { $ENV{PERLDB_OPTS} = "N"; } my $Version = '1.0'; require 5.00503; use strict; use Time::Period; # Needed for read_cf use Tk; use Tk::Dialog; use Tk::Scale; use Tk::Balloon; use Tk::HList; use Tk::BrowseEntry; use Tk::Pane; # Global variables for config file my $monconfigfile; # Config file my $savefile; # Save file my %CF; my %groups; my %watch; my %globals; # We have to get this ourselves; read_cf doesn't do it completely # Globals for windows we only want one of my $Help; my $Balloon; # For any and all balloon help my $Edit_globals; my $Edit_global_submenu; my $Edit_hostgroups; my $Edit_hostgroups_add; my $Edit_watches; my $Edit_watches_edit; my $Edit_watches_edit_period; my $Edit_watches_edit_period_add_alert; # # Tk Entry function used a lot in the hash below # sub GLOBALS_entry { my($parent, $value, $g, $msg, $widgets) = @_; my $id = $parent->Entry(); $id->insert(0, $value); $Balloon->attach($id, -balloonmsg => $msg); return $id; } # # Path validation function used a lot in the hash # sub GLOBALS_path_validate { my($parent, $val) = @_; $parent->Dialog(-title => 'Warning', -text => "The path \"$val\" Does Not Exist")->Show unless -e $val; } # # All possible global vars in the mon config file and the widget we use to display and set them # # The "widget" subroutine gets passed the following args: # parent widget ID # current value of variable # ref to duped globals hash # balloon help message # ref to hash of widget ids in parent window # # This is an odd assortment of stuff, but it's convenient. It would be really nice if Perl provided # some way to do simple in-line objects without having to go through the hassle of creating packages. # # The "validate" subroutine gets passed the parent id and the value # my %GLOBALS = ( 'ORDER' => [ 'Paths', 'Authentication', 'Tuning', 'History', 'Ports', 'Downtime logging', 'Dependency behavior' ], 'Paths' => { 'ORDER' => ['basedir', 'mondir', 'alertdir', 'logdir', 'statedir', 'pidfile'], 'basedir' => { 'bmsg' => 'The full path for the state, script, and alert directory (optional)', 'widget' => \&GLOBALS_entry, 'validate' => sub { my($parent, $val) = @_; &GLOBALS_path_validate($parent, $val) if $val; }, }, 'alertdir' => { 'bmsg' => 'The full path to the alert scripts', 'widget' => \&GLOBALS_entry, 'validate' => \&GLOBALS_path_validate, }, 'mondir' => { 'bmsg' => 'The full path to the monitor scripts', 'widget' => \&GLOBALS_entry, 'validate' => \&GLOBALS_path_validate, }, 'statedir' => { 'bmsg' => 'The full path to the state directory', 'widget' => \&GLOBALS_entry, 'validate' => \&GLOBALS_path_validate, }, 'logdir' => { 'bmsg' => 'The full path to the log directory', 'widget' => \&GLOBALS_entry, 'validate' => \&GLOBALS_path_validate, }, 'pidfile' => { 'bmsg' => 'The file the sever will store its pid in', 'widget' => \&GLOBALS_entry, 'validate' => \&GLOBALS_path_validate, }, }, 'Authentication' => { 'ORDER' => [ 'authfile', 'authtype', 'userfile'], 'authfile' => { 'bmsg' => 'The full path to the authentication file', 'widget' => \&GLOBALS_entry, 'validate' => \&GLOBALS_path_validate, }, 'authtype' => { 'bmsg' => 'The type of authentication to use', 'widget' => sub { my($parent, $value, $g, $msg, $widgets) = @_; my $type; my $F = $parent->Frame; foreach $type ( 'getpwnam', 'userfile' ){ $_ = $F->Radiobutton(-text => $type, value => $type, -variable => \$globals{authtype}, # Toggle the state of userfile -command => sub { if($g->{authtype} eq "userfile"){ $widgets->{userfile}->configure(-state=>'normal'); } else{ $widgets->{userfile}->configure(-state=>'disabled'); } } )->pack(-side => 'left'); } $Balloon->attach($F, -balloonmsg => $msg); return $F; }, 'validate' => sub {}, }, 'userfile' => { 'bmsg' => 'This file is used when authtype is set to userfile', 'widget' => sub{ my($parent, $value, $g, $msg) = @_; my $id; if( $g->{authtype} eq "userfile" ){ $id = $parent->Entry(); $Balloon->attach($id, -balloonmsg => $msg); } else { $id = $parent->Entry(-state => 'disabled'); } $id->insert(0, $value); return $id; }, 'validate' => sub { &GLOBALS_path_validate(@_) if $globals{authtype} eq "userfile"; }, }, }, 'Tuning' => { 'ORDER' => ['maxprocs', 'cltimeout', 'randstart'], 'maxprocs' => { 'bmsg' => 'Limit on the number of concurrently forked processes', 'widget' => \&GLOBALS_entry, 'validate' => sub { my($parent, $val) = @_; $parent->Dialog(-title => 'Warning', -text => "You must enter a number!")->Show unless $val =~ /^\d+$/ }, }, 'cltimeout' => { 'bmsg' => 'Client inactivity timeout in seconds', 'widget' => \&GLOBALS_entry, 'validate' => sub { my($parent, $val) = @_; $parent->Dialog(-title => 'Warning', -text => "You must enter a number!")->Show unless $val =~ /^\d+$/ }, }, 'randstart' => { 'bmsg' => 'Randomize runtime of all services within this window (seconds)', 'widget' => \&GLOBALS_entry, 'validate' => sub { my($parent, $val) = @_; $parent->Dialog(-title => 'Warning', -text => "You must enter a number!")->Show unless $val =~ /^\d+$/ }, }, }, # # FIXME need to somehow get the "use snmp" option in here # 'SNMP support' => { # 'snmpport' => { 'value' => "", 'bdoc' => "", }, # }, 'Ports' => { 'ORDER' => [ 'serverport', 'serverbind', 'trapport', 'trapbind'], 'serverport' => { 'bmsg' => 'The TCP port number that the server should bind to', 'widget' => sub { my($parent, $value, $g, $msg) = @_; my $id = $parent->Scale('-from' => 1, '-to' => '65535', -orient => 'horizontal', -tickinterval => 20000, -length => 200); $id->set($value); $Balloon->attach($id, -balloonmsg => $msg); return $id; }, 'validate' => sub {}, #FIXME check to see if this port is in use }, 'serverbind' => { 'bmsg' => 'Address to bind the server port to', 'widget' => \&GLOBALS_entry, 'validate' => sub {}, #FIXME make sure this is valid }, 'trapport' => { 'bmsg' => 'The UDP port number that the trap server should bind to', 'widget' => sub { my($parent, $value, $g, $msg) = @_; my $id = $parent->Scale('-from' => 1, '-to' => '65535', -orient => 'horizontal', -tickinterval => 20000, -length => 200); $id->set($value); $Balloon->attach($id, -balloonmsg => $msg); return $id; }, 'validate' => sub {}, #FIXME check to see if this port is in use }, 'trapbind' => { 'bmsg' => 'Address to bind the trap port to', 'widget' => \&GLOBALS_entry, 'validate' => sub {}, #FIXME make sure this is valid }, }, 'Downtime logging' => { 'ORDER' => [ 'dtlogging', 'dtlogfile' ], 'dtlogging' => { 'bmsg' => 'Turns downtime logging on or off', 'widget' => sub { my($parent, $value, $g, $msg, $widgets) = @_; my $F = $parent->Frame; foreach $_ ('yes', 'no'){ $F->Radiobutton(-text => $_, value => $_, -variable => \$g->{dtlogging}, -command => sub { if($g->{dtlogging} eq "yes"){ $widgets->{dtlogfile}->configure(-state=>'normal'); } else{ $widgets->{dtlogfile}->configure(-state=>'disabled'); } } )->pack(-side => 'left'); } return $F; }, 'validate' => sub {}, }, 'dtlogfile' => { 'bmsg' => 'File which will be used to record the downtime log', 'widget' => sub{ my($parent, $value, $g, $msg) = @_; my $id; if( $g->{dtlogging} eq "yes" ){ $id = $parent->Entry(); $Balloon->attach($id, -balloonmsg => $msg); } else { $id = $parent->Entry(-state => 'disabled'); } $id->insert(0, $value); return $id; }, 'validate' => sub { &GLOBALS_path_validate(@_) if $globals{dtlogging} eq "yes"; }, }, }, 'History' => { 'ORDER' => [ 'historicfile', 'histlength', 'historictime'], 'histlength' => { 'bmsg' => 'The maximum number of events to be retained in history list', 'widget' => sub { my($parent, $value, $g, $msg) = @_; my $id = $parent->Scale('-from' => 0, '-to' => 1000, -orient => 'horizontal', -tickinterval => 300, -length => 200); $id->set($value); $Balloon->attach($id, -balloonmsg => $msg); return $id; }, 'validate' => sub {}, }, 'historicfile' => { 'bmsg' => 'File to store alert history in', 'widget' => \&GLOBALS_entry, 'validate' => \&GLOBALS_path_validate, }, 'historictime' => { 'bmsg' => 'The amount of the history file to read upon startup (s, m, h)', 'widget' => \&GLOBALS_entry, 'validate' => sub {}, #FIXME make this real }, }, 'Dependency behavior' => { 'ORDER' => [ 'dep_recur_limit', 'dep_behavior' ], 'dep_recur_limit' => { 'bmsg' => 'Limit dependency recursion level to depth', 'widget' => sub { my($parent, $value, $g, $msg) = @_; my $id = $parent->Scale(-from => 0, -to => 100, -orient => 'horizontal', -tickinterval => 20, -length => 200); $id->set($value); $Balloon->attach($id, -balloonmsg => $msg); return $id; }, 'validate' => sub {}, }, 'dep_behavior' => { 'bmsg' => 'Controls whether the dependency expression suppresses alerts or monitors', 'widget' => sub { my($parent, $value, $g, $msg) = @_; my $F = $parent->Frame; $F->Radiobutton(-text => 'monitor', value => 'm', -variable => \$g->{dep_behavior},)->pack(-side => 'left'); $F->Radiobutton(-text => 'alert', value => 'a', -variable => \$g->{dep_behavior},)->pack(-side => 'right'); return $F; }, 'validate' => sub {}, }, }, ); my %Doc = ( 'Global Configs' => q{ Global Variables The following variables may be set to override compiled-in defaults. Command-line options will have a higher precedence than these definitions. alertdir = dir dir is the full path to the alert scripts. This is the value set by the -a command-line parameter. Multiple alert paths may be specified by separating them with a colon. All paths must be absolute. When the configuration file is read, all alerts referenced from the configuration will be looked up in each of these paths, and the full path to the first instance of the alert found is stored in a hash. This hash is only generated upon startup or after a "reset" command, so newly added alert scripts will not be recognized until a "reset" is performed. mondir = dir dir is the full path to the monitor scripts. This value may also be set by the -s command-line parameter. Multiple alert paths may be specified by separating them with a colon. All paths must be absolute. When the configuration file is read, all monitors referenced from the configuration will be looked up in each of these paths, and the full path to the first instance of the monitor found is stored in a hash. This hash is only generated upon startup or after a "reset" command, so newly added monitor scripts will not be recognized until a "reset" is performed. statedir = dir dir is the full path to the state directory. mon uses this directory to save various state information. logdir = dir dir is the full path to the log directory. mon uses this directory to save various logs, including the downtime log. basedir = dir dir is the full path for the state, script, and alert directory. cfbasedir = dir dir is the full path where all the config files can be found (monusers.cf, auth.cf, etc.). authfile = file file is the full path to the authentication file. authtype = type type is the type of authentication to use. If type is getpwnam, then the standard Unix passwd file authentication method will be used (calls getpwnam(3) on the user and compares the crypt(3)ed version of the password with what it gets from getpwnam). This will not work if shadow passwords are enabled on the system. If type is userfile, then usernames and hashed passwords are read from userfile, which is defined via the userfile configuration variable. If type is shadow, then shadow password may be used (NOT IMPLEMENTED). userfile = file This file is used when authtype is set to userfile. It consists of a sequence of lines of the format 'username : password'. password is stored as the hash returned by the standard Unix crypt(3) function. NOTE: the format of this file is compatible with the Apache file based username/password file format. It is possible to use the htpasswd program supplied with Apache to manage the mon userfile. Blank lines and lines beginning with # are ignored. snmpport = portnum Set the SNMP port that the server binds to. serverbind = addr trapbind = addr serverbind and trapbind specify which address to bind the server and trap ports to, respectively. If these are not defined, the default address is INADDR_ANY, which allows connections on all interfaces. For security reasons, it could be a good idea to bind only to the loopback interface. snmp ={yes|no} Turn on/off SNMP support (currently unimplemented). dtlogfile = file file is a file which will be used to record the downtime log. Whenever a service fails for some amount of time and then stop failing, this even is written to the log. If this parameter is not set, no logging is done. The format of the file is as follows (# is a comment and may be ignored): timenoticed group service firstfail downtime interval summary. timenoticed is the time(2) the service came back up. group service is the group and service which failed. firstfail is the time(2) when the service began to fail. downtime is the number of seconds the service failed. interval is the frequency (in seconds) that the service is polled. summary is the summary line from when the service was failing. dtlogging = yes/no Turns downtime logging on or off. The default is off. histlength = num num is the the maximum number of events to be retained in history list. The default is 100. This value may also be set by the -k command-line parameter. historicfile = file If this variable is set, then alerts are logged to file, and upon startup, some (or all) of the past history is read into memory. historictime = timeval num is the amount of the history file to read upon startup. "Now" - timeval is read. See the explanation of interval in the "Service Definitions" section for a description of timeval. serverport = port port is the TCP port number that the server should bind to. This value may also be set by the -p command-line parameter. Normally this port is looked up via getservbyname(3), and it defaults to 2583. trapport = port port is the UDP port number that the trap server should bind to. Normally this port is looked up via getservbyname(3), and it defaults to 2583. pidfile = path path is the file the sever will store its pid in. This value may also be set by the -P command-line parameter. maxprocs = num Throttles the number of concurrently forked processes to num. The intent is to provide a safety net for the unlikely situation when the server tries to take on too many tasks at once. Note that this situation has only been reported to happen when trying to use a garbled configuration file! You don't want to use a garbled configuration file now, do you? cltimeout = secs Sets the client inactivity timeout to secs. This is meant to help thwart denial of service attacks or recover from crashed clients. secs is interpreted as a "1h/1m/1s" string, where "1m" = 60 seconds. randstart = interval When the server starts, normally all services will not be scheduled until the interval defined in the respective service section. This can cause long delays before the first check of a service, and possibly a high load on the server if multiple things are scheduled at the same intervals. This option is used to randomize the scheduling of the first test for all services during the startup period, and immediately after the reset command. If randstart is defined, the scheduled run time of all services of all watch groups will be a random number between zero and randstart seconds. dep_recur_limit = depth Limit dependency recursion level to depth. If dependency recursion (dependencies which depend on other dependencies) tries to go beyond depth, then the recursion is aborted and a messages is logged to syslog. The default limit is 10. dep_behavior = {a|m} dep_behavior controls whether the dependency expression suppresses either the running of alerts or monitors when a node in the dependency graph fails. Read more about the behavior in the "Service Definitions" section below. This is a global setting which controls the default settings for the service-specified variable. syslog_facility = facility Specifies the syslog facility used for logging. daemon is the default. startupalerts_on_reset = {yes|no} If set to "yes", startupalerts will be invoked when the reset client command is executed. The default is "no". }, 'Host Groups' => q{ Hostgroup entries begin with the keyword hostgroup, and are followed by a hostgroup tag and one or more hostnames or IP addresses, separated by whitespace. The hostgroup tag must be composed of alphanumeric characters, a dash ("-"), a period ("."), or an underscore ("_"). Non-blank lines following the first hostgroup line are interpreted as more hostnames. The hostgroup definition ends with a blank line. For example: hostgroup servers nameserver smtpserver nntpserver nfsserver httpserver smbserver hostgroup router_group cisco7000 agsplus }, 'Watches' => q{ Watch Group Entries Watch entries begin with a line that starts with the keyword watch, followed by whitespace and a single word which normally refers to a pre-defined hostgroup. If the second word is not recognized as a hostgroup tag, a new hostgroup is created whose tag is that word, and that word is its only member. Watch entries consist of one or more service definitions. There is a special watch group entry called "default". If a default watch group is defined with a "default" service entry, then this definition will be used in handling unknown mon traps. Service Definitions service servicename A service definition begins with they keyword service followed by a word which is the tag for this service. The components of a service are an interval, monitor, and one or more time period definitions, as defined below. If a service name of "default" is defined within a watch group called "dafault" (see above), then the default/default definition will be used for handling unknown mon traps. interval timeval The keyword interval followed by a time value specifies the frequency that a monitor script will be triggered. Time values are defined as "30s", "5m", "1h", or "1d", meaning 30 seconds, 5 minutes, 1 hour, or 1 day. The numeric portion may be a fraction, such as "1.5h" or an hour and a half. This format of a time specification will be referred to as timeval. traptimeout timeval This keyword takes the same time specification argument as interval, and makes the service expect a trap from an external source at least that often, else a failure will be registered. This is used for a heartbeat-style service. trapduration timeval If a trap is received, the status of the service the trap was delivered to will normally remain constant. If trapduration is specified, the status of the service will remain in a failure state for the duration specified by timeval, and then it will be reset to "success". randskew timeval Rather than schedule the monitor script to run at the start of each interval, randomly adjust the interval specified by the interval parameter by plus-or-minus randskew. The skew value is specified as the interval parameter: "30s", "5m", etc... For example if interval is 1m, and randskew is "5s", then mon will schedule the monitor script some time between every 55 seconds and 65 seconds. The intent is to help distribute the load on the server when many services are scheduled at the same intervals. monitor monitor-name [arg...] The keyword monitor followed by a script name and arguments specifies the monitor to run when the timer expires. Shell-like quoting conventions are followed when specifying the arguments to send to the monitor script. The script is invoked from the directory given with the -s argument, and all following words are supplied as arguments to the monitor program, followed by the list of hosts in the group referred to by the current watch group. If the monitor line ends with ";;" as a separate word, the host groups are not appended to the argument list when the program is invoked. allow_empty_group The allow_empty_group option will allow a monitor to be invoked even when the hostgroup for that watch is empty because of disabled hosts. The default behavior is not to invoke the monitor when all hosts in a hostgroup have been disabled. description descriptiontext The text following description is queried by client programs, passed to alerts and monitors via an environment variable. It should contain a brief description of the service, suitable for inclusion in an email or on a web page. exclude_hosts host [host...] Any hosts listed after exclude_hosts will be excluded from the service check. exclude_period periodspec Do not run a scheduled monitor during the time identified by periodspec. depend dependexpression The depend keyword is used to specify a dependency expression, which evaluates to either true of false, in the boolean sense. Dependencies are actual Perl expressions, and must obey all syntactical rules. The expressions are evaluated in their own package space so as to not accidentally have some unwanted side-effect. If a syntax error is found when evaluating the expression, it is logged via syslog. Before evaluation, the following substitutions on the expression occur: phrases which look like "group:service" are substituted with the value of the current operational status of that specified service. These opstatus substitutions are computed recursively, so if service A depends upon service B, and service B depends upon service C, then service A depends upon service C. Successful operational statuses (which evaluate to "1") are "STAT_OK", "STAT_COLDSTART", "STAT_WARMSTART", and "STAT_UNKNOWN". The word "SELF" (in all caps) can be used for the group (e.g. "SELF:service"), and is an abbreviation for the current watch group. This feature can be used to control alerts for services which are dependent on other services, e.g. an SMTP test which is dependent upon the machine being ping-reachable. dep_behavior {a|m} The evaluation of dependency graphs can control the suppression of either alert or monitor invocations. Alert suppression. If this option is set to "a", then the dependency expression will be evaluated after the monitor for the service exits or after a trap is received. An alert will only be sent if the evaluation succeeds, meaning that none of the nodes in the dependency graph indicate failure. Monitor suppression. If it is set to "m", then the dependency expression will be evaulated before the monitor for the service is about to run. If the evaulation succeeds, then the monitor will be run. Otherwise, the monitor will not be run and the status of the service will remain the same. Period Definitions Periods are used to define the conditions which should allow alerts to be delivered. period [label:] periodspec A period groups one or more alarms and variables which control how often an alert happens when there is a failure. The period keyword has two forms. The first takes an argument which is a period specification from Patrick Ryan's Time::Period Perl 5 module. Refer to "perldoc Time::Period" for more information. The second form requires a label followed by a period specification, as defined above. The label is a tag consisting of an alphabetic character or underscore followed by zero or more alphanumerics or underscores and ending with a colon. This form allows multiple periods with the same period definition. One use is to have a period definition which has no alertafter or alertevery parameters for a particular time period, and another for the same time period with a different set of alerts that does contain those parameters. alertevery timeval The alertevery keyword (within a period definition) takes the same type of argument as the interval variable, and limits the number of times an alert is sent when the service continues to fail. For example, if the interval is "1h", then only the alerts in the period section will only be triggered once every hour. If the alertevery keyword is omitted in a period entry, an alert will be sent out every time a failure is detected. By default, if the output of two successive failures changes, then the alertevery interval is overridden. If the word "summary" is the last argument, then only the summary output lines will be considered when comparing the output of successive failures. alertafter num alertafter num timeval The alertafter keyword (within a period section) has two forms: only with the "num" argument, or with the "num timeval" arguments. In the first form, an alert will only be invoked after "num" consecutive failures. In the second form, the arguments are a positive integer followed by an interval, as described by the interval variable above. If these parameters are specified, then the alerts for that period will only be called after that many failures happen within that interval. For example, if alertafter is given the arguments "3 30m", then the alert will be called if 3 failures happen within 30 minutes. numalerts num This variable tells the server to call no more than num alerts during a failure. The alert counter is kept on a per-period basis, and is reset upon each success. comp_alerts If this option is specified, then upalerts will only be called if a corresponding "down" alert has been called. alert alert [arg...] A period may contain multiple alerts, which are triggered upon failure of the service. An alert is specified with the alert keyword, followed by an optional exit parmeter, and arguments which are interpreted the same as the monitor definition, but without the ";;" exception. The exit parameter takes the form of exit=x or exit=x-y and has the effect that the alert is only called if the exit status of the monitor script falls within the range of the exit parameter. If, for example, the alert line is alert exit=10-20 mail.alert mis then mail-alert will only be invoked with mis as its arguments if the monitor program's exit value is between 10 and 20. This feature allows you to trigger different alerts at different severity levels (like when free disk space goes from 8% to 3%). See the ALERT PROGRAMS section above for a list of the pramaeters mon will pass automatically to alert programs. upalert alert [arg...] An upalert is the compliment of an alert. An upalert is called when a services makes the state transition from failure to success. The upalert script is called supplying the same parameters as the alert script, with the addition of the -u parameter which is simply used to let an alert script know that it is being called as an upalert. Multiple upalerts may be specified for each period definition. Please note that the default behavior is that an upalert will be sent regardless if there were any prior "down" alerts sent, since upalerts are triggered on a state transition. Set the per-period comp_alerts option to pair upalerts with "down" alerts. startupalert alert [arg...] A startupalert is only called when the mon server starts execution. upalertafter timeval The upalertafter parameter is specified as a string that follows the syntax of the interval parameter ("30s", "1m", etc.), and controls the triggering of an upalert. If a service comes back up after being down for a time greater than or equal to the value of this option, an upalert will be called. Use this option to prevent upalerts to be called because of "blips" (brief outages). }, ); #' Keep emacs perl-mode happy... my $main = new MainWindow; $Balloon = $main->Balloon; my $menubar = $main->Frame; $menubar->pack(-fill => 'x'); my $file = $menubar->Menubutton(qw/-text File -underline 0 -menuitems/ => [ [Button => '~Open', -command => [\&open_config_file]], [Button => '~Save', -command => [\&save_config_file]], [Button => 'Save ~As...', -command => [\&save_config_file_as]], [Separator => ''], [Button => '~Quit', -command => [\&exit]], ])->pack(-side => 'left'); my $help = $menubar->Menubutton(qw/-text Help -underline 0 -menuitems/ => [ [Button => '~About', -command => sub { my $A = $main->Toplevel(-title => 'About'); $A->Label( -text => "mongui.pl Version $Version\n\nCopyright (C) 2000 Daniel J. Urist\nContact: Daniel J. Urist \nPortions of this code are Copyright (C) Jim Trocki\n\nThis program is free software; you can redistribute it and/or\nmodify it under the terms of the GNU General Public License\nas published by the Free Software Foundation; either version\n2 of the License, or (at your option) any later version." )->pack; $A->Button(-text => 'Dismiss', -command => sub {$A->destroy;})->pack; }], [Button => '~Global Configs', -command => [\&Help, 'Global Configs']], [Button => '~Host Groups', -command => [\&Help, 'Host Groups']], [Button => '~Watches', -command => [\&Help, 'Watches']], ])->pack(-side => 'right'); my $buttons = $main->Frame; $buttons->pack; $main->Button(-text => 'Global Configs', -command => sub { unless($monconfigfile){ &no_config_file_selected; return; } &Edit_globals; } )->pack(-side => 'left'); $main->Button(-text => 'Host Groups', -command => sub { unless($monconfigfile){ &no_config_file_selected; return; } &Edit_hostgroups; } )->pack(-side => 'left'); $main->Button(-text => 'Watches', -command => sub { unless($monconfigfile){ &no_config_file_selected; return; } &Edit_watches; } )->pack(-side => 'left'); MainLoop; sub Help { my ($subject) = @_; $Help->destroy if Exists($Help); $Help = $main->Toplevel(-takefocus => 1, -title => $subject); my $R = $Help->Scrolled('ROText', -scrollbars =>'e')->pack; $R->insert('end', $Doc{$subject}); $Help->Button(-text => 'Dismiss', -command => sub {$Help->destroy;})->pack; } sub open_config_file { $monconfigfile = $main->getOpenFile; $savefile = $monconfigfile; if( -e $monconfigfile ){ eval{ &read_cf($monconfigfile) }; if($@){ $main->Dialog(-title => 'Error', -text => "Parse error in config file $monconfigfile: $@")->Show; undef $monconfigfile; undef $savefile; return; } &read_globals($monconfigfile); } else{ $main->Dialog(-title => 'Warning', -text => "Config File $monconfigfile Does Not Exist")->Show; } } sub save_config_file { unless($monconfigfile){ &no_config_file_selected; return; } eval{ &write_cf($savefile) }; $main->Dialog(-title => 'Error', -text => "Could not write config file $savefile: $@")->Show if $@; } sub save_config_file_as { unless($monconfigfile){ &no_config_file_selected; return; } $savefile = $main->getSaveFile; &save_config_file($savefile); } sub no_config_file_selected { my $errmsg = $main->Dialog(-title => 'Error', -text => "No config file selected!")->Show; } sub implement_me { my $squawk = $main->Dialog(-text => "Implement me, Dan!"); $squawk->Show; } ############################################################ # Globals ############################################################ # # Read all the globals defined in mon.cf into %globals # sub read_globals { # Set some default values $globals{dtlogging} = "no"; $globals{serverport} = 2583; $globals{trapport} = 2583; $globals{serverbind} = "IN_ADDR_ANY"; # FIXME should we require POSIX or does mon want these as strings? $globals{trapbind} = "IN_ADDR_ANY"; # $globals{dep_recur_limit} = 10; $globals{dep_behavior} = "m"; # FIXME what is the real default for this? my($CF) = @_; open(CF, $CF) or die "Could not open $CF for reading\n"; my @vars = grep(!/^\s|^#|^hostgroup|^watch/, ); close CF; chomp(@vars); map { $_ =~ /^(\S+)\s*=\s*(\S+)/; $globals{$1} = $2; } @vars; # Postprocessing for consistency $globals{randstart} =~ s/s$//; } # # Create the "Edit Globals" window # sub Edit_globals { $Edit_globals->destroy if Exists($Edit_globals); $Edit_globals = $main->Toplevel(-takefocus => 1, -title => 'Global Configs'); my $F = $Edit_globals->Frame->pack; my %g = %globals; my $category; foreach $category (@{$GLOBALS{ORDER}}) { $F->Button( -text => $category, -command => [\&Edit_global_submenu, \%g, $category] )->pack(-fill => 'x'); } my $B = $Edit_globals->Frame->pack; $B->Button(-text => "Okay", -command => sub { %globals = %g; $Edit_globals->destroy; })->pack(-side => "left"); $B->Button(-text => "Cancel", -command => sub {$Edit_globals->destroy;})->pack(-side => "right"); } # # Create a submenu for global variables # sub Edit_global_submenu { my($g, $category) = @_; $Edit_global_submenu->destroy if Exists($Edit_global_submenu); $Edit_global_submenu = $Edit_globals->Toplevel(-takefocus => 1, -title => "Edit $category Configs"); my $S = $Edit_global_submenu->Frame; $S->pack; my %widgets = (); my $var; foreach $var ( @{$GLOBALS{$category}->{ORDER}} ){ $S->Label(-text => $var)->pack; $_ = &{$GLOBALS{$category}->{$var}->{widget}}($S, $g->{$var}, $g, $GLOBALS{$category}->{$var}->{bmsg}, \%widgets); $_->pack; $widgets{$var} = $_; } my $A = $Edit_global_submenu->Frame(-borderwidth => 10)->pack;; $A->Button(-text => "Okay", -command => sub { # This is a kludge to deal with inconsistencies in widgets; # some widgets support the "get" method, some are just bound # to variables (we are only dealing with single-values here). # eval traps the errors if the get() method doesn't exist foreach $_ ( keys %widgets ){ eval { $g->{$_} = $widgets{$_}->get; }; # validate &{$GLOBALS{$category}->{$_}->{validate}}($Edit_global_submenu, $g->{$_}); } $Edit_global_submenu->destroy; }, )->pack(-side => 'left'); $A->Button(-text => "Cancel", -command => [$Edit_global_submenu => 'destroy'] )->pack(-side => 'right'); } ############################################################ # HOSTGROUPS ############################################################ sub Edit_hostgroups { $Edit_hostgroups->destroy if Exists($Edit_hostgroups); $Edit_hostgroups = $main->Toplevel(-takefocus => 1, -title => 'Edit Host Groups'); my $F = $Edit_hostgroups->Frame->pack; my $H = $F->Scrolled('Pane', -scrollbars => 'e', -gridded => 'y')->pack; my $i = 0; foreach ( sort keys %groups ){ my $hg_name = $H->Entry(-width => 15,)->grid(-row => $i, -column => 0); $hg_name->insert(0, $_); $Balloon->attach($hg_name, -balloonmsg => 'Unique hostgroup name'); my $hg_list = $H->Entry(-width => 50,)->grid(-row => $i, -column => 1); $hg_list->insert(0, join(" ", @{$groups{$_}})); $Balloon->attach($hg_list, -balloonmsg => 'List of hosts'); $i++; } my $F2 = $F->Frame->pack; $F2->Button(-text => 'Add Hostgroup', -command => sub { &Edit_hostgroups_add($H); })->pack(-side => 'left'); $F2->Button(-text => 'Delete Hostgroup', -command => sub { my $widget = $Edit_hostgroups->focusCurrent; return unless Exists($widget); my $parentpath = $widget->parent->PathName; my $H_subwidgetpath = $H->Subwidget('scrolled')->PathName; # There seems to be an additional frame if( $parentpath =~ /^$H_subwidgetpath/ ){ %_ = $widget->gridInfo; my($hg_list, $hg_name)= $H->Subwidget('scrolled')->gridSlaves('-row' => $_{-row}); $hg_name->gridForget; $hg_name->destroy; $hg_list->gridForget; $hg_list->destroy; } })->pack(-side => 'right'); my $B = $Edit_hostgroups->Frame->pack; $B->Button(-text => "Okay", -command => sub { my($hg_list, $hg_name); %groups = (); @_ = $H->Subwidget('scrolled')->gridSlaves; while($hg_list = shift and $hg_name = shift){ next unless $hg_list->Exists and $hg_name->Exists; @{$groups{$hg_name->get}} = split(' ', $hg_list->get); } $Edit_hostgroups->destroy; })->pack(-side => "left"); $B->Button(-text => "Cancel", -command => sub {$Edit_hostgroups->destroy;})->pack(-side => "right"); } sub Edit_hostgroups_add { $Edit_hostgroups_add->destroy if Exists($Edit_hostgroups_add); $Edit_hostgroups_add = $Edit_hostgroups->Toplevel(-takefocus => 1, -title => 'Add Host Group'); my($H) = @_; $Edit_hostgroups_add->Label(-text => "Host Group Name")->pack; my $name = $Edit_hostgroups_add->Entry(-width => 15)->pack; $Balloon->attach($name, -balloonmsg => 'Unique hostgroup name'); $Edit_hostgroups_add->Label(-text => "Hosts")->pack; my $hosts = $Edit_hostgroups_add->Entry(-width => 50)->pack;; $Balloon->attach($hosts, -balloonmsg => 'List of hosts'); my $B = $Edit_hostgroups_add->Frame->pack; $B->Button(-text => "Okay", -command => sub { my @hgs = map{$_->get;} $H->Subwidget('scrolled')->gridSlaves(-column => 0); my $hg_name = $name->get; if ($hg_name !~ /\S/){ $Edit_hostgroups_add->Dialog(-title => 'Warning', -text => "Invalid Host Group Name!")->Show; return; } if( grep(/^$hg_name$/, @hgs) ) { $Edit_hostgroups_add->Dialog(-title => 'Warning', -text => "Duplicate Host Group Name!")->Show; return; } # FIXME we check here, but we don't check when we edit in place my @bogushosts = grep { $_ !~ /\d+\.\d+\.\d+\.\d+/ and !defined gethostbyname($_); } split(' ', $hosts->get); $Edit_hostgroups_add->Dialog(-title => 'Warning', -text => "Can not resolve hosts " . join("; ", @bogushosts))->Show if scalar @bogushosts; my $hg_name_entry = $H->Entry(-width => 15,)->grid(-column => 0, -row => scalar(@hgs) + 1); $hg_name_entry->insert(0,$hg_name); my $hg_list = $H->Entry(-width => 50,)->grid(-column => 1, -row => scalar(@hgs) + 1); $hg_list->insert(0, join(" ", $hosts->get)); $Edit_hostgroups_add->destroy; })->pack(-side => "left"); $B->Button(-text => "Cancel", -command => sub {$Edit_hostgroups_add->destroy;})->pack(-side => "right"); } ############################################################ # Watches ############################################################ sub Edit_watches { $Edit_watches->destroy if Exists($Edit_watches); $Edit_watches = $main->Toplevel(-takefocus => 1, -title => 'Edit Watches'); my %w = %watch; # Dup the watches hash so we can cancel my $H_holder; my $H = $Edit_watches->Scrolled ( 'HList', -scrollbars => 'se', -itemtype => 'text', -separator => '.', -selectmode => 'single', -height => 25, -command => sub { $_ = $$H_holder->info('anchor'); if($_ =~ /([^.]+)\.([^.]+)/){ &Edit_watches_edit($$H_holder, \%w, $1, $2 ); } }, ); $Balloon->attach($H, -balloonmsg => 'Double-click to edit watch'); $H_holder = \$H; $H->pack; my $w; foreach $w (sort keys %w ) { $H->add($w, -text => $w); map { $H->add($w . "." . $_, -text => $_); } sort(keys %{$w{$w}}); } my $WB = $Edit_watches->Frame->pack; $WB->Button(-text => "Add\nService/Watch", -command => sub { my $hg = ""; my $s = ""; &Edit_watches_edit($H, \%w, $hg, $s); })->pack(-side => "left"); $WB->Button(-text => "Delete\nService/Watch", -command => sub { $_ = $H->info('anchor'); $H->delete('entry', $_); if($_ =~ /([^.]+)\.([^.]+)/){ delete $w{$1}->{$2}; } else{ delete $w{$_}; } })->pack(-side => "left"); my $B = $Edit_watches->Frame->pack; $B->Button(-text => "Okay", -command => sub { %watch = %w; $Edit_watches->destroy; })->pack(-side => "left"); $B->Button(-text => "Cancel", -command => sub {$Edit_watches->destroy;})->pack(-side => "right"); } sub Edit_watches_edit { my ($Parent_H, $w, $hg, $s) = @_; my $w_hash = $w->{$hg}->{$s}; $Edit_watches_edit->destroy if Exists($Edit_watches_edit); $Edit_watches_edit = $Edit_watches->Toplevel(-takefocus => 1, -title => 'Edit Watch'); my $F = $Edit_watches_edit->Frame->pack; # host group $F->Label(-text => "Host Group")->pack; my $Hostgroup = $F->BrowseEntry( -width => 20, -state => 'readonly', -variable => \$hg, )->pack; map { $Hostgroup->insert("end", $_); } sort(keys %groups); # service $F->Label(-text => "Service")->pack; my $Service = $F->Entry(-width => 20, -textvariable => \$s)->pack; # description $F->Label(-text => "Description")->pack; my $Description = $F->Entry(-width => 50)->pack; $Description->insert( 0, $w_hash->{description} ) if $hg; # interval $F->Label(-text => "Interval")->pack; my $Interval= $F->Scale(-from => 0, -to => 60000, -orient => 'horizontal', -tickinterval => 20000, -length => 200, -variable => \$w_hash->{interval} )->pack; $Balloon->attach($Interval, -balloonmsg => 'Time in seconds between monitor runs'); # monitor opendir(MOND, $globals{mondir}) or $Edit_watches->Dialog(-title => 'Warning', -text => "Can not open mon directory " . $globals{mondir})->Show; my @choices = sort( grep(/\.monitor$/, readdir(MOND) ) ); closedir MOND; $F->Label(-text => "Monitor")->pack; my $Monitor = $F->BrowseEntry( -width => 50, -choices => \@choices, -variable => \$w_hash->{monitor} )->pack; $Balloon->attach($Monitor, -balloonmsg => 'Monitor script with arguments'); # depend $F->Label(-text => "Dependencies")->pack; $F->Scrolled('Entry', -width => 50, -textvariable => \$w_hash->{depend})->pack; my $Depend_H = $F->Scrolled('HList', -scrollbars => "e", -selectmode => 'single', -width => 30, -command => sub { $w_hash->{depend} = join(" ", $w_hash->{depend}, $_[0]) unless $w_hash->{depend} =~ /\b$_[0]\b/; }, )->pack; $Balloon->attach($Depend_H, -balloonmsg => 'Double-click to add dependencies'); my ($x, $y); foreach $x ( sort keys %watch ){ foreach $y ( sort keys %{$watch{$x}} ){ $_ = $x . ":" . $y; $Depend_H->add($_, -text => $_); } } # periods $F->Label(-text => "Periods")->pack; my $Periods_H; $Periods_H = $F->Scrolled('HList', -scrollbars => "e", -selectmode => 'single', -width => 30, -command => [\&Edit_watches_edit_period, \$Periods_H, $hg, $s], )->pack; $Balloon->attach($Periods_H, -balloonmsg => 'Double-click to edit period'); foreach ( sort keys %{$w_hash->{periods}} ){ $Periods_H->add($_, -text => $_); } my $PB = $F->Frame->pack; $PB->Button(-text => "Add Period", -command => [\&Edit_watches_edit_period, \$Periods_H, $hg, $s])->pack(-side => 'left'); $PB->Button(-text => "Delete Period", -command => sub { if( $_ = $Periods_H->info('anchor') ){ delete $w_hash->{periods}->{$_}; $Periods_H->delete('entry',$_); } })->pack(-side => 'right'); my $B = $F->Frame->pack; $B->Button(-text => "Okay", -command => sub { $w->{$hg}->{$s} = $w_hash; $Parent_H->add("$hg", -text => $hg) unless $Parent_H->info('exists', "$hg"); $Parent_H->add("$hg.$s", -text => $s) unless $Parent_H->info('exists', "$hg.$s"); $Edit_watches_edit->destroy; })->pack(-side => 'left'); $B->Button(-text => "Cancel", -command => sub {$Edit_watches_edit->destroy;} )->pack(-side => 'right'); } # If we change a hg above or a period here, we treat that as an implicit add # and don't delete the old one sub Edit_watches_edit_period { my($Periods_H, $hg, $s, $p) = @_; $Edit_watches_edit_period->destroy if Exists($Edit_watches_edit_period); $Edit_watches_edit_period = $Edit_watches->Toplevel(-takefocus => 1, -title => "Edit Period for Hostgroup $hg, Service $s"); my $F = $Edit_watches_edit_period->Frame->pack; # period $p = "" unless $p; my $p_orig = $p; $F->Label(-text => "Period")->pack; my $Period = $F->Entry(-width => 50, -textvariable => \$p)->pack; $Balloon->attach($Period, -balloonmsg => 'Time period for alerts in perl Time::Period format'); my $F2 = $F->Frame->pack; # We need to dupe the subhash here so we can do a "cancel" or "okay" my %period_hash = $p ? %{$watch{$hg}->{$s}->{periods}->{$p}} : (); my $ph = \%period_hash; # numalerts # FIXME should be scale? $ph->{numalerts} = 0 unless $ph->{numalerts}; # FIXME is this a good default: $F2->Label(-text => "Num Alerts")->pack(-side => 'left'); my $numalerts = $F2->Entry(-width => 3, -textvariable => \$ph->{numalerts})->pack(-side => 'left'); $Balloon->attach($numalerts, -balloonmsg => 'Call no more than this number of alerts during a failure'); # compalerts $ph->{comp_alerts} = 0 unless $ph->{comp_alerts}; $F2->Label(-text => "Comp Alerts")->pack(-side => 'left'); my $compalerts = $F2->Checkbutton(-variable => \$ph->{compalerts})->pack(-side => 'left'); $Balloon->attach($compalerts, -balloonmsg => 'Upalerts will only be called if a corresponding alert has been called'); # alertevery $ph->{alertevery} = "" unless $ph->{alertevery}; $F->Label(-text => "Alertevery")->pack; my $alertevery = $F->Entry(-width => 10, -textvariable => \$ph->{alertevery})->pack; $Balloon->attach($alertevery, -balloonmsg => 'Alert once per this time period during a failure'); # alertafter $ph->{alertafter} = "" unless $ph->{alertafter}; $F->Label(-text => "Alertafter")->pack; my $alertafter = $F->Entry(-width => 10, -textvariable => \$ph->{alertafter})->pack; $Balloon->attach($alertafter, -balloonmsg => 'Alert after this many failures or this many failures per time period'); # alerts $F->Label(-text => "Alerts")->pack; my $A = $F->Scrolled('Pane', -scrollbars => 'e', -gridded => 'y')->pack; # It would be nice to have this sunken, $Balloon->attach($A, -balloonmsg => 'Alert scripts with arguments'); my $i = 0; # but (relief => sunken) doesn't seem to work foreach ( @{$ph->{alerts}}){ next unless defined $_; $A->Entry(-textvariable => \$_, -width => 50,)->grid(-row => $i); $i++; } my $F3 = $F->Frame->pack; $F3->Button(-text => 'Add Alert', -command => [\&Edit_watches_edit_period_add_alert, $ph, $A, 'alerts'])->pack(-side => 'left'); $F3->Button(-text => 'Delete Alert', -command => sub { my $alert_w = $Edit_watches_edit_period->focusCurrent; return unless Exists($alert_w); my $parentpath = $alert_w->parent->PathName; my $A_subwidgetpath = $A->Subwidget('scrolled')->PathName; # There seems to be an additional frame if( $parentpath =~ /^$A_subwidgetpath/ ){ $_ = $alert_w->cget('-textvariable'); undef $$_; # Undef the variable; note that this does NOT delete it from the array $alert_w->destroy; } })->pack(-side => 'right'); # upalerts $F->Label(-text => "Upalerts")->pack; my $U = $F->Scrolled('Pane', -scrollbars => 'e', -gridded => 'y')->pack; $Balloon->attach($U, -balloonmsg => 'Alert scripts with arguments'); $i = 0; foreach ( @{$ph->{upalerts}}){ next unless defined $_; $U->Entry(-textvariable => \$_, -width => 50)->grid(-row => $i); $i++; } my $F4 = $F->Frame->pack; $F4->Button(-text => 'Add Upalert', -command => [\&Edit_watches_edit_period_add_alert, $ph, $U, 'upalerts'])->pack(-side => 'left'); $F4->Button(-text => 'Delete Upalert', -command => sub { my $upalert_w = $Edit_watches_edit_period->focusCurrent; return unless Exists($upalert_w); my $parentpath = $upalert_w->parent->PathName; my $U_subwidgetpath = $U->Subwidget('scrolled')->PathName; # There seems to be an additional frame if( $parentpath =~ /^$U_subwidgetpath/ ){ $_ = $upalert_w->cget('-textvariable'); undef $$_; # Undef the variable; note that this does NOT delete it from the array $upalert_w->destroy; } })->pack(-side => 'right'); # okay and cancel buttons my $B = $Edit_watches_edit_period->Frame->pack; $B->Button(-text => 'Okay', -command => sub{ delete $watch{$hg}->{$s}->{periods}->{$p}; $watch{$hg}->{$s}->{periods}->{$p} = $ph; $$Periods_H->delete('entry', $p_orig) if $$Periods_H->info('exists', $p_orig); # FIXME this inserts it at the end, and we want the same position! $$Periods_H->add($p, -text => $p); # BUT there seems to be no way to get that from HList :( $Edit_watches_edit_period->destroy; }, )->pack(-side => 'left'); $B->Button(-text => 'Cancel', -command => sub{$Edit_watches_edit_period->destroy;} )->pack(-side => 'right'); } sub Edit_watches_edit_period_add_alert { my($ph, $pane, $type) = @_; $Edit_watches_edit_period_add_alert->destroy if Exists($Edit_watches_edit_period_add_alert); $Edit_watches_edit_period_add_alert = $Edit_watches_edit_period->Toplevel(-takefocus => 1, -title => "Add Alert"); # Get the list of alert scripts for use in alerts/upalerts opendir(ALERTD, $globals{alertdir}) or $Edit_watches_edit_period_add_alert->Dialog(-title => 'Warning', -text => "Could not open directory " . $globals{alertdir})->Show; my @alertscripts = sort( grep(/\.alert$/, readdir(ALERTD)) ); closedir ALERTD; my $var = ""; $Edit_watches_edit_period_add_alert->BrowseEntry(-variable => \$var, -width => 50, -choices => \@alertscripts)->pack; $Edit_watches_edit_period_add_alert->Button(-text => 'Cancel', -command => sub {$Edit_watches_edit_period_add_alert->destroy;})->pack(-side => 'left'); $Edit_watches_edit_period_add_alert->Button( -text => 'Okay', -command => sub { # Add the alert/upalert to our hash $pane->Entry(-textvariable =>\$var, -width => 50)->grid(-row => scalar @{$ph->{$type}}); push @{$ph->{$type}}, $var; $Edit_watches_edit_period_add_alert->destroy; } )->pack(-side => 'left'); } # ======================================================== # ======================================================== # Write the config file sub write_cf { my ($CF) = @_; my %globals = %globals; # Make a local copy so we can do some kludgery $globals{randstart} .= "s"; # FIXME should flock this open(CF, ">$CF") or die "Could not open $CF for writing"; # Write out the global vars print CF "#####################################\n"; print CF "# Global Options\n#\n"; my $k; foreach $k ( sort keys %globals ){ printf CF ("%-10s %s %s\n", $k, "=", $globals{$k}); } print CF "\n\n"; # Write out the hostgroups print CF "#####################################\n"; print CF "# Host Groups\n#\n"; foreach $k ( sort keys %groups ){ print CF join(" ", "hostgroup", $k, @{$groups{$k}}), "\n"; } # Write out the watch definitions # This gets a little more complicated my($w, $s, $s_var, $p, $p_var, @a_types, $a_t, $a); foreach $w ( sort keys %watch ){ print CF "\n#####################################\n"; print CF "watch $w\n"; foreach $s ( sort keys %{$watch{$w}} ){ print CF "\t", "service ", $s, "\n"; foreach $s_var ( sort keys %{$watch{$w}{$s}} ){ next if $s_var =~ /^_/ or ref $watch{$w}{$s}{$s_var} or $s_var eq "service"; # KLUDGEY -- fix inconsistent interval variables next if $s_var eq "randskew" and $watch{$w}{$s}{$s_var} eq "0"; $watch{$w}{$s}{$s_var} .= "s" if $s_var =~ /^interval|randskew$/; print CF "\t\t", $s_var, " ", $watch{$w}{$s}{$s_var}, "\n"; } # Now we do the periods foreach $p (sort keys %{$watch{$w}{$s}{periods}}){ @a_types = (); print CF "\t\t", "period", " ", $p, "\n"; foreach $p_var ( sort keys %{$watch{$w}{$s}{periods}{$p}} ){ next if $p_var =~ /^_/ or $p_var eq "period"; # KLUDGEY -- fix inconsistent period variables next if $p_var eq "alertevery" and $watch{$w}{$s}{periods}{$p}{$p_var} eq "0"; if( ref $watch{$w}{$s}{periods}{$p}{$p_var} ){ push @a_types, $p_var; next; } print CF "\t\t\t", $p_var, " ", $watch{$w}{$s}{periods}{$p}{$p_var}, "\n"; } foreach $a_t (sort @a_types){ foreach $a ( @{$watch{$w}{$s}{periods}{$p}{$a_t}} ){ $a_t =~ s/s$//; # strip plural print CF "\t\t\t", $a_t, " ", $a, "\n"; } } } } } close CF; } # ======================================================== # ======================================================== # Ripped from mon itself # # parse configuration file # # build the following data structures: # # %group # each element of %group is an array of hostnames # group records are terminated by a blank line in the # configuration file # %watch{"group"}->{"service"}->{"variable"} = value # sub read_cf { # KLUDGE: make "strict" shut up my(%opt, %alias, $PWD, $STAT_UNTESTED); my ($CF) = @_; my ($var, $watchgroup, $ingroup, $curgroup, $inwatch, $servnum, $args, $hosts, %disabled, $h, $i, $aliasReading, $aliasGroup); my ($sref, $pref); my ($service, $period); # # parse configuration file # if ($opt{"M"} || $CF =~ /\.m4$/) { open (CFG, "m4 $CF |") || die "could not open m4 pipe of cf file: $CF: $!\n"; } else { open (CFG, $CF) || die "could not open cf file: $CF: $!\n"; } $servnum = 0; %alias = (); my $DEP_BEHAVIOR = "a"; my $incomplete_line = 0; my $linepart = ""; my $l = ""; my $acc_line = ""; for (;;) { last if (!defined ($linepart = )); next if $linepart =~ /^\s*#/; # # accumulate multi-line lines (ones which are \-escaped) # if ($incomplete_line) { $linepart =~ s/^\s*//; } if ($linepart =~ /^(.*)\\\s*$/) { $incomplete_line = 1; $acc_line .= $1; chomp $acc_line; next; } else { $acc_line .= $linepart; } $l = $acc_line; $acc_line = ""; chomp $l; $l =~ s/^\s*//; $l =~ s/\s*$//; $incomplete_line = 0; $linepart = ""; # # variables than can be overriden by the command line # if ($l =~ /^(\w+) \s* = \s* (.*) \s*$/ix) { if ($1 eq "alertdir") { $CF{"ALERTDIR"} = $2; next; } elsif ($1 eq "basedir") { $CF{"BASEDIR"} = $2; $CF{"BASEDIR"} = "$PWD/$CF{BASEDIR}" if ($CF{"BASEDIR"} !~ m{^/}); $CF{"BASEDIR"} =~ s{/$}{}; next; } elsif ($1 eq "cfbasedir") { $CF{"CFBASEDIR"} = $2; $CF{"CFBASEDIR"} = "$PWD/$CF{CFBASEDIR}" if ($CF{"CFBASEDIR"} !~ m{^/}); $CF{"CFBASEDIR"} =~ s{/$}{}; next; } elsif ($1 eq "mondir") { $CF{"SCRIPTDIR"} = $2; next; } elsif ($1 eq "logdir") { $CF{"LOGDIR"} = $2; next; } elsif ($1 eq "histlength") { $CF{"MAX_KEEP"} = $2; next; } elsif ($1 eq "serverport") { $CF{"SERVPORT"} = $2; next; } elsif ($1 eq "trapport") { $CF{"TRAPPORT"} = $2; next; } elsif ($1 eq "serverbind") { $CF{"SERVERBIND"} = $2; next; } elsif ($1 eq "trapbind") { $CF{"TRAPBIND"} = $2; next; } elsif ($1 eq "pidfile") { $CF{"PIDFILE"} = $2; next; } elsif ($1 eq "randstart") { $CF{"RANDSTART"} = dhmstos($2); die "cf error: bad syntax, line $.\n" if (!defined ($CF{"RANDSTART"})); next; } elsif ($1 eq "maxprocs") { $CF{"MAXPROCS"} = $2; next; } elsif ($1 eq "statedir") { $CF{"STATEDIR"} = $2; next; } elsif ($1 eq "authfile") { $CF{"AUTHFILE"} = $2; next; } elsif ($1 eq "authtype") { $CF{"AUTHTYPE"} = $2; next; } elsif ($1 eq "userfile") { $CF{"USERFILE"} = $2; next; } elsif ($1 eq "ocfile") { $CF{"OCFILE"} = $2; next; } elsif ($1 eq "historicfile") { $CF{"HISTORICFILE"} = $2; next; } elsif ($1 eq "historictime") { $CF{"HISTORICTIME"} = dhmstos($2); die "cf error: bad syntax, line $.\n" if (!defined $CF{"HISTORICTIME"}); next; } elsif ($1 eq "cltimeout") { $CF{"CLIENT_TIMEOUT"} = dhmstos($2); die "cf error: bad syntax, line $.\n" if (!defined ($CF{"CLIENT_TIMEOUT"})); next; } elsif ($1 eq "use snmp") { $CF{"SNMP"} = 1; eval "use SNMP"; die "could not use SNMP: $@\n" if ($@ ne ""); next; } elsif ($1 eq "dtlogfile") { $CF{"DTLOGFILE"} = $2; next; } elsif ($1 eq "dtlogging") { $CF{"DTLOGGING"} = 0; if ($2 == 1 || $2 eq "yes" || $2 eq "true") { $CF{"DTLOGGING"} = 1; } next; } elsif ($1 eq "snmpport") { $CF{"SNMPPORT"} = $2; next; } elsif ($1 eq "dtlogfile") { $CF{"DTLOGFILE"} = $2; next; } elsif ($1 eq "dep_recur_limit") { $CF{"DEP_RECUR_LIMIT"} = $2; next; } elsif ($1 eq "dep_behavior") { if ($2 ne "m" && $2 ne "a") { die "cf error: unknown dependency behavior, line $.\n"; } $DEP_BEHAVIOR = $2; next; } else { die "cf error: unknown variable, line $.\n"; } } # # end of record # if ($l eq "") { $ingroup = 0; $curgroup = ""; $inwatch = 0; $watchgroup = ""; $servnum = 0; $period = 0; undef $aliasReading; next; } # # group record # if ($l =~ /^hostgroup\s+([a-zA-Z0-9_.-]+)\s*(.*)/) { $curgroup = $1; $hosts = $2; %disabled = (); foreach $h (grep (/^\*/, @{$groups{$curgroup}})) { $h =~ s/^\*//; $disabled{$h} = 1; } @{$groups{$curgroup}} = split(/\s+/, $hosts); # # keep hosts which were previously disabled # for ($i=0;$i<@{$groups{$curgroup}};$i++) { $groups{$curgroup}[$i] = "*$groups{$curgroup}[$i]" if ($disabled{$groups{$curgroup}[$i]}); } $ingroup = 1; next; } elsif ($ingroup) { push (@{$groups{$curgroup}}, split(/\s+/, $l)); for ($i=0;$i<@{$groups{$curgroup}};$i++) { $groups{$curgroup}[$i] = "*$groups{$curgroup}[$i]" if ($disabled{$groups{$curgroup}[$i]}); } next; } # # alias record # if ($l =~ /^alias\s+([a-zA-Z0-9_.-]+)\s*$/) { $aliasReading = 1; $aliasGroup = $1; next; } elsif ($aliasReading) { if ($l =~ /\A(.*)\Z/) { push (@{$alias{$aliasGroup}}, $1); next; } } # # watch record # if ($l =~ /^watch\s+([a-zA-Z0-9_.-]+)\s*/) { $watchgroup = $1; if (!defined ($groups{$watchgroup})) { @{$groups{$watchgroup}} = ($watchgroup); } die "cf error: watch already defined, line $.\n" if ($watch{$watchgroup}); $ingroup = 0; $curgroup = ""; $service = ""; $period = 0; $inwatch = 1; next; } # # from here on we are in a watch record # next if (!$inwatch); # # env variables # if ($l =~ /^([A-Z_][A-Z0-9_]*)=(.*)/) { die "cf error: environment variable defined without a service, line $.\n" if ($service eq ""); $watch{$watchgroup}->{$service}->{"ENV"}->{$1} = $2; next; } # # non-env variables # else { $l =~ /^(\w+)\s*(.*)$/; $var = $1; $args = $2; } # # service entry # if ($var eq "service") { $service = $args; die "cf error: invalid service tag, line $.\n" if ($service !~ /^[a-zA-Z0-9_.-]+$/); $period = 0; $sref = \%{$watch{$watchgroup}->{$service}}; $sref->{"service"} = $args; $sref->{"interval"} = undef; $sref->{"randskew"} = 0; $sref->{"dep_behavior"} = $DEP_BEHAVIOR; $sref->{"_op_status"} = $STAT_UNTESTED; $sref->{"_last_op_status"} = $STAT_UNTESTED; $sref->{"_ack"} = 0; $sref->{"_ack_comment"} = ''; $sref->{"_consec_failures"} = 0; $sref->{"_failure_count"} = 0 if (!defined($sref->{"_failure_count"})); $sref->{"_start_of_monitor"} = time if (!defined($sref->{"_start_of_monitor"})); $sref->{"_alert_count"} = 0 if (!defined($sref->{"_alert_count"})); $sref->{"_last_failure"} = 0 if (!defined($sref->{"_last_failure"})); $sref->{"_last_success"} = 0 if (!defined($sref->{"_last_success"})); $sref->{"_last_trap"} = 0 if (!defined($sref->{"_last_trap"})); $sref->{"_exitval"} = "undef" if (!defined($sref->{"_exitval"})); $sref->{"_last_check"} = undef; $sref->{"_depend_status"} = undef; next; } if ($service eq "") { die "cf error: need to specify service in watch record, line $.\n"; } # # period definition # if ($var eq "period") { $period = 1; my $periodstr; if ($args =~ /^([a-z_]\w*) \s* : \s* (.*)$/ix) { $periodstr = $1; $args = $2; } else { $periodstr = $args; } $pref = \%{$sref->{"periods"}->{$periodstr}}; if (inPeriod (time, $args) == -1) { die "cf error: malformed period, line $.\n"; } $pref->{"period"} = $args; $pref->{"alertevery"} = 0; $pref->{"numalerts"} = 0; $pref->{"_alert_sent"} = 0; @{$pref->{"alerts"}} = (); @{$pref->{"upalerts"}} = (); @{$pref->{"startupalerts"}} = (); next; } # # alert # if ($var eq "alert" && !$period) { die "cf error: need to specify a period for alert, line $.\n"; } elsif ($var eq "upalert" && !$period) { die "cf error: need to specify a period for upalert, line $.\n"; } elsif ($var eq "alertevery" && !$period) { die "cf error: need to specify a period for alertevery, line $.\n"; } elsif ($var eq "alertafter" && !$period) { die "cf error: need to specify a period for alertafter, line $.\n"; } # # for each service there can be one or more alert periods # this is stored as an array of hashes named # %{$watch{$watchgroup}->{$service}->{"periods"}} # each index for this hash is something like "wd {Mon-Fri} hr {7am-11pm}" # the value of the hash is an array containing the list of alert commands # and arguments # if ($var eq "alert") { push @{$pref->{"alerts"}}, $args; } elsif ($var eq "upalert") { $sref->{"_upalert"} = 1; push @{$pref->{"upalerts"}}, $args; } elsif ($var eq "startupalert") { push @{$pref->{"startupalerts"}}, $args; } # # non-alert variables # else { if ($var eq "interval") { $args = dhmstos ($args) || die "cf error: invalid time interval, line $.\n"; } elsif ($var eq "traptimeout") { $args = dhmstos ($args) || die "cf error: invalid waitfortrap interval, line $.\n"; $sref->{"_trap_timer"} = $args; } elsif ($var eq "trapduration") { $args = dhmstos ($args) || die "cf error: invalid trapduration interval, line $.\n"; } elsif ($var eq "randskew") { $args = dhmstos ($args) || die "cf error: invalid random skew time, line $.\n"; } elsif ($var eq "alertevery") { my $summary_flag; if ($args =~ /(\S+)(\s+)summary(\s*)$/i) { $summary_flag = 1; $args = $1; } else { $summary_flag = 0; } $args = dhmstos ($args) || die "cf error: invalid time interval, line $.\n"; $pref->{"alertevery"} = $args; $pref->{"_alertsum"} = $summary_flag; next; } elsif ($var eq "alertafter") { my ($p1, $p2); if ($args =~ /^(\d+)$/) { $p1 = $1; $pref->{"alertafter_consec"} = $p1; } elsif ($args =~ /(\d+)\s+(\d+[hms])$/) { ($p1, $p2) = ($1, $2); if (($p1 - 1) * $sref->{"interval"} >= dhmstos($p2)) { die "cf error: interval & alertafter not sensible.\n" . "No alerts can be generated with those parameters, line $.\n"; } $pref->{"alertafter"} = $p1; $pref->{"alertafterival"} = dhmstos ($p2); $pref->{"_1stfailtime"} = 0; $pref->{"_failcount"} = 0; } else { die "cf error: invalid interval specification, line $.\n"; } } elsif ($var eq "upalertafter") { $args = dhmstos ($args) || die "cf error: invalid upalertafter specification, line $.\n"; } elsif ($var eq "numalerts") { die "cf error: non-numeric arg, line $.\n" if ($args !~ /^\d+$/); $pref->{"numalerts"} = $args; next; } elsif ($var eq "comp_alerts") { $pref->{"comp_alerts"} = 1; next; } elsif ($var eq "dep_behavior") { if ($args ne "m" && $args ne "a") { die "cf error: unknown dependency behavior, line $.\n"; } } $sref->{$var} = $args; } next; } close (CFG); 1; } # # convert a string like "20m" into seconds # sub dhmstos { my ($str) = @_; my ($s); if ($str =~ /^\s*(\d+(?:\.\d+)?)([dhms])\s*$/i) { if ($2 eq "m") { $s = $1 * 60; } elsif ($2 eq "h") { $s = $1 * 60 * 60; } elsif ($2 eq "d") { $s = $1 * 60 * 60 * 24; } else { $s = $1; } } else { return undef; } $s; } .