"""Check nodes for an outdated Barbara Core, and optionally update them. Barbara Core is the single versioned package that bundles a node's OS and Node Manager together (e.g. "Barbara Core 1.10.1.471") — Panel's own update modal shows one version number, not two separate firmwares. Demonstrates: - Reading a field not yet exposed by a typed model (``deviceVersion``) from a node's raw payload. - Using ``client.nodes.update_barbara_core(...)`` to send the update — the same resource method Panel's "Barbara Core Update" button calls. Update status comes from ``deviceVersion.lastVersion.updateAvailable`` on the node. Sending ``update_type="update"`` takes no other body: the panel pushes whatever it currently reports as the latest version. Usage: python check_barbara_core_updates.py # every node in your company python check_barbara_core_updates.py [ ...] python check_barbara_core_updates.py --apply Without --apply, this only reports which nodes need an update (read-only). With --apply, it sends the update to every node that needs one. Credentials are read from environment variables (BBR_API_CLIENT_ID / BBR_API_CLIENT_SECRET / BBR_API_USERNAME / BBR_API_PASSWORD), optionally loaded from a .env file first — see load_dotenv() below and README.md. """ from __future__ import annotations import argparse import json import os import re from typing import Any from barbara import BarbaraApiError, BarbaraAuthError, BarbaraClient from barbara.models import Node INTERNAL_ID_RE = re.compile(r"^[0-9a-f]{24}$") def load_dotenv(path: str) -> None: """Minimal .env loader (no extra dependency). Variables already set in the environment always take precedence over the file. """ if not os.path.isfile(path): return with open(path, encoding="utf-8") as fh: for line in fh: line = line.strip() if not line or line.startswith("#") or "=" not in line: continue key, _, value = line.partition("=") os.environ.setdefault(key.strip(), value.strip().strip('"').strip("'")) def resolve_node(client: BarbaraClient, identifier: str) -> Node: if INTERNAL_ID_RE.match(identifier): return client.nodes.get(identifier) return client.nodes.resolve(identifier) def all_nodes(client: BarbaraClient) -> list[Node]: """Every node in the company, paging through results — a plain ``client.nodes.list()`` call only returns one page. """ nodes: list[Node] = [] offset = 0 size = 100 while True: page = client.nodes.paginate(size=size, from_=offset) nodes.extend(page.items) if not page.items or page.total is None or len(nodes) >= page.total: break offset += size return nodes def evaluate_node(node: Node) -> dict[str, Any]: """Barbara Core update status for one node, derived from its raw payload.""" version = node.raw.get("deviceVersion") or {} last = version.get("lastVersion") or {} current_applied = ((version.get("current") or {}).get("applied") or {}).get("config") or {} return { "nodeId": node.id, "nodeName": node.node_name, "needsUpdate": bool(last.get("updateAvailable")), "currentOsVersion": (current_applied.get("os") or {}).get("version"), "latestOsVersion": (last.get("os") or {}).get("version"), "currentAgentVersion": (current_applied.get("agent") or {}).get("version"), "latestAgentVersion": (last.get("agent") or {}).get("version"), } def send_update(client: BarbaraClient, node_id: str) -> None: client.nodes.update_barbara_core(node_id, "update") def main() -> None: parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) parser.add_argument( "nodes", nargs="*", help="Node names (deviceName) or internal _ids (default: every node in your company)", ) parser.add_argument("--apply", action="store_true", help="Send the update to outdated nodes") parser.add_argument( "--env-file", default=".env", help="Path to a .env file with Barbara API Credentials" ) args = parser.parse_args() load_dotenv(args.env_file) with BarbaraClient.from_env() as client: try: client.api_version() except BarbaraAuthError as exc: raise SystemExit(f"Login failed — check your Barbara API Credentials: {exc}") print("Login OK.") nodes = ( [resolve_node(client, d) for d in args.nodes] if args.nodes else all_nodes(client) ) results = [] for node in nodes: result = evaluate_node(node) if not result["needsUpdate"]: result["status"] = "up-to-date" elif not args.apply: result["status"] = "dry-run" else: try: send_update(client, node.id) result["status"] = "updated" except BarbaraApiError as exc: result["status"] = "error" result["error"] = str(exc) results.append(result) print(json.dumps(results, indent=2)) if any(r["status"] == "error" for r in results): raise SystemExit(1) if __name__ == "__main__": main()