Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6d36b4e958 | |||
| 90c6c80843 |
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
|
|||||||
import json, os, sys, time, urllib.request, urllib.parse, urllib.error
|
import json, os, re, sys, time, urllib.request, urllib.parse, urllib.error
|
||||||
|
|
||||||
|
|
||||||
RETRIES = 3
|
RETRIES = 3
|
||||||
@@ -70,6 +70,28 @@ RANGE_FILTERS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Version bounds. The API matches a version only exactly, and a range lookup it
|
||||||
|
# does not support would be dropped without a word — so these are compared here,
|
||||||
|
# number by number (4.10 is above 4.9, which a string comparison denies).
|
||||||
|
VERSION_BOUNDS = {
|
||||||
|
"version_lt": lambda v, t: v < t,
|
||||||
|
"version_lte": lambda v, t: v <= t,
|
||||||
|
"version_gt": lambda v, t: v > t,
|
||||||
|
"version_gte": lambda v, t: v >= t,
|
||||||
|
}
|
||||||
|
PAGE = 500
|
||||||
|
|
||||||
|
|
||||||
|
def parse_version(v):
|
||||||
|
parts = re.findall(r"\d+", str(v or ""))
|
||||||
|
return tuple(int(x) for x in parts) if parts else None
|
||||||
|
|
||||||
|
|
||||||
|
def pad(a, b):
|
||||||
|
n = max(len(a), len(b))
|
||||||
|
return a + (0,) * (n - len(a)), b + (0,) * (n - len(b))
|
||||||
|
|
||||||
|
|
||||||
def to_bool(v):
|
def to_bool(v):
|
||||||
if isinstance(v, bool):
|
if isinstance(v, bool):
|
||||||
return v
|
return v
|
||||||
@@ -86,7 +108,13 @@ def main():
|
|||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
}
|
}
|
||||||
# === per-command logic ===
|
# === per-command logic ===
|
||||||
filters = {}
|
# An instance bound to one tenant of a shared, multi-tenant manager carries it
|
||||||
|
# in its config: it scopes the count, and the fleet total beside it, unless the
|
||||||
|
# block names a tenant itself.
|
||||||
|
scope = {}
|
||||||
|
if not str(inputs.get("tenant") or "").strip() and str(secrets.get("tenant") or "").strip():
|
||||||
|
scope["tenant"] = str(secrets["tenant"]).strip()
|
||||||
|
filters = dict(scope)
|
||||||
for field in EXACT_FILTERS:
|
for field in EXACT_FILTERS:
|
||||||
value = inputs.get(field)
|
value = inputs.get(field)
|
||||||
if value is None or value == "":
|
if value is None or value == "":
|
||||||
@@ -103,17 +131,48 @@ def main():
|
|||||||
if len(kv) == 2 and kv[0].strip():
|
if len(kv) == 2 and kv[0].strip():
|
||||||
filters[kv[0].strip()] = kv[1]
|
filters[kv[0].strip()] = kv[1]
|
||||||
|
|
||||||
|
bounds = {}
|
||||||
|
for field in VERSION_BOUNDS:
|
||||||
|
value = str(inputs.get(field) or "").strip()
|
||||||
|
if value:
|
||||||
|
target = parse_version(value)
|
||||||
|
if target is None:
|
||||||
|
raise ValueError(field + " must be a version such as 4.10.2, got: " + repr(value))
|
||||||
|
bounds[field] = target
|
||||||
|
|
||||||
# The agent list is a DRF paginated collection: `count` is the total number
|
# The agent list is a DRF paginated collection: `count` is the total number
|
||||||
# of matches, independent of the page size. limit=1 keeps the payload tiny.
|
# of matches, independent of the page size. limit=1 keeps the payload tiny.
|
||||||
agents = base + "/api/data/endpoint/Agent/"
|
agents = base + "/api/data/endpoint/Agent/"
|
||||||
|
if bounds:
|
||||||
|
# A version bound reads every agent the other filters keep and counts
|
||||||
|
# here. An agent with no readable version is left out and counted apart,
|
||||||
|
# rather than guessed above or below the bound.
|
||||||
|
count, scanned, unreadable, offset = 0, 0, 0, 0
|
||||||
|
while True:
|
||||||
|
page = request("GET", agents + qs(dict(filters, limit=PAGE, offset=offset)), headers)
|
||||||
|
rows = page.get("results") or []
|
||||||
|
for agent in rows:
|
||||||
|
scanned += 1
|
||||||
|
version = parse_version(agent.get("version"))
|
||||||
|
if version is None:
|
||||||
|
unreadable += 1
|
||||||
|
continue
|
||||||
|
if all(VERSION_BOUNDS[f](*pad(version, t)) for f, t in bounds.items()):
|
||||||
|
count += 1
|
||||||
|
offset += len(rows)
|
||||||
|
if not rows or offset >= page.get("count", 0):
|
||||||
|
break
|
||||||
|
result = {"count": count, "filters": dict(filters, **{f: inputs[f] for f in bounds}),
|
||||||
|
"scanned": scanned, "unreadable_version": unreadable}
|
||||||
|
else:
|
||||||
matched = request("GET", agents + qs(dict(filters, limit=1, offset=0)), headers)
|
matched = request("GET", agents + qs(dict(filters, limit=1, offset=0)), headers)
|
||||||
result = {"count": matched.get("count", 0), "filters": filters}
|
result = {"count": matched.get("count", 0), "filters": filters}
|
||||||
|
|
||||||
# The API silently drops a query parameter it does not recognise as a
|
# The API silently drops a query parameter it does not recognise as a
|
||||||
# filter, which yields the unfiltered total dressed up as a filtered count.
|
# filter, which yields the unfiltered total dressed up as a filtered count.
|
||||||
# Returning the fleet total alongside makes that failure visible.
|
# Returning the fleet total alongside makes that failure visible.
|
||||||
if filters:
|
if filters != scope or bounds:
|
||||||
everything = request("GET", agents + qs({"limit": 1, "offset": 0}), headers)
|
everything = request("GET", agents + qs(dict(scope, limit=1, offset=0)), headers)
|
||||||
result["total"] = everything.get("count", 0)
|
result["total"] = everything.get("count", 0)
|
||||||
else:
|
else:
|
||||||
result["total"] = result["count"]
|
result["total"] = result["count"]
|
||||||
|
|||||||
Reference in New Issue
Block a user