patchcenter/tools/import_sanef_physical.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

153 lines
5.7 KiB
Python

"""Import SANEF Serveurs physiques + Hyperviseurs CSV -> table servers (machine_type=physical).
Usage:
python tools/import_sanef_physical.py <csv_path> [--hypervisor]
Le script auto-détecte le type via la colonne 'Sous-classe de CI'.
Marque les hyperviseurs avec is_hypervisor (via patch_owner_details si pas de colonne dédiée).
Skip les hostnames déjà présents (évite doublons avec les VMs).
"""
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 norm_os_family(famille):
f = (famille or "").strip().lower()
if "windows" in f:
return "windows"
if "linux" in f or "oracle" in f or "esxi" in f:
return "linux"
return None
ITOP_ETATS = {
"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é",
}
def norm_etat(status, etat):
"""Retourne la valeur iTop verbatim si reconnue, sinon None (pas de fallback silencieux)."""
raw = (etat or "").strip()
if not raw or raw == "-":
return None
return raw if raw in ITOP_ETATS else None
def main():
parser = argparse.ArgumentParser()
parser.add_argument("csv_path")
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")
conn = engine.connect().execution_options(isolation_level="AUTOCOMMIT")
inserted = 0
skipped = 0
duplicates = 0
for r in rows:
hostname = (r.get("Nom") or r.get("Hostname") or "").strip()
if not hostname or hostname in ("(null)",) or hostname.startswith(("...", "#")):
skipped += 1
continue
# Ignore les lignes parasites (chiffres seuls, etc.)
if not any(c.isalpha() for c in hostname):
skipped += 1
continue
# Skip si hostname déjà présent (déjà importé comme VM)
existing = conn.execute(text("SELECT id FROM servers WHERE hostname=:h"),
{"h": hostname}).fetchone()
if existing:
duplicates += 1
continue
sous_classe = (r.get("Sous-classe de CI") or "").strip()
is_hypervisor = "Hyperviseur" in sous_classe or "Serveur" in r.get("Sous-classe de CI", "") and "vir" in hostname.lower()
os_family = norm_os_family(r.get("Famille OS->Nom"))
os_version = (r.get("Version OS->Nom") or "").strip()[:200] or None
ip = (r.get("IP") or "").strip() or None
domain = (r.get("Domaine") or "").strip()[:50] or None
site = (r.get("Lieu->Nom") or "").strip()[:50] or None
etat = norm_etat(r.get("Status"), r.get("Etat"))
responsable = (r.get("Logiciel->Responsable Domaine DTS") or "").strip() or None
marque = (r.get("Marque->Nom") or "").strip()
modele = (r.get("Modèle->Nom") or r.get("Mod\u00e8le->Nom") or "").strip()
underlying = (r.get("Serveur->Nom") or "").strip()
cluster = (r.get("vCluster->Nom") or "").strip()
desc = (r.get("Description") or "").strip()
fonction = (r.get("Fonction") or "").strip()
details_parts = []
if is_hypervisor:
details_parts.append("[HYPERVISEUR]")
if marque or modele:
details_parts.append(f"{marque} {modele}".strip())
if underlying:
details_parts.append(f"Serveur: {underlying}")
if cluster:
details_parts.append(f"Cluster: {cluster}")
if desc:
details_parts.append(desc)
if fonction:
details_parts.append(fonction)
details = " | ".join(details_parts)[:2000] or None
fqdn = None
if domain and "." in domain and domain.lower() not in ("workgroup", "host"):
fqdn = f"{hostname}.{domain}"[:255]
try:
conn.execute(text("""
INSERT INTO servers
(hostname, fqdn, domain_ltd, os_family, os_version, machine_type,
etat, site, responsable_nom, patch_owner_details)
VALUES
(:hostname, :fqdn, :domain_ltd, :os_family, :os_version, 'physical',
:etat, :site, :responsable_nom, :patch_owner_details)
"""), {
"hostname": hostname, "fqdn": fqdn, "domain_ltd": domain,
"os_family": os_family, "os_version": os_version,
"etat": etat, "site": site, "responsable_nom": responsable,
"patch_owner_details": details,
})
inserted += 1
if ip:
sid = conn.execute(text("SELECT id FROM servers WHERE hostname=:h"),
{"h": hostname}).fetchone()
if sid:
try:
conn.execute(text("""
INSERT INTO server_ips (server_id, ip_address, is_primary)
VALUES (:sid, :ip, true)
"""), {"sid": sid.id, "ip": ip})
except Exception:
pass
except Exception as e:
print(f" [ERR] {hostname}: {str(e)[:150]}")
skipped += 1
conn.close()
print(f"[DONE] Inseres: {inserted} | Doublons (deja en base): {duplicates} | Ignores: {skipped}")
if __name__ == "__main__":
main()