NAME

Monitoring::Sneck::Config - parses and validates sneck config files

VERSION

Version 1.7.0

SYNOPSIS

use Monitoring::Sneck::Config;

my $config;
eval { $config = Monitoring::Sneck::Config->new( { file => '/usr/local/etc/sneck.conf' } ); };
if ($@) {
    die($@);
}

if ( !$config->is_valid ) {
    foreach my $error ( $config->errors ) {
        print $error->{where} . ': ' . $error->{message} . "\n";
    }
}

foreach my $name ( sort keys %{ $config->checks } ) {
    print $name . ' runs ' . $config->substitute( $config->checks->{$name} ) . "\n";
}

CONFIG FORMAT

Two formats are supported, the sneck format and YAML. Files ending in .yaml or .yml are read as YAML, anything else as the sneck format. This can be overridden via the format arg to new.

The following applies to both.

Names of options, variables, environment variables, checks, debug checks, compliance checks, and restarts are made up of A-Z, a-z, 0-9, and _.

Variables are used in commands in the form /%+variable_name%+/. A reference to a variable that is not defined is left as written and produces a warning, as it may just be part of the command, such as 'date +%Y%m%d'.

Debug checks are the same as checks, but are not counted towards any of the counts. They exist purely for debugging. A check and a debug check may share a name.

Compliance checks are checks for how a system is set up, such as sshd settings or file modes, rather than if something is working. They are counted separately from checks and do not set alert by default. A compliance check may share a name with a check or debug check. See COMPLIANCE.

Commands may not be empty.

Checks, debug checks, and compliance checks starting with perl: are perl checks, run in process. See PERL CHECKS.

Environment variables are only set if the whole config is valid.

SNECK FORMAT

Each line has leading spaces and tabs removed before it is looked at. A trailing \r is also removed, so files with CRLF line endings work.

Blank lines are ignored.

Lines starting with # are comments and are ignored.

Lines matching /^[Ee][Nn][Vv]\ [A-Za-z0-9\_]+\=/ set environment variables. The name is between the space and the first =, the value is everything after it. The value may be empty. These may be set more than once, with the last one winning.

Lines matching /^\$[A-Za-z0-9\_]+\=/ are options. The name is between the $ and the first =, the value is everything after it. See OPTIONS.

Lines matching /^[A-Za-z0-9\_]+\=/ are variables. The name is before the first =, the value is everything after it. The value may be empty.

Lines matching /^[A-Za-z0-9\_]+\|/ are checks. The name is before the first |, the command is everything after it with leading whitespace removed.

Lines matching /^\%[A-Za-z0-9\_]+\|/ are debug checks. The leading % is not part of the name.

Lines matching /^\@[A-Za-z0-9\_]+\|[^\|]*\|/ are restarts. The name is between the @ and the first |, the options are between the first and second |, and the command is everything after the second |, with leading whitespace removed. Options are separated by spaces or tabs and are in the form key=value. Values containing spaces or tabs may be quoted with " or ', such as not_every="* 0-3 * * *". The checks and depends options are comma separated lists. See RESTARTS.

Lines matching /^\![A-Za-z0-9\_]+\|[^\|]*\|/ are compliance checks. They are laid out the same as restarts, with the name after the !. The options may be empty, but the second | is still needed, such as !foo||/bin/true. See COMPLIANCE.

Any other sort of line is an error.

Option, variable, check, debug check, compliance check, and restart names may not be redefined.

EXAMPLE SNECK CONFIG

env PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin
# this is a comment
GEOM_DEV=foo
geom_foo|/usr/local/libexec/nagios/check_geom mirror %GEOM_DEV%
does_not_exist|/bin/this_will_error yup... that it will

does_not_exist_2|/usr/bin/env /bin/this_will_also_error

#includes route info
%routes|netstat -rn

php_check|/usr/local/libexec/nagios/check_procs -C php-fpm -c 1:
@php_fpm|checks=php_check|/usr/sbin/service php_fpm restart

The first line sets the environment variable PATH.

The second is ignored as it is a comment.

The third sets the variable GEOM_DEV to 'foo'.

The fourth creates a check named geom_foo that calls check_geom with the value of the variable GEOM_DEV.

The fifth is an example of a check that calls a file that does not exist.

The sixth line is ignored as it is blank.

The seventh is an example of another command erroring.

The eighth is ignored as it is blank.

The ninth is ignored as it is a comment.

The tenth creates a debug check named routes.

The eleventh is ignored as it is blank.

The twelfth creates a check named php_check.

The thirteenth creates a restart named php_fpm that runs the command when php_check is critical, if restarts are enabled.

When it is run, errors for the fifth and seventh lines are printed to STDERR. For this reason, use '2> /dev/null' when calling it from snmpd or '2> /dev/null > /dev/null' when calling it from cron.

YAML FORMAT

This needs YAML::XS, which is optional and only loaded when a YAML config is used.

The top level is a mapping with up to seven keys, each of which is a mapping of names to values.

- options :: Options. See OPTIONS.

- env :: Environment variables to set. Set in sorted name order.

- vars :: Variables.

- checks :: Checks, with the command as the value.

- debugs :: Debug checks, with the command as the value.

- compliance :: Compliance checks. Each is a mapping of options plus command. See COMPLIANCE.

- restarts :: Restarts. Each is a mapping of options plus command. See RESTARTS.

Any other top level key is an error. Any section may be left out or empty. An empty file is a valid config with nothing in it.

Values must be strings or numbers. An empty value, such as 'FOO:' or 'FOO: ~', is an empty string for env and vars and an error for checks and debugs.

YAML::XS turns some unquoted values into something else. 'true' becomes 1 and 'false' becomes a empty string. Quote values like those.

Values starting with % or containing ': ' or ending in ':' must be quoted, such as '-w 80 -c 1:'.

YAML::XS keeps the last of any duplicate keys without saying anything, so redefinitions can not be caught like they are in the sneck format.

EXAMPLE YAML CONFIG

env:
  PATH: /sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin
vars:
  GEOM_DEV: foo
checks:
  geom_foo: /usr/local/libexec/nagios/check_geom mirror %GEOM_DEV%
  does_not_exist: /bin/this_will_error yup... that it will
  php_check: '/usr/local/libexec/nagios/check_procs -C php-fpm -c 1:'
debugs:
  routes: netstat -rn
restarts:
  php_fpm:
    command: /usr/sbin/service php_fpm restart
    checks: [php_check]

OPTIONS

Options are settings for sneck itself. Options given on the command line override these. cache_file, pid_dir, and locking are only used by sneck, not Monitoring::Sneck.

- cache_file :: The cache file. The same as -C for sneck. May not be empty.

- pid_dir :: The directory for the PID file used for locking. The same as -P for sneck. May not be empty.

- locking :: If 1, locking is enabled. The same as -l for sneck. -L disables it. Takes true and false in YAML.

- check_timeout :: Seconds to wait on each check, debug check, and compliance check before giving up on it. Compliance checks may override it via their timeout option. Works the same as timeout for restarts. A timeout counts as errored, with a exit of -1. The same as -T for sneck. Default :: 30

- check_timeout_signal :: Signal to send a check, debug check, or compliance check on timeout. Works the same as timeout_signal for restarts. The same as -s for sneck. Default :: none

- check_kill_sub_pids :: If 1, the timeout signal is also sent to all child processes of the check, debug check, or compliance check. Works the same as kill_sub_pids for restarts. The same as -k for sneck. -K disables it. Takes true and false in YAML. Default :: 1

- compliance_alert :: If 1, compliance checks that are not ok and not waived also set alert and are added to alertString, the same as checks. Takes true and false in YAML. Default :: 0

Any other option is an error.

$cache_file=/var/db/sneck/sneck.cache
$pid_dir=/var/run/sneck
$locking=1
$check_timeout=60
$check_timeout_signal=TERM

options:
  cache_file: /var/db/sneck/sneck.cache
  pid_dir: /var/run/sneck
  locking: true
  check_timeout: 60
  check_timeout_signal: TERM

COMPLIANCE

A compliance check is a nagios style check for how a system is set up, such as root logins being off or a file having the right mode. They are run the same as checks, but are counted separately, under compliance_ok, compliance_critical, and the like, and set compliance_alert instead of alert. Set the compliance_alert option to also have them set alert.

Restarts can not watch compliance checks.

Options are as below. All are optional.

- control :: The ID of the control it checks, such as CIS-5.2.10. Used in place of the name in compliance_alertString. Default :: none

- framework :: The framework the control is from, such as CIS or STIG. Compliance checks with a framework also get counts and a score for it under compliance_frameworks. Default :: none

- severity :: One of low, medium, high, or critical, in any case. Only used for reporting. It does not change how the exit code is counted. Default :: none

- interval :: Minimum seconds between runs. Until it is due again, the last result is reused and marked as cached. A change to the command, after variables are put in place, makes it run right away. 0 runs it every time. Default :: 0

- timeout :: Seconds to wait on it before giving up on it. Works the same as check_timeout, which is used if this is not set. Default :: check_timeout

- waived_until :: A date in the form YYYY-MM-DD. Through the end of that day, local time, a warning, critical, or unknown result is counted as waived instead. Errored is never waived, as that means the check itself is broken. Once the date passes, it counts as normal again. Default :: none

- waiver :: Why it is waived, such as a ticket number. Needs waived_until. Default :: none

The state used for interval is kept in a file. For sneck, this is the cache file name with '.compliance' added, so by default /var/cache/sneck.cache.compliance.

!ssh_no_root|control=CIS-5.2.10 framework=CIS severity=high|sshd -T | grep -qi '^permitrootlogin no$'
!pkg_vulns|control=VULN-1 severity=high interval=86400 timeout=300|/usr/local/libexec/nagios/check_pkg_audit
!telnet_off|control=CIS-2.2.18 framework=CIS waived_until=2026-12-31 waiver="legacy box, ticket 1234"|/usr/local/libexec/sneck/check_telnet_off

compliance:
  ssh_no_root:
    command: sshd -T | grep -qi '^permitrootlogin no$'
    control: CIS-5.2.10
    framework: CIS
    severity: high
  pkg_vulns:
    command: /usr/local/libexec/nagios/check_pkg_audit
    control: VULN-1
    severity: high
    interval: 86400
    timeout: 300
  telnet_off:
    command: /usr/local/libexec/sneck/check_telnet_off
    control: CIS-2.2.18
    framework: CIS
    waived_until: 2026-12-31
    waiver: legacy box, ticket 1234

PERL CHECKS

A check, debug check, or compliance check in the form 'perl:$name $args' runs the check $name from Monitoring::Sneck::Checks inside of sneck, instead of starting a program for it. Each is a module, such as Monitoring::Sneck::Checks::http, whose POD lists its args.

www|perl:http -H www.example.org -S -w 1 -c 2
www_cert|perl:https_cert -H www.example.org -w 30 -c 14
!hosts_file|control=LOCAL-1|perl:hosts -e db.example.org=%DB_IP%

Variables are put in place first, then the args are split as a shell would, so quote args with spaces in them.

The name must be a-z, 0-9, and _, starting with a letter, which is checked along with the args having balanced quotes when the config is read. Whether the check is there is only known when it is run.

The exit is what the check returns, as for any other command, with two differences. A exit of -1, which is errored, means the check, or Monitoring::Sneck::Checks, could not be loaded, such as it not being installed or the name being base. Running past check_timeout, or the timeout of a compliance check, is UNKNOWN, a exit of 3, as nothing is forked to be killed. Each check passes the timeout on to what it talks to the network through, so this is rare. check_timeout_signal and check_kill_sub_pids do not apply.

Monitoring::Sneck::Checks is optional and only loaded once a perl check is run.

Restarts can not be perl checks, as they are commands to run, not checks. A restart command starting with perl: is a error. Restarts may watch perl checks though, and check_restart reruns them in process.

RESTARTS

A restart is a command that is run when enough of the checks it watches fail. Restarts only run when asked for, via the restart option of Monitoring::Sneck or -r for sneck. Otherwise they are just reported.

The command can not be a perl check. See PERL CHECKS.

Options are as below.

- checks :: Required. The checks to watch. Debug checks and compliance checks can not be watched.

- threshold :: How many of the watched checks must fail for it to trigger. Must be between 1 and the number of checks. Default :: 1

- depends :: Other restarts this one depends on. When both trigger, the ones depended on run first. A depend that did not trigger is assumed to be fine and is not run. If a depend runs and fails, or was itself skipped for that reason, this one is skipped. Dependency cycles are errors. Default :: none

- cascade :: If 1, this also runs when any of its depends ran without failing, even if its own threshold was not met. Default :: 0

- ignore_unknown :: If 0, unknown, exit 3, counts as failed. Default :: 1

- ignore_errored :: If 0, errored counts as failed. This is any exit other than 0 to 3, dying on a signal, or not being able to run the check. Default :: 1

- min_interval :: Minimum seconds from when this restart last finished to when it may run again. 0 turns it off. Default :: 180

- max_retries :: How many times in a row it will run for its threshold before giving up until its checks recover. 0 means always retry. Runs from cascade do not count towards this. Default :: 0

- timeout :: Seconds to wait on the command before giving up on it. On timeout its output pipes are closed and it is left running, unless timeout_signal is set. If it writes again it gets SIGPIPE, or whatever it does when the reader goes away. A timeout counts as failed, with a exit of -1. Default :: 30

- timeout_signal :: Signal to send the command on timeout, as a name such as TERM or SIGTERM, or a number. 0 is not allowed. It is sent to the PID of the command and it is not waited on afterwards. If the command has shell metacharacters in it, that PID is the /bin/sh running it, so use kill_sub_pids to reach the command. Default :: none

- kill_sub_pids :: If 1, the timeout signal is also sent to all child processes of the command, found via pgrep and signaled via pkill, deepest first. Does nothing without timeout_signal. Default :: 1

- check_restart :: If 1, its checks are rerun after the command finishes and their results replace the earlier ones in what is returned. If they still meet the threshold, the restart counts as failed, the same as the command failing. Default :: 0

- check_restart_delay :: Seconds to wait after the command finishes before rerunning its checks. Does nothing without check_restart. Default :: 5

- not_every :: A maintenance window, as a five field cron spec of minute, hour, day of month, month, and day of week. While the local time matches it, this restart does not run. Only numbers are allowed, no names such as sat or jan. Sunday is 0 or 7. As with cron, if both day of month and day of week are given, either one matching is enough. Restarts depending on one held back by this treat it as if it did not trigger. In the sneck format it must be quoted. In YAML it must be quoted if it starts with *. Matched via DateTime::Event::Cron. Default :: none

Critical always counts as failed. Ok and warning never do.

The 0/1 options also take true and false in YAML.

Restart commands get variables put in place the same as checks. A restart does not wait for its output to be closed once its command has exited, so a daemon started by it holding stdout open is fine.

The command is also given the environment variables below, describing why it is running.

- SNECK_RESTART :: The name of the restart.

- SNECK_REASON :: Either 'threshold' or 'cascade'.

- SNECK_CASCADE_FROM :: The depend it cascaded from, or empty.

- SNECK_FAILED_CHECKS :: Comma separated list of its checks that counted as failed. May be empty for a cascade.

- SNECK_CHECKS :: Comma separated list of the checks it watches.

- SNECK_THRESHOLD :: The threshold.

- SNECK_ATTEMPTS :: Runs for its threshold since its checks last recovered, this one included.

@httpd|checks=http_check,php_check threshold=2 depends=php_fpm cascade=1 timeout=60|/usr/sbin/service apache24 restart

restarts:
  httpd:
    command: /usr/sbin/service apache24 restart
    checks: [http_check, php_check]
    threshold: 2
    depends: [php_fpm]
    cascade: true
    timeout: 60

@php_fpm|checks=php_check timeout=60 timeout_signal=TERM check_restart=1|/usr/sbin/service php_fpm restart

restarts:
  php_fpm:
    command: /usr/sbin/service php_fpm restart
    checks: [php_check]
    timeout: 60
    timeout_signal: TERM
    check_restart: true

@php_fpm|checks=php_check not_every="* 2-3 * * 0"|/usr/sbin/service php_fpm restart

restarts:
  php_fpm:
    command: /usr/sbin/service php_fpm restart
    checks: [php_check]
    not_every: '* 2-3 * * 0'

The last two never restart between 02:00 and 03:59 on Sundays.

METHODS

new

Reads and parses a config.

One argument is taken and that is a hash ref.

- file :: Path to the config file to read.

- raw :: The config as a string. Used instead of file if both are given.

- format :: Either 'sneck' or 'yaml'. If not given, a file ending in .yaml or .yml, in any case, is 'yaml' and anything else, including raw, is 'sneck'.

Dies if neither file nor raw is given, the file can not be read, the format is unknown, or the format is 'yaml' and YAML::XS can not be loaded. Problems with the config itself never die. Check them via is_valid and errors.

my $config;
eval { $config = Monitoring::Sneck::Config->new( { file => $file } ); };
if ($@) {
    die($@);
}

my $config = Monitoring::Sneck::Config->new( { raw => "FOO=bar\nfoo_check|/bin/echo %FOO%\n" } );

my $config = Monitoring::Sneck::Config->new( { raw => "checks:\n  foo_check: /bin/true\n", format => 'yaml' } );

file

Returns the path to the config file read or undef if it was created from a string.

my $file = $config->file;

format

Returns the format the config was parsed as, either 'sneck' or 'yaml'.

my $format = $config->format;

raw

Returns the raw config as a string, exactly as read.

my $raw = $config->raw;

is_valid

Returns 1 if the config has no errors, otherwise 0. Warnings do not affect this.

if ( !$config->is_valid ) {
    warn('config has errors');
}

errors

Returns a list of errors found in the config. Each is a hash ref as below.

- where :: Where the problem is, for printing. For the sneck format this is 'line N'. For YAML this is the path to the problem, such as 'checks.foo', or 'YAML' for a YAML syntax error.

- line :: The line number, starting at 1. Undef for YAML.

- path :: The path to the problem, such as 'checks.foo'. Undef for the sneck format.

- text :: The line as it appears in the file. Undef for YAML.

- message :: A description of the problem.

For the sneck format these are in line order. For YAML they are in section order, options, env, vars, checks, debugs, compliance, then restarts, then by name.

foreach my $error ( $config->errors ) {
    print $error->{where} . ': ' . $error->{message} . "\n";
}

warnings

Returns a list of warnings found in the config. Same format and order as errors.

foreach my $warning ( $config->warnings ) {
    print $warning->{where} . ': ' . $warning->{message} . "\n";
}

options

Returns a hash ref of the options set in the config, with the names as keys. Options not set are left out, so the caller can fall back to its own defaults. Invalid ones are also left out. Values are in the standard form described under validate_option. This is a copy, so changing it does not change the config.

my $cache_file = $config->options->{cache_file};

validate_option

Validates a option value and puts it in a standard form, the same as is done for the config. May be called on the class or a object.

Two arguments are taken, the option name and the value.

Returns two values. The first is the value in standard form or undef if it is not valid. The second is undef if it is valid or a error message.

The standard forms are as below.

- locking, check_kill_sub_pids, compliance_alert :: 0 or 1.

- check_timeout :: A number.

- check_timeout_signal :: The signal name without the SIG prefix, such as TERM. Numbers are turned into names.

my ( $signal, $error ) = Monitoring::Sneck::Config->validate_option( 'check_timeout_signal', 'sigterm' );
# $signal is 'TERM' and $error is undef

my ( $timeout, $error ) = Monitoring::Sneck::Config->validate_option( 'check_timeout', 0 );
# $timeout is undef and $error is 'option "check_timeout" must be a whole number of at least 1'

vars

Returns a hash ref of the variables, with the names as keys. This is a copy, so changing it does not change the config.

my $value = $config->vars->{GEOM_DEV};

env

Returns a array ref of environment variables to set, in the order they should be set. Each item is a array ref of the name and value. This is a copy, so changing it does not change the config.

For the sneck format, the order is the order they appear in the config. For YAML, it is sorted by name.

Nothing is set in %ENV by this module. That is left to the caller.

foreach my $env ( @{ $config->env } ) {
    $ENV{ $env->[0] } = $env->[1];
}

checks

Returns a hash ref of the checks, with the names as keys and the commands, before variable substitution, as values. This is a copy, so changing it does not change the config.

my $command = $config->checks->{geom_foo};

debugs

Returns a hash ref of the debug checks. Same format as checks. The names do not include the leading % used by the sneck format.

my $command = $config->debugs->{routes};

restarts

Returns a hash ref of the restarts, with the names as keys. Each value is a hash ref with every option filled in, defaults included, as below. This is a copy, so changing it does not change the config.

- command :: The command, before variable substitution.

- checks :: Array ref of the names of the checks watched.

- depends :: Array ref of the names of the restarts depended on.

- threshold, cascade, ignore_unknown, ignore_errored, min_interval, max_retries, timeout, kill_sub_pids, check_restart, check_restart_delay :: As described under RESTARTS. The 0/1 options are always 0 or 1.

- timeout_signal :: The signal name without the SIG prefix, such as TERM, or undef if not set. Numbers are turned into names.

- not_every :: The cron spec with surrounding whitespace removed and other runs of whitespace turned into a single space, or undef if not set.

my $threshold = $config->restarts->{httpd}{threshold};

compliance

Returns a hash ref of the compliance checks, with the names as keys. Each value is a hash ref with every option filled in, as below. This is a copy, so changing it does not change the config.

- command :: The command, before variable substitution.

- control, framework, waived_until, waiver :: As described under COMPLIANCE, or undef if not set.

- severity :: Lower case, or undef if not set.

- interval :: As described under COMPLIANCE.

- timeout :: As described under COMPLIANCE, or undef if not set, in which case check_timeout is used.

my $control = $config->compliance->{ssh_no_root}{control};

substitute

Takes a string, usually a check command, and returns it with the variables put in place. Variables are put in place in sorted name order. Undefined variables are left as written.

my $ran = $config->substitute( $config->checks->{geom_foo} );

BUGS

Please report any bugs or feature requests to bug-monitoring-sneck at rt.cpan.org, or through the web interface at https://rt.cpan.org/NoAuth/ReportBug.html?Queue=Monitoring-Sneck.