patchcenter/tools/import_etat_itop.py
Admin MPCZ 753d4076c9 Migre etat vers labels iTop verbatim (Production, Nouveau, etc.)
Aligne la colonne servers.etat sur les valeurs iTop exactes au lieu
des codes lowercase internes.

Impact:
- servers.etat stocke: Production, Implémentation, Stock, Obsolète,
  EOL, prêt, tests, Nouveau, A récupérer, Cassé, Cédé, En panne,
  Perdu, Recyclé, Occasion, A détruire, Volé
- Remplace tous les 'production'/'obsolete'/'stock'/'eol'/'implementation'
  en WHERE/comparisons par les labels iTop verbatim (~10 fichiers)
- Templates badges/filtres: valeurs + labels iTop
- itop_service: maintient mapping iTop API internal code <-> DB label
- import_sanef_*: norm_etat retourne la valeur iTop verbatim ou None
  (plus de fallback silencieux sur 'production')

Ajoute:
- tools/import_etat_itop.py : migration lowercase -> iTop + re-import CSV
- tools/import_environnement.py : fix dry-run pour ADD COLUMN idempotent

Supprime:
- tools/fix_etat_extend.py (obsolete par import_etat_itop.py)
2026-04-14 18:40:56 +02:00

144 lines
5.1 KiB
Python

"""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 <csv1> [<csv2> ...] [--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()