From 5eb729304d89f042fb7d93744921d5bb6ace4af1 Mon Sep 17 00:00:00 2001 From: Marsell Kukuljevic Date: Wed, 8 Jul 2026 11:10:18 +0200 Subject: [PATCH] Tighten up messaging and checks for VSPC, to eliminate situations where data has temporarily disappeared from parts of the VSPC API. --- .../libexec/agent_vspc_backup_checks | 51 ++++++++++++++---- .../2.3/vspc_backup_checks-0.6.0.mkp | Bin 6709 -> 0 bytes .../2.3/vspc_backup_checks-0.6.2.mkp | Bin 0 -> 6956 bytes 3 files changed, 42 insertions(+), 9 deletions(-) delete mode 100755 vspc_backup_checks/2.3/vspc_backup_checks-0.6.0.mkp create mode 100755 vspc_backup_checks/2.3/vspc_backup_checks-0.6.2.mkp diff --git a/vspc_backup_checks/2.3/local/lib/python3/cmk_addons/plugins/vspc_backup_checks/libexec/agent_vspc_backup_checks b/vspc_backup_checks/2.3/local/lib/python3/cmk_addons/plugins/vspc_backup_checks/libexec/agent_vspc_backup_checks index 319350b..ce263ba 100755 --- a/vspc_backup_checks/2.3/local/lib/python3/cmk_addons/plugins/vspc_backup_checks/libexec/agent_vspc_backup_checks +++ b/vspc_backup_checks/2.3/local/lib/python3/cmk_addons/plugins/vspc_backup_checks/libexec/agent_vspc_backup_checks @@ -36,6 +36,23 @@ def get_url(conn, path, headers): return +# Try to connect; if fail retry using exponential backoff. Returns boolean if +# connect succeeded. This isn't the best way to handle things, but it is +# minimally invasive. +def connect(conn): + delay = 2 + + while delay <= 64: + try: + conn.connect() + return True + except: + time.sleep(delay) + delay *= 2 + + return False + + # GET HTTP with Bearer auth. Returns data structure parsed from JSON. # # Since results in VSPC are paginated, we go through all pages and return an @@ -52,6 +69,10 @@ def get_paginated_json_url(host, port, path, token, insecure): conn = http.client.HTTPSConnection(host, port=port, timeout=10, context=ctx) headers = { "Authorization": f"Bearer {token}" } + connected = connect(conn) + if not connected: + raise ConnectionError("Failed to connect to VSPC") + results = [] offset = 0 @@ -68,6 +89,7 @@ def get_paginated_json_url(host, port, path, token, insecure): # If not meta, we're not paginated, so return the data immediately if not meta: + conn.close() return data results.extend(data) @@ -76,7 +98,7 @@ def get_paginated_json_url(host, port, path, token, insecure): count = meta["pagingInfo"]["count"] offset = offset + count - if offset >= total: + if offset >= total or count == 0: break conn.close() @@ -173,6 +195,7 @@ def process(mAgents, bAgents, jobs, restores, policies, get_customized_policy): bToR[bId][jId] = r results = defaultdict(list) + for mAgent in mAgents: host = mAgent["tag"] or mAgent["hostName"] mAgentId = mAgent["instanceUid"] @@ -212,12 +235,17 @@ def process(mAgents, bAgents, jobs, restores, policies, get_customized_policy): rEntries = bToR.get(bAgentId, {}) - jobs = bToJ.get(bAgentId) - if not jobs: + bJobs = bToJ.get(bAgentId) + if not bJobs: + results[host].append({ + "status": WARN, + "message": f"Backup agent {bAgentId} job query returned empty." + }) continue - for job in jobs: + for job in bJobs: jobId = job["instanceUid"] + oriId = job["originalUid"] last = job["lastEndTime"] sched = job["scheduleType"] polId = job["backupPolicyUid"] @@ -316,8 +344,12 @@ def process(mAgents, bAgents, jobs, restores, policies, get_customized_policy): "message": f"Backup agent {bAgentId} job {jobId} is healthy; last backup ran {daysSinceLastRun:.1f} days ago." }) - rEntry = rEntries.get(jobId) + rEntry = rEntries.get(oriId) if not rEntry: + results[host].append({ + "status": WARN, + "message": f"Backup agent {bAgentId} job {jobId} has no restore points." + }) continue lastRestorePoint = rEntry["creationDate"] @@ -327,17 +359,18 @@ def process(mAgents, bAgents, jobs, restores, policies, get_customized_policy): if daysSinceLastSuccess > critDays: results[host].append({ "status": CRIT, - "message": f"Job {jobId} last SUCCESSFULLY ran {daysSinceLastSuccess:.1f} days ago!" + "message": f"Job {jobId}'s last successful restore point was {daysSinceLastSuccess:.1f} days ago!" }) elif daysSinceLastSuccess > warnDays: results[host].append({ "status": WARN, - "message": f"Job {jobId} last SUCCESSFULLY ran {daysSinceLastSuccess:.1f} days ago!" + "message": f"Job {jobId}'s last successful restore point was {daysSinceLastSuccess:.1f} days ago!" }) - else: + # To prevent noise in output, we only print this message if there were no others until now + elif len(results[host]) == 0: results[host].append({ "status": OK, - "message": f"Job {jobId} last successfully ran {daysSinceLastSuccess:.1f} days ago." + "message": f"Job {jobId}'s last successful restore point was {daysSinceLastSuccess:.1f} days ago." }) return results diff --git a/vspc_backup_checks/2.3/vspc_backup_checks-0.6.0.mkp b/vspc_backup_checks/2.3/vspc_backup_checks-0.6.0.mkp deleted file mode 100755 index 3b34f9f8302db6fa8b8845d3a73d6af4e7590a73..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6709 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 ai)Cz{f0X`TJXACJ!f4$^$^sMu0RICG5m})C literal 0 HcmV?d00001