Add support for host-specific VSPC policies, and HTTP retries to increase reliability.

This commit is contained in:
Marsell Kukuljevic 2026-06-26 16:35:47 +02:00
parent 15c9b77230
commit 78b2e3c84e
3 changed files with 75 additions and 23 deletions

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# Copyright (C) 2026 Spearhead Systems SRL # 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 datetime import datetime, timezone
from collections import defaultdict from collections import defaultdict
@ -16,6 +16,26 @@ DAILY = "Daily"
WEEKLY = "Weekly" 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. # 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 # 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 offset = 0
while True: while True:
conn.request("GET", f"{path}?offset={offset}", headers=headers) response = get_url(conn, f"{path}?offset={offset}", headers)
response = conn.getresponse()
if response.status != 200: if not response or response.status != 200:
raise Exception(f"Status code for {path} was {response.status}") raise Exception(f"Status code for {path} was {response.status}")
page = json.loads(response.read()) page = json.loads(response.read())
meta = page["meta"] meta = page.get("meta")
data = page["data"] data = page["data"]
# If not meta, we're not paginated, so return the data immediately
if not meta:
return data
results.extend(data) results.extend(data)
total = meta["pagingInfo"]["total"] 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 = {} mToB = {}
for agent in bAgents: for agent in bAgents:
mToB[agent["managementAgentUid"]] = agent mToB[agent["managementAgentUid"]] = agent
@ -228,6 +251,22 @@ def process(mAgents, bAgents, jobs, restores, policies):
critDays = 0 critDays = 0
if sched == DAILY: if sched == DAILY:
policyType = policies.get(polId) 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: if policyType == WEEKLY:
# It may seem silly to check for WEEKLY under a DAILY section, # It may seem silly to check for WEEKLY under a DAILY section,
# but the scheduling allows for this, and we actually use this # 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. # run certain backup jobs only on a Saturday.
# #
# So we need to filter through VSPC's somewhat inconsistent policy API to find # 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 # daily settings.
# 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 mergePolicies(linuxPolicies, windowsPolicies, macPolicies): def mergePolicies(linuxPolicies, windowsPolicies, macPolicies):
policies = {} policies = {}
schedules = {} schedules = {}
@ -339,19 +372,32 @@ def mergePolicies(linuxPolicies, windowsPolicies, macPolicies):
schedules[policy["instanceUid"]] = schedule schedules[policy["instanceUid"]] = schedule
for polId, sched in schedules.items(): for polId, sched in schedules.items():
dailySched = sched.get("dailyScheduleSettings") period = get_period(sched)
if not dailySched: if period:
continue policies[polId] = period
days = dailySched.get("specificDays")
if days and len(days) == 1:
policies[polId] = WEEKLY
else:
policies[polId] = DAILY
return policies 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(): def print_demo():
print(""" print("""
<<<<newveeam>>>> <<<<newveeam>>>>
@ -401,6 +447,11 @@ def main(argv=None):
if args.demo: if args.demo:
return print_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) 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) 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) 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) 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) print_out(results)

Binary file not shown.