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 700bd73..0000000 Binary files a/vspc_backup_checks/2.3/vspc_backup_checks-0.5.0.mkp and /dev/null differ 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 0000000..3b34f9f Binary files /dev/null and b/vspc_backup_checks/2.3/vspc_backup_checks-0.6.0.mkp differ