"""Lie qualys_assets a servers via IP quand le hostname ne match pas. Cas d'usage : Qualys enregistre l'asset sous un display name (ex 'node3') alors que le vrai serveur dans iTop/PatchCenter s'appelle differemment (ex 'vdameasxt3'). On retombe sur le matching par IP via server_ips. Ne touche QUE les qualys_assets dont server_id est NULL. Usage: python tools/link_qualys_by_ip.py [--dry-run] """ import os import argparse from sqlalchemy import create_engine, text DATABASE_URL = os.getenv("DATABASE_URL_DEMO") or os.getenv("DATABASE_URL") \ or "postgresql://patchcenter:PatchCenter2026!@localhost:5432/patchcenter_demo" def main(): parser = argparse.ArgumentParser() parser.add_argument("--dry-run", action="store_true") args = parser.parse_args() engine = create_engine(DATABASE_URL) print(f"[INFO] DB: {DATABASE_URL.split('@')[-1]}") conn = engine.connect().execution_options(isolation_level="AUTOCOMMIT") # Recupere les qualys_assets sans lien serveur, avec une IP rows = conn.execute(text(""" SELECT id, qualys_asset_id, hostname, name, fqdn, ip_address::text as ip FROM qualys_assets WHERE server_id IS NULL AND ip_address IS NOT NULL ORDER BY hostname """)).fetchall() print(f"[INFO] {len(rows)} qualys_assets sans server_id avec une IP") stats = {"linked": 0, "no_match": 0, "ambiguous": 0} no_match = [] ambiguous = [] for r in rows: # IP peut etre 'a.b.c.d' ou 'a.b.c.d/32', strip /XX ip = r.ip.split("/")[0] # Cherche server_ips matching cette IP matches = conn.execute(text(""" SELECT s.id, s.hostname FROM servers s JOIN server_ips si ON si.server_id = s.id WHERE si.ip_address = CAST(:ip AS inet) """), {"ip": ip}).fetchall() if len(matches) == 0: stats["no_match"] += 1 no_match.append((r.hostname or r.name, ip)) continue if len(matches) > 1: stats["ambiguous"] += 1 ambiguous.append((r.hostname, ip, [m.hostname for m in matches])) continue sid = matches[0].id srv_name = matches[0].hostname if args.dry_run: print(f" DRY: {r.hostname or r.name:25s} ({ip}) -> server '{srv_name}' (id={sid})") else: conn.execute(text("UPDATE qualys_assets SET server_id=:sid WHERE id=:qid"), {"sid": sid, "qid": r.id}) print(f" OK : {r.hostname or r.name:25s} ({ip}) -> '{srv_name}'") stats["linked"] += 1 conn.close() print(f"\n[DONE] Lies: {stats['linked']} | Sans match IP: {stats['no_match']} | Ambigus: {stats['ambiguous']}") if ambiguous: print("\n[WARN] Cas ambigus (plusieurs servers ont la meme IP) :") for h, ip, srvs in ambiguous[:10]: print(f" {h} ({ip}) -> {srvs}") if __name__ == "__main__": main()