diff --git a/check-dns-zone.sh b/check-dns-zone.sh new file mode 100755 index 0000000..d6c32eb --- /dev/null +++ b/check-dns-zone.sh @@ -0,0 +1,325 @@ +#!/bin/bash +# shellcheck disable=SC2317 # Don't warn about unreachable commands in this file +# +# After ideas from https://raw.githubusercontent.com/ableyjoe/checksig.sh/master/checksig.sh +# + +set -e +set -u + +VERBOSE="" +DEBUG="" + +VERSION="0.1" + +BASENAME=$(basename "${0}" ) +BASE_DIR=$( dirname "$0" ) +cd "${BASE_DIR}" +BASE_DIR=$( readlink -f . ) + +# DIG Options +DNSSEC= +SERVER= +ZONE= +WARN=$(( 7 * 24 * 60 * 60 )) +CRIT=$(( 5 * 24 * 60 * 60 )) + +#------------------------------------------------------------------------------ +description() { + cat <<-EOF + Checks the validity of the given DNS zone by retrieving the SOA for this zone. + + If the option --dnssec is given, additionally the DNSSEC RRSIG of the SOA is + checked for its existence and its remaining livetime. + + The threshold may be given as integers of seconds, or as minutes with the suffix 'm', + as hours with the suffix 'h' or as days with the suffix 'd'. + + EOF + +} + +#------------------------------------------------------------------------------ +usage() { + + cat <<-EOF + Usage: ${BASENAME} [-d|--debug] [-v|--verbose] [-D|--dnssec] [-s|--server NAMESERVER] [-w|--warn WARNING_TIME] [-c|--crit CRITICAL_TIME} -z|--zone ZONE + ${BASENAME} [-h|--help] + ${BASENAME} [-V|--version] + + Options: + -d|--debug Debug output (bash -x). + -v|--verbose Set verbosity on. + -D|--dnssec Check the DNSSEC RRDS of the SOA of the zone. + -s|--server NAMESERVER + The nameserver to use for checking the zone. + If omitted, one arbitrary nameserver from /etc/resolv.conf is used. + -w|--warn WARNING_TIME + The threshold for warning about the remaining livetime of the RRSIG. + Not used, if --dnssec was not given. + Default: ${WARN} seconds. + -c|--crit CRITICAL_TIME + The threshold for critical about the remaining livetime of the RRSIG. + Not used, if --dnssec was not given. + Default: ${CRIT} seconds. + -z|--zone ZONE The zone to check. Mandatory option. + -h|--help Show this output and exit. + -V|--version Prints out version number of the script and exit. + + EOF + +} + +#------------------------------------------------------------------------------ +timespec () { + case $1 in + *m) + echo "$(( ${1/[dhm]/} * 60))" + ;; + *h) + echo "$(( ${1/[dhm]/} * 60 * 60 ))" + ;; + *d) + echo "$(( ${1/[dhm]/} * 60 * 60 * 24 ))" + ;; + *) + echo "$1" + ;; + esac +} + +#------------------------------------------------------------------------------ +seconds2human() { + + local seconds_total="$1" + local out="" + + local seconds=$(( seconds_total % 60 )) + local minutes_total=$(( seconds_total / 60 )) + local minutes=$(( minutes_total % 60 )) + local hours_total=$(( minutes_total / 60 )) + local hours=$(( hours_total % 24 )) + local days=$(( hours_total / 24 )) + + if [[ "${days}" -gt 0 ]] ; then + out="${days}d ${hours}h ${minutes}m ${seconds}s" + elif [[ "${hours}" -gt 0 ]] ; then + out="${hours}h ${minutes}m ${seconds}s" + elif [[ "${minutes_total}" -gt 0 ]] ; then + out="${minutes}m ${seconds}s" + else + out="${seconds}s" + fi + + echo "${out}" + +} + +#------------------------------------------------------------------------------ +get_options() { + + local tmp= + local short_options="dvDs:z:w:c:hV" + local long_options="debug,verbose,dnssec,server:,zone:,warn:,crit:,help,version" + + set +e + tmp=$( getopt -o "${short_options}" --long "${long_options}" -n "${BASENAME}" -- "$@" ) + ret="$?" + if [[ "${ret}" != 0 ]] ; then + echo "" >&2 + usage >&2 + exit 1 + fi + set -e + + # Note the quotes around `$TEMP': they are essential! + eval set -- "${tmp}" + + while true ; do + case "$1" in + -d|--debug) + DEBUG="y" + shift + ;; + -v|--verbose) + VERBOSE="y" + shift + ;; + -D|--dnssec) + DNSSEC="y" + shift + ;; + -s|--server) + SERVER="$2" + shift + shift + ;; + -w|--warn) + WARN=$( timespec "$2" ) + shift + shift + ;; + -c|--crit) + CRIT=$( timespec "$2" ) + shift + shift + ;; + -z|--zone) + ZONE="$2" + shift + shift + ;; + -h|--help) + description + echo + usage + exit 0 + ;; + -V|--version) + echo "${BASENAME} version: ${VERSION}" + exit 0 + ;; + --) shift + break + ;; + *) echo "Internal error!" + exit 1 + ;; + esac + done + + if [[ "${DEBUG}" = "y" ]] ; then + set -x + fi + + if [[ -z "${ZONE}" ]] ; then + echo "UNKNOWN - ${BASENAME}: No zone was given." + echo + usage + exit 3 + fi + + if [[ "${WARN}" -lt "${CRIT}" ]] ; then + echo "UNKNOWN - ${BASENAME}: The warning threshold (${WARN} seconds) is less than the critical threshold (${CRIT} seconds), which is weird." + exit 3 + fi + +} + +#------------------------------------------------------------------------------ +check_soa() { + + local cmd + local response + local server + local remaining + local msg + local remaining_out + local warn_out + local crit_out + + cmd="dig \"${ZONE}\" " + + if [[ -n "${SERVER}" ]] ; then + cmd+="\"@${SERVER}\" " + fi + + cmd+="SOA +nocomments +noadditional +nocmd +noquestion" + + if [[ "${DNSSEC}" ]] ; then + cmd+=" +dnssec" + fi + + if [[ "${VERBOSE}" ]] ; then + echo "Executing: ${cmd}" >&2 + fi + # shellcheck disable=disable=SC2086,SC2294 + response=$( eval ${cmd} ) + if [[ "${VERBOSE}" ]] ; then + echo -e "Response:\n${response}" >&2 + fi + + server=$( echo "${response}" | grep -P -i '^;; SERVER:') + server="${server/;; SERVER: /}" + + if echo "${response}" | grep -P -q -i "^${ZONE}\\.\\s.*\\ssoa\\s" ; then + : + else + echo "CRITICAL - ${BASENAME}: Did not found SOA of zone '${ZONE}', as observed on server ${server}." + exit 2 + fi + + msg="Found SOA of zone '${ZONE}', as observed on server ${server}." + + if [[ "${DNSSEC}" ]] ; then + + if echo "${response}" | grep -P -q -i "^${ZONE}\\.\\s.*\\sRRSIG\\s" ; then + : + else + echo "CRITICAL - ${BASENAME}: Missing RRSIG response for SOA of '${ZONE}', as observed on server ${server}." + exit 2 + fi + + rrsig=( $( echo "${response}" | grep -P -i '\s+IN\s+RRSIG\s+SOA\s+') ) + remaining=$(( $(date --utc --date="${rrsig[8]:0:4}-${rrsig[8]:4:2}-${rrsig[8]:6:2} ${rrsig[8]:8:2}:${rrsig[8]:10:2}:${rrsig[8]:12:2}" +%s) - $(date --utc +%s) )) + + remaining_out=$( seconds2human "${remaining}" ) + warn_out=$( seconds2human "${WARN}" ) + crit_out=$( seconds2human "${CRIT}" ) + + if [[ "${VERBOSE}" ]] ; then + echo -e "Remaining: ${remaining_out}" >&2 + fi + + if [[ -z "${remaining}" ]] ; then + msg+=" Could not find RRSIG of SOA or the signature expiration for zone '${ZONE}'." + echo "CRITICAL - ${BASENAME}: ${msg}" + exit 2 + fi + + if [[ "${remaining}" -lt 0 ]] ; then + msg+=" Signatures in zone '${ZONE}' expired ${remaining} seconds ago." + echo "CRITICAL - ${BASENAME}: ${msg}" + exit 2 + fi + + if [[ "${remaining}" -lt "${CRIT}" ]] ; then + msg+=" Remaining signature validity for zone '${ZONE}' is in ${remaining_out}, less than the critical threshold of ${crit_out}." + echo "CRITICAL - ${BASENAME}: ${msg}" + exit 2 + fi + + if [[ "${remaining}" -lt "${WARN}" ]] ; then + msg+=" Remaining signature validity for zone '${ZONE}' is ${remaining_out}, less than the warning threshold of ${warn_out}." + echo "WARNING - ${BASENAME}: ${msg}" + exit 1 + fi + + msg+=" Remaining signature validity for zone '${ZONE}' is ${remaining} seconds." + fi + + echo "OK - ${BASENAME}: ${msg}" + exit 0 + +} + +################################################################################ +## +## Main +## +################################################################################ + +#------------------------------------------------------------------------------ +main() { + + get_options "$@" + check_soa + +} + +main "$@" + + +exit 0 + +# vim: ts=4 list diff --git a/tests/test_07_monitoring_range.py b/tests/test_07_monitoring_range.py index 9c8fe35..21fae39 100755 --- a/tests/test_07_monitoring_range.py +++ b/tests/test_07_monitoring_range.py @@ -158,6 +158,182 @@ def test_init(self): self.assertEqual(mon_range.end_infinity, test_data["end_infinity"]) self.assertEqual(mon_range.alert_on, test_data["alert_on"]) + # ------------------------------------------------------------------------- + def test_operator_in(self): + """Test of 'in' and 'not in' operators.""" + LOG.info(self.get_method_doc()) + + from monitoring.range import MonitoringRange + + mon_range = MonitoringRange("6") + LOG.debug("Initialized range:\n" + pp(mon_range.as_dict())) + + test_data = ( + (-0.1, False), + (0, True), + (0.1, True), + (4, True), + (5.99, True), + (6, True), + (6.01, False), + ) + + for token in test_data: + val = token[0] + exp = token[1] + + LOG.debug(f"Test value {val} in range {str(mon_range)!r}: {exp}") + if exp: + self.assertIn(val, mon_range) + else: + self.assertNotIn(val, mon_range) + + if self.verbose >= 1: + print() + mon_range = MonitoringRange("@6") + LOG.debug("Initialized inverted range:\n" + pp(mon_range.as_dict())) + + test_data = ( + (-0.1, True), + (0, False), + (0.1, False), + (4, False), + (5.99, False), + (6, False), + (6.01, True), + ) + + for token in test_data: + val = token[0] + exp = token[1] + + LOG.debug(f"Test value {val} outside of range {str(mon_range)!r}: {exp}") + if exp: + self.assertIn(val, mon_range) + else: + self.assertNotIn(val, mon_range) + + # ------------------------------------------------------------------------- + def test_check_value(self): + """Test checking different values.""" + LOG.info(self.get_method_doc()) + + from monitoring.range import MonitoringRange + + test_data = ( + ( + "-7:23", ( + (-23, False), + (-7, True), + (-1, True), + (0, True), + (4, True), + (23, True), + (23.1, False), + (79.999999, False), + ), + ), + ( + ":5.75", ( + (-1, False), + (0, True), + (4, True), + (5.75, True), + (5.7501, False), + (6, False), + ), + ), + ( + "~:-95.99", ( + (-1001341, True), + (-96, True), + (-95.999, True), + (-95.99, True), + (-95.989, False), + (-95, False), + (0, False), + (5.7501, False), + ), + ), + ( + "10:", ( + (-95.999, False), + (-1, False), + (0, False), + (9.91, False), + (10, True), + (11.11, True), + (123456789012346, True), + ), + ), + ( + "123456789012345:", ( + (-95.999, False), + (0, False), + (123456789012344.91, False), + (123456789012345, True), + (123456789012345.61, True), + (123456789012346, True), + ), + ), + ( + "~:0", ( + (-123456789012344.91, True), + (-1, True), + (0, True), + (.001, False), + (123456789012345, False), + ), + ), + ( + "@0:657.8210567", ( + (-134151, True), + (-1, True), + (0, False), + (.001, False), + (657.8210567, False), + (657.9, True), + (123456789012345, True), + ), + ), + ( + "1:1", ( + (-1, False), + (0, False), + (0.99, False), + (1, True), + (1.001, False), + (5.2, False), + ), + ), + ) + + for token in test_data: + if self.verbose >= 1: + print() + + range_str = token[0] + test_tokens = token[1] + + mon_range = MonitoringRange(range_str) + + for test_pair in test_tokens: + + val = test_pair[0] + exp = test_pair[1] + if exp: + if mon_range.invert_match: + LOG.debug(f"Test value {val} outside of range {str(mon_range)!r}.") + else: + LOG.debug(f"Test value {val} in range {str(mon_range)!r}.") + self.assertIn(val, mon_range) + else: + if mon_range.invert_match: + LOG.debug(f"Test value {val} in range {str(mon_range)!r}.") + else: + LOG.debug(f"Test value {val} not in range {str(mon_range)!r}.") + self.assertNotIn(val, mon_range) + # ============================================================================= if __name__ == "__main__": @@ -172,6 +348,8 @@ def test_init(self): suite = unittest.TestSuite() suite.addTest(TestMonitoringRange("test_init", verbose)) + suite.addTest(TestMonitoringRange("test_operator_in", verbose)) + suite.addTest(TestMonitoringRange("test_check_value", verbose)) runner = unittest.TextTestRunner(verbosity=verbose) diff --git a/tests/test_10_monitoring_threshold.py b/tests/test_10_monitoring_threshold.py new file mode 100755 index 0000000..a11f158 --- /dev/null +++ b/tests/test_10_monitoring_threshold.py @@ -0,0 +1,241 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +@summary: Test script (and module) for unit tests on monitoring.threshold. + +In this test case the MonitoringThreshold object is tested. + +@author: Frank Brehm +@contact: frank@brehm-online.com +@copyright: © 2023 - 2026 Frank Brehm, Berlin +@license: GPL3 +""" + +import logging +import unittest + +from general import MonitoringScriptsTestcase +from general import get_arg_verbose +from general import init_root_logger +from general import pp + +LOG = logging.getLogger("test_monitoring_threshold") + + +# ============================================================================= +class TestMonitoringThreshold(MonitoringScriptsTestcase): + """Testcase class for testing MonitoringThreshold object.""" + + # ------------------------------------------------------------------------- + def setUp(self): + """Execute this on seting up before calling each particular test method.""" + if self.verbose >= 1: + print() + + # ------------------------------------------------------------------------- + def test_init(self): + """Test init of a monitoring.MonitoringThreshold.""" + LOG.info(self.get_method_doc()) + + from monitoring import MonitoringThreshold + + threshold = MonitoringThreshold() + + if self.verbose > 2: + LOG.debug("Initialized object:\n" + pp(threshold.as_dict())) + + LOG.debug("Test, whether warning is not set.") + self.assertFalse(threshold.warning.is_set) + + LOG.debug("Test, whether critical is not set.") + self.assertFalse(threshold.critical.is_set) + + if self.verbose >= 1: + print() + + LOG.debug("Set warning and critical to ''.") + threshold.set_thresholds(warning="", critical="") + + LOG.debug("Test, whether warning is not set.") + self.assertFalse(threshold.warning.is_set) + + LOG.debug("Test, whether critical is not set.") + self.assertFalse(threshold.critical.is_set) + + # ------------------------------------------------------------------------- + def test_use_ranges(self): + """Test init of a monitoring.MonitoringThreshold with ranges.""" + LOG.info(self.get_method_doc()) + + from monitoring import MonitoringThreshold + + warn_value = 80 + crit_value = 90 + + threshold = MonitoringThreshold(warning=f"{warn_value}", critical=f"{crit_value}") + + if self.verbose > 2: + LOG.debug("Initialized object:\n" + pp(threshold.as_dict())) + + LOG.debug("Test, whether warning is set.") + self.assertTrue(threshold.warning.is_set) + + LOG.debug("Test, whether critical is set.") + self.assertTrue(threshold.critical.is_set) + + LOG.debug("Test, whether warning.start == 0.") + self.assertEqual(threshold.warning.start, 0) + + LOG.debug(f"Test, whether warning.end == {warn_value}.") + self.assertEqual(threshold.warning.end, warn_value) + + LOG.debug("Test, whether critical.start == 0.") + self.assertEqual(threshold.critical.start, 0) + + LOG.debug(f"Test, whether critical.end == {crit_value}.") + self.assertEqual(threshold.critical.end, crit_value) + + # ------------------------------------------------------------------------- + def test_bad_ranges(self): + """Test init of a monitoring.MonitoringThreshold with bad ranges.""" + LOG.info(self.get_method_doc()) + + from monitoring import MonitoringThreshold + from monitoring import InvalidRangeError + + LOG.debug("Try create Threshold wih warning='total', critical='rubbish'.") + with self.assertRaises(InvalidRangeError) as cm: + threshold = MonitoringThreshold(warning="total", critical="rubbish") + LOG.error(f"This should never be visible: {threshold!r}.") + e = cm.exception + LOG.debug("{c} raised: {e}".format(c=e.__class__.__name__, e=e)) + + # ------------------------------------------------------------------------- + def test_get_status(self): + """Test get status of a monitoring.MonitoringThreshold by a value.""" + LOG.info(self.get_method_doc()) + + from monitoring import MonitoringThreshold + + test_data = ( + { + "warning": "5:33", + "tests": ( + (-1, "WARNING"), + (4, "WARNING"), + (4.9999, "WARNING"), + (5, "OK"), + (14.21, "OK"), + (33, "OK"), + (33.01, "WARNING"), + (10231, "WARNING"), + ), + }, + { + "warning": "~:30", + "critical": "~:60", + "tests": ( + (-1, "OK"), + (4, "OK"), + (29.999999, "OK"), + (30, "OK"), + (30.1, "WARNING"), + (59.9, "WARNING"), + (60, "WARNING"), + (60.00001, "CRITICAL"), + (10231, "CRITICAL"), + ), + }, + { + "critical": "~:25", + "tests": ( + (-1, "OK"), + (4, "OK"), + (24.999999, "OK"), + (25, "OK"), + (25.001, "CRITICAL"), + (31001, "CRITICAL"), + ), + }, + { + "warning": "10:25", + "critical": "~:25", + "tests": ( + (-1, "WARNING"), + (4, "WARNING"), + (4.9999, "WARNING"), + (10, "OK"), + (14.21, "OK"), + (25, "OK"), + (25.01, "CRITICAL"), + (31001, "CRITICAL"), + ), + }, + { + "warning": "@10:25", + "critical": "10:", + "tests": ( + (-1, "CRITICAL"), + (4, "CRITICAL"), + (4.9999, "CRITICAL"), + (10, "WARNING"), + (14.21, "WARNING"), + (25, "WARNING"), + (25.01, "OK"), + (31001, "OK"), + ), + }, + ) + + for test_token in test_data: + if self.verbose >= 1: + print() + + if "warning" in test_token: + w = test_token["warning"] + else: + w = None + + if "critical" in test_token: + c = test_token["critical"] + else: + c = None + + LOG.info(f"Testing threshold with warning={w!r} and critical={c!r}.") + threshold = MonitoringThreshold(warning=w, critical=c) + + for test_pair in test_token["tests"]: + val = test_pair[0] + exp = test_pair[1] + status_exp = threshold.errors[exp] + + LOG.debug(f"Test threshold against {val} => {status_exp} ({exp!r}).") + status_got = threshold.get_status(val) + LOG.debug(f"Got status {status_got}.") + self.assertEqual(status_exp, status_got) + + +# ============================================================================= +if __name__ == "__main__": + + verbose = get_arg_verbose() + if verbose is None: + verbose = 0 + init_root_logger(verbose) + + LOG.info("Starting tests ...") + + suite = unittest.TestSuite() + + suite.addTest(TestMonitoringThreshold("test_init", verbose)) + suite.addTest(TestMonitoringThreshold("test_use_ranges", verbose)) + suite.addTest(TestMonitoringThreshold("test_bad_ranges", verbose)) + suite.addTest(TestMonitoringThreshold("test_get_status", verbose)) + + runner = unittest.TextTestRunner(verbosity=verbose) + + result = runner.run(suite) + +# ============================================================================= + +# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 diff --git a/tests/test_20_monitoring_performance.py b/tests/test_20_monitoring_performance.py new file mode 100755 index 0000000..20291d4 --- /dev/null +++ b/tests/test_20_monitoring_performance.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +@summary: Test script (and module) for unit tests on monitoring.perf. + +In this test case the MonitoringPerformance object is tested. + +@author: Frank Brehm +@contact: frank@brehm-online.com +@copyright: © 2023 - 2026 Frank Brehm, Berlin +@license: GPL3 +""" + +import logging +import unittest + +from general import MonitoringScriptsTestcase +from general import get_arg_verbose +from general import init_root_logger +from general import pp + +LOG = logging.getLogger("test_monitoring_performance") + + +# ============================================================================= +class TestMonitoringPerformance(MonitoringScriptsTestcase): + """Testcase class for testing MonitoringPerformance object.""" + + # ------------------------------------------------------------------------- + def setUp(self): + """Execute this on seting up before calling each particular test method.""" + if self.verbose >= 1: + print() + + # ------------------------------------------------------------------------- + def test_init(self): + """Test init of a monitoring.MonitoringPerformance.""" + LOG.info(self.get_method_doc()) + + from monitoring import MonitoringPerformance + from monitoring import MonitoringPerformanceError + + perf = MonitoringPerformance("sample", 0) + + if self.verbose > 2: + LOG.debug("Initialized object:\n" + pp(perf.as_dict())) + + if self.verbose >= 1: + print() + + LOG.info("Test init of MonitoringPerformance with bad arguments.") + + bad_init_data = ( + [], + [""], + ["", 0], + [" "], + [" ", 0], + ["sample"], + ["sample", "bla"], + ) + + for args in bad_init_data: + LOG.debug(f"Trying to init MonitoringPerformance with: {pp(args)}.") + with self.assertRaises((MonitoringPerformanceError, TypeError)) as cm: + perf = MonitoringPerformance(*args) + LOG.error(f"This should never be visible: {perf!r}.") + e = cm.exception + LOG.debug("{c} raised: {e}".format(c=e.__class__.__name__, e=e)) + + +# ============================================================================= +if __name__ == "__main__": + + verbose = get_arg_verbose() + if verbose is None: + verbose = 0 + init_root_logger(verbose) + + LOG.info("Starting tests ...") + + suite = unittest.TestSuite() + + suite.addTest(TestMonitoringPerformance("test_init", verbose)) + + runner = unittest.TextTestRunner(verbosity=verbose) + + result = runner.run(suite) + +# ============================================================================= + +# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 diff --git a/update-env.sh b/update-env.sh index 35689bc..7d6e067 100755 --- a/update-env.sh +++ b/update-env.sh @@ -8,7 +8,7 @@ VERBOSE="n" DEBUG="n" QUIET='n' -VERSION="3.0" +VERSION="3.1" # console colors: RED="" @@ -188,7 +188,7 @@ get_options() { local tmp= local short_options="dvqhV" - local long_options="debug,verbose,quiet,help,version" + local long_options="debug,verbose,quiet,nocolor,help,version" set +e tmp=$( getopt -o "${short_options}" --long "${long_options}" -n "${BASENAME}" -- "$@" )