Files
claudetools/.claude/skills/owncloud/scripts/owncloud.py
Howard Enos fa4867580f sync: auto-sync from HOWARD-HOME at 2026-07-05 15:29:59
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-05 15:29:59
2026-07-05 15:30:28 -07:00

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())