#!/usr/bin/env python3 """Qdrant collection audit + cleanup for test_col_* artifacts. T15 (G14 FIX) - Qdrant accumulates test_col_* collections from integration tests that forget to clean up. This script audits and cleans them. Usage: # Audit only (safe - no changes): python scripts/ops/qdrant_audit.py python scripts/ops/qdrant_audit.py --audit-only # Audit + cleanup (drops test_col_*): python scripts/ops/qdrant_audit.py --cleanup # Custom Qdrant URL: python scripts/ops/qdrant_audit.py --url http://qdrant.example:6333 --cleanup # CI gate mode (exit 1 if any test_col_* found): python scripts/ops/qdrant_audit.py --check Cron suggestion (netcup): # Daily audit, notify if anything leaked: 0 6 * * * python3 /root/scripts/qdrant_audit.py --audit-only | \\ curl -s --data-binary @- http://localhost:9080/rmi-info || true """ from __future__ import annotations import argparse import json import sys import urllib.request def fetch_collections(base_url: str) -> list[dict]: with urllib.request.urlopen(f"{base_url}/collections", timeout=10) as r: return json.loads(r.read()).get("result", {}).get("collections", []) def fetch_collection_info(base_url: str, name: str) -> dict: try: with urllib.request.urlopen( f"{base_url}/collections/{name}", timeout=5 ) as r: return json.loads(r.read()).get("result", {}) except Exception as e: return {"error": str(e)} def delete_collection(base_url: str, name: str) -> bool: req = urllib.request.Request( f"{base_url}/collections/{name}", method="DELETE" ) try: with urllib.request.urlopen(req, timeout=10) as r: return json.loads(r.read()).get("status") == "ok" except Exception: return False def main() -> int: p = argparse.ArgumentParser(description=__doc__) p.add_argument( "--url", default="http://localhost:6333", help="Qdrant base URL (default: http://localhost:6333)", ) p.add_argument( "--audit-only", action="store_true", help="Only audit - never delete anything", ) p.add_argument( "--cleanup", action="store_true", help="Audit + drop all test_col_* collections", ) p.add_argument( "--check", action="store_true", help="CI mode: exit 1 if any test_col_* found (no output unless found)", ) args = p.parse_args() cols = fetch_collections(args.url) test_like = [c["name"] for c in cols if c["name"].startswith("test_col")] production = [c["name"] for c in cols if not c["name"].startswith("test_col")] if args.check: if test_like: print( f"FAIL: {len(test_like)} test_col_* collection(s) found: " f"{test_like}", file=sys.stderr, ) return 1 return 0 print(f"=== Qdrant Audit: {args.url} ===") print(f"Total collections: {len(cols)}") print(f" Production: {len(production)}") print(f" Test artifacts (test_col_*): {len(test_like)}") if not test_like: print("\nCLEAN - zero test_col_* artifacts.") return 0 print("\n--- Test artifacts detail ---") for name in test_like: info = fetch_collection_info(args.url, name) points = info.get("points_count", "?") vectors_size = ( info.get("config", {}).get("params", {}).get("vectors", {}).get("size", "?") ) print(f" {name}: {points} points, {vectors_size}-dim vectors") if args.audit_only: print(f"\n(Use --cleanup to drop these {len(test_like)} collection(s))") return 0 if args.cleanup: print("\n--- Cleanup ---") dropped = 0 for name in test_like: if delete_collection(args.url, name): print(f" ✓ dropped {name}") dropped += 1 else: print(f" ✗ FAILED to drop {name}") print(f"\n{dropped}/{len(test_like)} dropped.") return 0 if dropped == len(test_like) else 1 print("\n(Use --cleanup to drop these, or --audit-only to suppress this hint)") return 0 if __name__ == "__main__": raise SystemExit(main())