Add support for host-specific VSPC policies, and HTTP retries to increase reliability.
This commit is contained in:
parent
15c9b77230
commit
78b2e3c84e
@ -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.
BIN
vspc_backup_checks/2.3/vspc_backup_checks-0.6.0.mkp
Executable file
BIN
vspc_backup_checks/2.3/vspc_backup_checks-0.6.0.mkp
Executable file
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user