"""Migration etat: lowercase codes -> labels iTop verbatim + import CSV. Aligne la colonne servers.etat sur les valeurs iTop exactes: Lifecycle: Production, Implémentation, Stock, Obsolète, EOL, prêt, tests Condition: Nouveau, A récupérer, Cassé, Cédé, En panne, Perdu, Recyclé, Occasion, A détruire, Volé Etapes: 1. DROP ancien CHECK, UPDATE lowercase -> iTop verbatim 2. ADD nouveau CHECK avec labels iTop 3. Relecture CSV pour re-synchroniser depuis iTop Usage: python tools/import_etat_itop.py [ ...] [--dry-run] """ import os import csv 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" ITOP_ETATS = [ # Lifecycle "Production", "Implémentation", "Stock", "Obsolète", "EOL", "prêt", "tests", # Condition "Nouveau", "A récupérer", "Cassé", "Cédé", "En panne", "Perdu", "Recyclé", "Occasion", "A détruire", "Volé", ] # Migration valeurs existantes (lowercase) -> labels iTop verbatim LEGACY_MAP = { "production": "Production", "implementation": "Implémentation", "stock": "Stock", "obsolete": "Obsolète", "eol": "EOL", "pret": "prêt", "tests": "tests", "nouveau": "Nouveau", "casse": "Cassé", "cede": "Cédé", "en_panne": "En panne", "a_recuperer": "A récupérer", "perdu": "Perdu", "recycle": "Recyclé", "occasion": "Occasion", "a_detruire": "A détruire", "vole": "Volé", } def norm_etat(raw): if not raw: return None s = raw.strip() if not s or s in ("-", "(null)"): return None if s in ITOP_ETATS: return s # Tolerance case-insensitive + sans accent pour les alias courants low = s.lower() if low in LEGACY_MAP: return LEGACY_MAP[low] return None def main(): parser = argparse.ArgumentParser() parser.add_argument("csv_paths", nargs="*", default=[]) 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") # 1. Drop CHECK + migrate legacy values print("[INFO] Drop ancien CHECK...") if not args.dry_run: conn.execute(text("ALTER TABLE servers DROP CONSTRAINT IF EXISTS servers_etat_check")) print("[INFO] Migration lowercase -> iTop verbatim...") for old, new in LEGACY_MAP.items(): cnt = conn.execute(text("SELECT COUNT(*) FROM servers WHERE etat=:o"), {"o": old}).scalar() if cnt: print(f" {old:18s} -> {new:18s} : {cnt}") if not args.dry_run: conn.execute(text("UPDATE servers SET etat=:n WHERE etat=:o"), {"n": new, "o": old}) # 2. CHECK iTop verbatim allowed_sql = ", ".join(f"'{v}'" for v in ITOP_ETATS) if not args.dry_run: conn.execute(text( f"ALTER TABLE servers ADD CONSTRAINT servers_etat_check " f"CHECK (etat IN ({allowed_sql}) OR etat IS NULL)" )) print(f"[INFO] CHECK iTop applique: {ITOP_ETATS}") # 3. Re-sync CSV if args.csv_paths: updated = unchanged = not_found = 0 unknown = set() for csv_path in args.csv_paths: print(f"\n[INFO] Lecture {csv_path}") with open(csv_path, "r", encoding="utf-8-sig", newline="") as f: sample = f.read(4096); f.seek(0) delim = ";" if sample.count(";") > sample.count(",") else "," rows = list(csv.DictReader(f, delimiter=delim)) print(f"[INFO] {len(rows)} lignes (delim={delim!r})") for r in rows: hostname = (r.get("Nom") or r.get("Hostname") or "").strip() if not hostname or not any(c.isalpha() for c in hostname): continue raw = (r.get("Etat") or r.get("État") or "").strip() new_etat = norm_etat(raw) if raw and new_etat is None and raw not in ("-", "(null)"): unknown.add(raw); continue if new_etat is None: continue srv = conn.execute(text("SELECT id, etat FROM servers WHERE hostname=:h"), {"h": hostname}).fetchone() if not srv: not_found += 1; continue if srv.etat == new_etat: unchanged += 1; continue if args.dry_run: print(f" DRY: {hostname} {srv.etat} -> {new_etat}") else: conn.execute(text("UPDATE servers SET etat=:e WHERE id=:sid"), {"e": new_etat, "sid": srv.id}) updated += 1 print(f"\n[DONE] Maj CSV: {updated} | Inchanges: {unchanged} | Hors base: {not_found}") if unknown: print(f"[WARN] Etats iTop non reconnus: {unknown}") conn.close() print("[OK] Migration terminee") if __name__ == "__main__": main()