From 78b2e3c84ee8e02a36b5ba02195688290c9e96fa Mon Sep 17 00:00:00 2001 From: Marsell Kukuljevic Date: Fri, 26 Jun 2026 16:35:47 +0200 Subject: [PATCH] Add support for host-specific VSPC policies, and HTTP retries to increase reliability. --- .../libexec/agent_vspc_backup_checks | 98 ++++++++++++++---- .../2.3/vspc_backup_checks-0.5.0.mkp | Bin 6227 -> 0 bytes .../2.3/vspc_backup_checks-0.6.0.mkp | Bin 0 -> 6709 bytes 3 files changed, 75 insertions(+), 23 deletions(-) delete mode 100755 vspc_backup_checks/2.3/vspc_backup_checks-0.5.0.mkp create mode 100755 vspc_backup_checks/2.3/vspc_backup_checks-0.6.0.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 d083213..319350b 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 @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # Copyright (C) 2026 Spearhead Systems SRL -import http.client, argparse, json, ssl +import http.client, argparse, json, ssl, time from datetime import datetime, timezone from collections import defaultdict @@ -16,6 +16,26 @@ DAILY = "Daily" WEEKLY = "Weekly" +# Attempt to GET a URL, retrying with exponential back-off every time a 429 +# is returned. 429 is what VSPC returns when its rate limiting kicks in. +def get_url(conn, path, headers): + delay = 2 # seconds + + while delay <= 64: + conn.request("GET", path, headers=headers) + response = conn.getresponse() + + if response.status != 429: + return response + + response.read() + + time.sleep(delay) + delay *= 2 + + return + + # 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 @@ -36,17 +56,20 @@ def get_paginated_json_url(host, port, path, token, insecure): offset = 0 while True: - conn.request("GET", f"{path}?offset={offset}", headers=headers) - response = conn.getresponse() + response = get_url(conn, f"{path}?offset={offset}", headers) - if response.status != 200: + if not response or response.status != 200: raise Exception(f"Status code for {path} was {response.status}") page = json.loads(response.read()) - meta = page["meta"] + meta = page.get("meta") data = page["data"] + # If not meta, we're not paginated, so return the data immediately + if not meta: + return data + results.extend(data) total = meta["pagingInfo"]["total"] @@ -124,7 +147,7 @@ def parse_arguments(): # ], # ... # } -def process(mAgents, bAgents, jobs, restores, policies): +def process(mAgents, bAgents, jobs, restores, policies, get_customized_policy): mToB = {} for agent in bAgents: mToB[agent["managementAgentUid"]] = agent @@ -228,6 +251,22 @@ def process(mAgents, bAgents, jobs, restores, policies): critDays = 0 if sched == DAILY: policyType = policies.get(polId) + + # There's no policyType when a customized policy is used, + # since the policy is specific to a single backup agent and job. + # That means we must fetch this customized policy here. + if policyType is None: + customizedPolicy = get_customized_policy(bAgent, jobId) + jobConfig = customizedPolicy["jobConfiguration"] + customizedSched = jobConfig.get("scheduleSettings") + + if jobConfig.get("serverModeSettings"): + customizedSched = jobConfig["serverModeSettings"]["scheduleSetting"] + elif jobConfig.get("workstationModeSettings"): + customizedSched = jobConfig["workstationModeSettings"]["scheduleSetting"] + + policyType = get_period(customizedSched) + if policyType == WEEKLY: # It may seem silly to check for WEEKLY under a DAILY section, # but the scheduling allows for this, and we actually use this @@ -310,13 +349,7 @@ def process(mAgents, bAgents, jobs, restores, policies): # run certain backup jobs only on a Saturday. # # So we need to filter through VSPC's somewhat inconsistent policy API to find -# daily settings. If a job is supposed to run on exactly one day a week, we -# treat it as weekly, otherwise daily. -# -# NB: if we ever use more than one day during the week (e.g. two), the logic in -# this plugin will need to be changed. The logic here, and in process() above, -# match what we currently do in prod, since a "better" approach will require a -# lot more code for a system we might phase out for something else. +# daily settings. def mergePolicies(linuxPolicies, windowsPolicies, macPolicies): policies = {} schedules = {} @@ -339,19 +372,32 @@ def mergePolicies(linuxPolicies, windowsPolicies, macPolicies): schedules[policy["instanceUid"]] = schedule for polId, sched in schedules.items(): - dailySched = sched.get("dailyScheduleSettings") - if not dailySched: - continue - - days = dailySched.get("specificDays") - if days and len(days) == 1: - policies[polId] = WEEKLY - else: - policies[polId] = DAILY + period = get_period(sched) + if period: + policies[polId] = period return policies +# Return whether a policy schedule is DAILY or WEEKLY. If a job is supposed to +# run on exactly one day a week, we treat it as weekly, otherwise daily. +# +# NB: if we ever use more than one day during the week (e.g. two), the logic in +# this plugin will need to be changed. The logic here, and in process() above, +# match what we currently do in prod, since a "better" approach will require a +# lot more code for a system we might phase out for something else. +def get_period(sched): + dailySched = sched.get("dailyScheduleSettings") + if not dailySched: + return + + days = dailySched.get("specificDays") + if days and len(days) == 1: + return WEEKLY + else: + return DAILY + + def print_demo(): print(""" <<<>>> @@ -401,6 +447,11 @@ def main(argv=None): if args.demo: return print_demo() + def get_customized_policy(bAgent, jobId): + os = bAgent["agentPlatform"].lower() + bId = bAgent["instanceUid"] + return get_paginated_json_url(args.hostname, args.port, f"/api/v3/infrastructure/backupAgents/{os}/{bId}/jobs/{jobId}/configuration", args.token, args.insecure) + linuxPolicies = get_paginated_json_url(args.hostname, args.port, '/api/v3/configuration/backupPolicies/linux', args.token, args.insecure) windowsPolicies = get_paginated_json_url(args.hostname, args.port, '/api/v3/configuration/backupPolicies/windows', args.token, args.insecure) macPolicies = get_paginated_json_url(args.hostname, args.port, '/api/v3/configuration/backupPolicies/mac', args.token, args.insecure) @@ -412,7 +463,8 @@ def main(argv=None): policies = mergePolicies(linuxPolicies, windowsPolicies, macPolicies) - results = process(mAgents, bAgents, jobs, restores, policies) + + results = process(mAgents, bAgents, jobs, restores, policies, get_customized_policy) print_out(results) diff --git a/vspc_backup_checks/2.3/vspc_backup_checks-0.5.0.mkp b/vspc_backup_checks/2.3/vspc_backup_checks-0.5.0.mkp deleted file mode 100755 index 700bd739b85bfc9bd0cfdb88f6a6a951e4358d45..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6227 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 V{{P3^)iZi#7U(x82NVJT{|8x--faK? 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 new file mode 100755 index 0000000000000000000000000000000000000000..3b34f9f8302db6fa8b8845d3a73d6af4e7590a73 GIT binary patch literal 6709 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 p)b>rjg~c@VJ@<3|SFLPvrj^