From 25ad4fdb6c8ea8eb25f2b4e804cb46dae8dde54e Mon Sep 17 00:00:00 2001 From: Anima Artificialis Date: Wed, 20 May 2026 21:48:29 +0000 Subject: [PATCH 1/2] feat: add raid mode for monitoring storage pool raid status MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a `raid` mode that surfaces Synology raid status (OID 1.3.6.1.4.1.6574.3.1.1.x) for every storage pool: Normal → OK, intermediate states (Repairing/Migrating/Expanding/.../Canceling) → WARNING, Degrade and Crashed → CRITICAL, anything else → UNKNOWN. The previous severity is preserved across iterations so a WARNING pool can never downgrade a CRITICAL one. Status "2" (Repairing) is included in the WARNING set. README updated with the new mode. This is an alternative take on #62 incorporating review feedback from issuecomment-4502847249. --- README.md | 1 + check_synology.py | 35 ++++++++++++++++++++++++++++++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 39bec85..b0cc334 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,7 @@ Custom timeouts (`-t`) and retries (`-r`) can be specified by using `-t` and `-r | storage | Detects and checks all disks (free, total, %) | if more used than w/c in % | | update | Shows the current DSM version and if DSM update is available | if update is "Unavailable", will trigger OK
if update is "Available", will trigger WARNING
otherwise: UNKNOWN | | status | Shows model, s/n, temp and status of system, fan, cpu fan and power supply | if temp higher than w/c in °C | +| raid | Shows raid volume status for every storage pool | WARNING for intermediate states (Repairing/Migrating/Expanding/Deleting/Creating/RaidSyncing/RaidParityChecking/RaidAssembling/Canceling); CRITICAL for Degrade and Crashed; OK for Normal | diff --git a/check_synology.py b/check_synology.py index fe72ae5..b0fd393 100755 --- a/check_synology.py +++ b/check_synology.py @@ -15,7 +15,7 @@ parser.add_argument("username", help="the snmp user name", type=str) parser.add_argument("authkey", help="the auth key", type=str) parser.add_argument("privkey", help="the priv key", type=str) -parser.add_argument("mode", help="the mode", type=str, choices=["load", "memory", "disk", "storage", "update", "status"]) +parser.add_argument("mode", help="the mode", type=str, choices=["load", "memory", "disk", "storage", "update", "status", "raid"]) parser.add_argument("-w", help="warning value for selected mode", type=int) parser.add_argument("-c", help="critical value for selected mode", type=int) parser.add_argument("-p", help="the snmp port", type=int, dest="port", default=161) @@ -302,3 +302,36 @@ def exitCode(): # 3. Respond with textual and perfdata output and propagate exit code. print(state + ' - Model: %s, S/N: %s, System Temperature: %s C, System Status: %s, System Fan: %s, CPU Fan: %s, Powersupply : %s' % (status_model, status_serial, status_temperature, status_system, status_system_fan, status_cpu_fan, status_power) + ' | system_temp=%sc' % status_temperature) exitCode() + +if mode == 'raid': + output = '' + perfdata = '|' + # Synology raid status values (from synology MIB). + # 1 = Normal — the raid functions normally. + # 2 = Repairing + # 3 = Migrating + # 4 = Expanding + # 5 = Deleting + # 6 = Creating + # 7 = RaidSyncing + # 8 = RaidParityChecking + # 9 = RaidAssembling + # 10 = Canceling + # 11 = Degrade — a tolerable disk failure has occurred (still actionable). + # 12 = Crashed — raid is in read-only state. + WARNING_STATUSES = {"2", "3", "4", "5", "6", "7", "8", "9", "10"} + CRITICAL_STATUSES = {"11", "12"} # 11 = Degrade, 12 = Crashed + for item in snmpwalk('1.3.6.1.4.1.6574.3.1.1.1'): + i = item.oid_index or item.oid.split('.')[-1] + storage_name = snmpget('1.3.6.1.4.1.6574.3.1.1.2.' + str(i)) + raid_status = str(snmpget('1.3.6.1.4.1.6574.3.1.1.3.' + str(i))) + if raid_status in CRITICAL_STATUSES: + state = "CRITICAL" + elif raid_status in WARNING_STATUSES and state != "CRITICAL": + state = "WARNING" + elif raid_status != "1" and state == "OK": + state = "UNKNOWN" + output += ' - raid status: [' + storage_name + '] status=' + raid_status + perfdata += ' "' + storage_name + '"=' + raid_status + print('%s%s %s' % (state, output, perfdata)) + exitCode() From fa56408e9059981b8de6c6db95101e8cd1ebec2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Peka=C5=99?= Date: Fri, 29 May 2026 08:11:01 +0000 Subject: [PATCH 2/2] feat(storage): --warning-free / --critical-free + perfdata max field - New --warning-free / --critical-free args: PERC_FREE thresholds (check_disk -w/-c convention). Precedence over upstream -w/-c. - Storage perfdata gets `;;;0;` (value;warn;crit;min;max) so Graphite-backed forecast plugins can compute 'days until full'. Backwards-compatible. _parse_pct accepts '10', '10%', ' 5 % '. Patch from Imatic IT; 6 threshold scenarios verified locally. --- check_synology.py | 33 ++++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/check_synology.py b/check_synology.py index b0fd393..97b7c7b 100755 --- a/check_synology.py +++ b/check_synology.py @@ -16,8 +16,10 @@ parser.add_argument("authkey", help="the auth key", type=str) parser.add_argument("privkey", help="the priv key", type=str) parser.add_argument("mode", help="the mode", type=str, choices=["load", "memory", "disk", "storage", "update", "status", "raid"]) -parser.add_argument("-w", help="warning value for selected mode", type=int) -parser.add_argument("-c", help="critical value for selected mode", type=int) +parser.add_argument("-w", help="warning value for selected mode (storage mode: PERC_USED, warn if used > N)", type=int) +parser.add_argument("-c", help="critical value for selected mode (storage mode: PERC_USED, crit if used > N)", type=int) +parser.add_argument("--warning-free", help="storage mode only: PERC_FREE, warn if free < N (matches check_disk -w convention; takes precedence over -w if both set)", type=str, default=None, dest="warning_free") +parser.add_argument("--critical-free", help="storage mode only: PERC_FREE, crit if free < N (matches check_disk -c convention; takes precedence over -c if both set)", type=str, default=None, dest="critical_free") parser.add_argument("-p", help="the snmp port", type=int, dest="port", default=161) parser.add_argument("-e", help="SNMP privacy protocol encryption", type=str, default="AES128", choices=["AES128", "DES"]) parser.add_argument("-t", help="timeout for snmp connection", type=int, default=10) @@ -32,6 +34,17 @@ mode = args.mode warning = args.w critical = args.c + +def _parse_pct(s): + """Strip optional '%' and whitespace, convert to int. None → None.""" + if s is None: + return None + return int(str(s).strip().rstrip('%').strip()) + +warning_free = _parse_pct(args.warning_free) +critical_free = _parse_pct(args.critical_free) +if (warning_free is not None and warning is not None) or (critical_free is not None and critical is not None): + print("check_synology: both -w/-c and --warning-free/--critical-free given; using free thresholds", file=sys.stderr) priv_protocol = args.e snmp_timeout = args.t snmp_retries = args.r @@ -225,15 +238,25 @@ def exitCode(): continue storage_used_percent = int(storage_used * 100 / storage_size) + storage_free_percent = 100 - storage_used_percent - if warning and warning < int(storage_used_percent): + # Free-percent (PERC_FREE) wins; falls back to upstream -w/-c PERC_USED. + if warning_free is not None: + if storage_free_percent < warning_free and state != 'CRITICAL': + state = 'WARNING' + elif warning and warning < storage_used_percent: if state != 'CRITICAL': state = 'WARNING' - if critical and critical < int(storage_used_percent): + + if critical_free is not None: + if storage_free_percent < critical_free: + state = 'CRITICAL' + elif critical and critical < storage_used_percent: state = 'CRITICAL' output += ' - free space: ' + storage_name + ' ' + str(storage_free) + ' GB (' + str(storage_used) + ' GB of ' + str(storage_size) + ' GB used, ' + str(storage_used_percent) + '%)' - perfdata += storage_name + '=' + str(storage_used) + 'c ' + # Perfdata format: value;warn;crit;min;max — max=storage_size enables disk_forecast (.max series in Graphite). + perfdata += storage_name + '=' + str(storage_used) + 'c;;;0;' + str(storage_size) + ' ' print('%s%s %s' % (state, output, perfdata)) exitCode()