"""Move hypervisors from servers table to a dedicated hypervisors table. Usage: python tools/move_hypervisors.py """ import csv 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("csv_path", help="Hyperviseur CSV iTop") args = parser.parse_args() engine = create_engine(DATABASE_URL) print(f"[INFO] DB: {DATABASE_URL.split('@')[-1]}") print(f"[INFO] CSV: {args.csv_path}") with open(args.csv_path, "r", encoding="utf-8-sig", newline="") as f: reader = csv.DictReader(f) rows = list(reader) print(f"[INFO] {len(rows)} lignes CSV") conn = engine.connect().execution_options(isolation_level="AUTOCOMMIT") # Crée la table si pas existante conn.execute(text(""" CREATE TABLE IF NOT EXISTS hypervisors ( id SERIAL PRIMARY KEY, hostname citext UNIQUE NOT NULL, fqdn varchar(255), cluster_name varchar(100), underlying_server varchar(100), site varchar(100), domain_ltd varchar(50), description text, responsable_nom text, moved_from_server_id integer, created_at timestamptz DEFAULT now() ) """)) moved = 0 not_in_servers = 0 skipped = 0 for r in rows: hostname = (r.get("Nom") or r.get("Hostname") or "").strip() if not hostname or hostname in ("(null)",) or not any(c.isalpha() for c in hostname): skipped += 1 continue srv = conn.execute(text( "SELECT id, fqdn, site, domain_ltd, patch_owner_details, responsable_nom FROM servers WHERE hostname=:h" ), {"h": hostname}).fetchone() if not srv: # Pas en base servers, on insère quand même dans hypervisors try: conn.execute(text(""" INSERT INTO hypervisors (hostname, cluster_name, underlying_server, description, responsable_nom) VALUES (:h, :cl, :srv, :desc, :resp) ON CONFLICT (hostname) DO NOTHING """), { "h": hostname, "cl": (r.get("vCluster->Nom") or "").strip()[:100] or None, "srv": (r.get("Serveur->Nom") or "").strip()[:100] or None, "desc": (r.get("Description") or "").strip() or None, "resp": None, }) not_in_servers += 1 except Exception as e: print(f" [ERR insert] {hostname}: {str(e)[:120]}") skipped += 1 continue try: # Insert dans hypervisors conn.execute(text(""" INSERT INTO hypervisors (hostname, fqdn, cluster_name, underlying_server, site, domain_ltd, description, responsable_nom, moved_from_server_id) VALUES (:h, :fqdn, :cl, :usrv, :site, :domain, :desc, :resp, :sid) ON CONFLICT (hostname) DO UPDATE SET cluster_name=EXCLUDED.cluster_name, underlying_server=EXCLUDED.underlying_server, moved_from_server_id=EXCLUDED.moved_from_server_id """), { "h": hostname, "fqdn": srv.fqdn, "cl": (r.get("vCluster->Nom") or "").strip()[:100] or None, "usrv": (r.get("Serveur->Nom") or "").strip()[:100] or None, "site": srv.site, "domain": srv.domain_ltd, "desc": (r.get("Description") or srv.patch_owner_details or "").strip()[:1000] or None, "resp": srv.responsable_nom, "sid": srv.id, }) # Nettoie les FK puis delete du servers for tbl, col in [("server_ips", "server_id"), ("qualys_assets", "server_id"), ("server_audit", "server_id"), ("server_audit_full", "server_id"), ("server_correspondance", "server_id"), ("cluster_members", "server_id"), ("patch_history", "server_id"), ("patch_sessions", "server_id"), ("patch_validation", "server_id"), ("quickwin_entries", "server_id"), ("quickwin_server_config", "server_id"), ("server_specifics", "server_id"), ("server_pairs", "server_a_id"), ("server_pairs", "server_b_id")]: try: conn.execute(text(f"DELETE FROM {tbl} WHERE {col}=:sid"), {"sid": srv.id}) except Exception: pass conn.execute(text("DELETE FROM servers WHERE id=:sid"), {"sid": srv.id}) moved += 1 except Exception as e: print(f" [ERR move] {hostname}: {str(e)[:150]}") skipped += 1 conn.close() print(f"[DONE] Deplaces: {moved} | Inseres directement (pas en servers): {not_in_servers} | Ignores: {skipped}") if __name__ == "__main__": main()