170 lines
6.2 KiB
Python
170 lines
6.2 KiB
Python
#!/usr/bin/env python3
|
|
"""CLI for the `owncloud` skill - read-only ownCloud 10.16 inventory for the GPS
|
|
backup audit (server: cloud.acghosting.com / 172.16.3.22, driven via occ over SSH).
|
|
|
|
Commands:
|
|
status SSH ok + occ version / edition
|
|
users [--json] all users + display name, enabled, quota, used, last login
|
|
usage [--json] per-user rollup: used GB (desc) + quota + last login (audit view)
|
|
audit [--client NAME] [--rmm] [--json]
|
|
active users holding data; with --rmm cross-check GuruRMM
|
|
endpoints for the ownCloud desktop client (scope with --client)
|
|
|
|
Server-side (occ) is the source of "who has an account + how much data"; --rmm adds
|
|
the "which enrolled machine actually runs the ownCloud client" half.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
from owncloud_client import OwncloudClient # noqa: E402
|
|
import backup_common as bc # noqa: E402
|
|
|
|
GB = 1_000_000_000
|
|
|
|
|
|
def _gb(n) -> float:
|
|
try:
|
|
return round(int(n) / GB, 2)
|
|
except (TypeError, ValueError):
|
|
return 0.0
|
|
|
|
|
|
def cmd_status(c: OwncloudClient, args) -> int:
|
|
s = c.server_status()
|
|
print(f"[OK] {c.host}")
|
|
print(json.dumps(s, indent=2))
|
|
return 0
|
|
|
|
|
|
def cmd_users(c: OwncloudClient, args) -> int:
|
|
users = c.list_users()
|
|
users.sort(key=lambda u: int(u.get("used_bytes") or 0), reverse=True)
|
|
if args.json:
|
|
print(json.dumps(users, indent=2))
|
|
return 0
|
|
print(f"{'UID':20} {'NAME':22} {'EN':3} {'USED':>10} {'QUOTA':>12} {'LAST LOGIN':20}")
|
|
for u in users:
|
|
print(f"{(u.get('uid') or '')[:20]:20} "
|
|
f"{(u.get('display_name') or '')[:22]:22} "
|
|
f"{'yes' if u.get('enabled') else 'NO':3} "
|
|
f"{_gb(u.get('used_bytes')):>7} GB "
|
|
f"{str(u.get('quota') or '-')[:12]:>12} "
|
|
f"{str(u.get('last_login') or '-')[:19]:20}")
|
|
print(f"\n{len(users)} users; total used "
|
|
f"{round(sum(_gb(u.get('used_bytes')) for u in users), 1)} GB")
|
|
return 0
|
|
|
|
|
|
def _usage_rollup(c: OwncloudClient) -> list[dict]:
|
|
users = c.list_users()
|
|
rollup = []
|
|
for u in users:
|
|
rollup.append({
|
|
"uid": u.get("uid"),
|
|
"display_name": u.get("display_name"),
|
|
"email": u.get("email"),
|
|
"enabled": bool(u.get("enabled")),
|
|
"used_bytes": int(u.get("used_bytes") or 0),
|
|
"used_gb": _gb(u.get("used_bytes")),
|
|
"used_source": u.get("used_source"),
|
|
"quota": u.get("quota"),
|
|
"last_login": u.get("last_login"),
|
|
})
|
|
rollup.sort(key=lambda x: x["used_bytes"], reverse=True)
|
|
return rollup
|
|
|
|
|
|
def cmd_usage(c: OwncloudClient, args) -> int:
|
|
rollup = _usage_rollup(c)
|
|
if args.json:
|
|
print(json.dumps(rollup, indent=2))
|
|
return 0
|
|
print(f"{'UID':20} {'EN':3} {'USED':>10} {'QUOTA':>12} {'LAST LOGIN':20}")
|
|
for r in rollup:
|
|
print(f"{(r['uid'] or '')[:20]:20} "
|
|
f"{'yes' if r['enabled'] else 'NO':3} "
|
|
f"{r['used_gb']:>7} GB "
|
|
f"{str(r['quota'] or '-')[:12]:>12} "
|
|
f"{str(r['last_login'] or '-')[:19]:20}")
|
|
print(f"\n{len(rollup)} users; total used "
|
|
f"{round(sum(r['used_gb'] for r in rollup), 1)} GB")
|
|
return 0
|
|
|
|
|
|
def cmd_audit(c: OwncloudClient, args) -> int:
|
|
rollup = _usage_rollup(c)
|
|
# enabled users holding data are the ones actually backing up here
|
|
active = [r for r in rollup if r["enabled"] and r["used_bytes"] > 0]
|
|
result = {"backend": "owncloud", "server": c.host, "accounts": active}
|
|
|
|
if args.rmm:
|
|
rmm = bc.RmmClient()
|
|
rmm.login()
|
|
checks = []
|
|
agents = rmm.find_agents(client_substr=args.client) if args.client else rmm.list_agents()
|
|
for a in agents:
|
|
det = rmm.detect_client(a, ["ownCloud"])
|
|
if det.get("detected"):
|
|
checks.append(det)
|
|
result["rmm_endpoints_with_client"] = checks
|
|
|
|
if args.json:
|
|
print(json.dumps(result, indent=2))
|
|
return 0
|
|
|
|
print(f"=== ownCloud backup accounts - {c.host} ===")
|
|
print(f"{'UID':20} {'USED':>10} {'QUOTA':>12} {'LAST LOGIN':20}")
|
|
for r in active:
|
|
print(f"{(r['uid'] or '')[:20]:20} {r['used_gb']:>7} GB "
|
|
f"{str(r['quota'] or '-')[:12]:>12} {str(r['last_login'] or '-')[:19]:20}")
|
|
print(f"\n{len(active)} active accounts with data; "
|
|
f"{round(sum(r['used_gb'] for r in active), 1)} GB total")
|
|
if args.rmm:
|
|
eps = result.get("rmm_endpoints_with_client", [])
|
|
print(f"\n--- GuruRMM endpoints running an ownCloud client ({len(eps)}) ---")
|
|
for e in eps:
|
|
print(f" {e['hostname']:25} client={e.get('client_name')}")
|
|
return 0
|
|
|
|
|
|
def build_parser() -> argparse.ArgumentParser:
|
|
p = argparse.ArgumentParser(prog="owncloud", description="ownCloud backup inventory (GPS audit)")
|
|
p.add_argument("--host", help="override server host/IP (default 172.16.3.22)")
|
|
sub = p.add_subparsers(dest="cmd", required=True)
|
|
|
|
sub.add_parser("status")
|
|
u = sub.add_parser("users"); u.add_argument("--json", action="store_true")
|
|
us = sub.add_parser("usage"); us.add_argument("--json", action="store_true")
|
|
a = sub.add_parser("audit"); a.add_argument("--client"); a.add_argument("--rmm", action="store_true"); a.add_argument("--json", action="store_true")
|
|
return p
|
|
|
|
|
|
def main(argv=None) -> int:
|
|
args = build_parser().parse_args(argv)
|
|
c = OwncloudClient(args.host)
|
|
handlers = {"status": cmd_status, "users": cmd_users,
|
|
"usage": cmd_usage, "audit": cmd_audit}
|
|
try:
|
|
return handlers[args.cmd](c, args)
|
|
except bc.BackupError as exc:
|
|
print(f"[ERROR] {exc}", file=sys.stderr)
|
|
# best-effort error logging per house rule
|
|
try:
|
|
root = bc.resolve_root()
|
|
bc.subprocess.run(["bash", str(root / ".claude/scripts/log-skill-error.sh"),
|
|
"owncloud", str(exc)[:200]], timeout=15)
|
|
except Exception:
|
|
pass
|
|
return 1
|
|
finally:
|
|
c.close()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|