Add align_from_ayoub: alignement servers depuis Excel Planning Patching
Lit la sheet 'Serveurs patchables 2026' et met a jour pour chaque hostname : - domaine (nouvelle colonne text iTop verbatim, ajoutee si absente) - environnement (override si non vide) - responsable_nom (Responsable Domaine DTS) - referent_nom (Referent technique) Pas de jointure via domain_env_id: stockage plain-text aligne sur iTop. Dry-run affiche les diff avant/apres.
This commit is contained in:
parent
9048a87397
commit
c16a360cdd
168
tools/align_from_ayoub.py
Normal file
168
tools/align_from_ayoub.py
Normal file
@ -0,0 +1,168 @@
|
||||
"""Alignement servers depuis fichier Excel Ayoub (Planning Patching 2026_ayoub.xlsx).
|
||||
|
||||
Lit le sheet 'Serveurs patchables 2026' et met a jour pour chaque hostname :
|
||||
- domaine (col D : Domaine)
|
||||
- environnement (col C : Environnement — override si vide)
|
||||
- responsable_nom (col H : Responsable Domaine DTS)
|
||||
- referent_nom (col J : Referent technique)
|
||||
|
||||
Ajoute la colonne servers.domaine si absente (varchar(100)).
|
||||
|
||||
Usage:
|
||||
python tools/align_from_ayoub.py <fichier.xlsx> [--sheet "Serveurs patchables 2026"] [--dry-run]
|
||||
|
||||
Requiert openpyxl: pip install openpyxl
|
||||
"""
|
||||
import os
|
||||
import argparse
|
||||
import re
|
||||
from sqlalchemy import create_engine, text
|
||||
|
||||
try:
|
||||
import openpyxl
|
||||
except ImportError:
|
||||
print("[ERR] Installer openpyxl: pip install openpyxl")
|
||||
raise
|
||||
|
||||
DATABASE_URL = os.getenv("DATABASE_URL_DEMO") or os.getenv("DATABASE_URL") \
|
||||
or "postgresql://patchcenter:PatchCenter2026!@localhost:5432/patchcenter_demo"
|
||||
|
||||
NBSP = "\u00a0"
|
||||
|
||||
|
||||
def clean(v):
|
||||
"""Normalise une cellule Excel: strip + enleve nbsp + None si vide."""
|
||||
if v is None:
|
||||
return None
|
||||
s = str(v).replace(NBSP, " ").strip()
|
||||
return s or None
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("xlsx_path")
|
||||
parser.add_argument("--sheet", default="Serveurs patchables 2026")
|
||||
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]}")
|
||||
print(f"[INFO] Fichier: {args.xlsx_path}")
|
||||
print(f"[INFO] Sheet: {args.sheet}")
|
||||
|
||||
conn = engine.connect().execution_options(isolation_level="AUTOCOMMIT")
|
||||
|
||||
# 1. Ajoute colonne domaine si absente (idempotent)
|
||||
conn.execute(text("ALTER TABLE servers ADD COLUMN IF NOT EXISTS domaine varchar(100)"))
|
||||
|
||||
# 2. Lecture Excel
|
||||
wb = openpyxl.load_workbook(args.xlsx_path, data_only=True)
|
||||
if args.sheet not in wb.sheetnames:
|
||||
print(f"[ERR] Sheet '{args.sheet}' introuvable. Sheets: {wb.sheetnames}")
|
||||
return
|
||||
ws = wb[args.sheet]
|
||||
|
||||
# Detection dynamique des colonnes via header (ligne 1)
|
||||
header = [clean(c.value) for c in ws[1]]
|
||||
print(f"[INFO] Header: {header[:12]}...")
|
||||
|
||||
def col_idx(name_candidates):
|
||||
for i, h in enumerate(header):
|
||||
if h and any(cand.lower() in h.lower() for cand in name_candidates):
|
||||
return i
|
||||
return -1
|
||||
|
||||
idx_host = col_idx(["Asset Name", "Hostname", "Nom"])
|
||||
idx_env = col_idx(["Environnement"])
|
||||
idx_dom = col_idx(["Domaine"])
|
||||
idx_resp = col_idx(["Responsable Domaine"])
|
||||
idx_ref = col_idx(["Référent technique", "Referent technique"])
|
||||
|
||||
print(f"[INFO] Indices: host={idx_host} env={idx_env} dom={idx_dom} "
|
||||
f"resp={idx_resp} ref={idx_ref}")
|
||||
|
||||
if idx_host == -1:
|
||||
print("[ERR] Colonne Asset Name/Hostname introuvable")
|
||||
return
|
||||
|
||||
stats = {"updated": 0, "unchanged": 0, "not_found": 0, "skipped": 0}
|
||||
changes_detail = []
|
||||
|
||||
for row in ws.iter_rows(min_row=2, values_only=True):
|
||||
hostname = clean(row[idx_host]) if idx_host < len(row) else None
|
||||
if not hostname or not any(c.isalpha() for c in hostname):
|
||||
stats["skipped"] += 1
|
||||
continue
|
||||
|
||||
# Extract sans suffixe FQDN si present
|
||||
hostname = hostname.split(".")[0].lower()
|
||||
|
||||
fields = {}
|
||||
if idx_dom >= 0 and idx_dom < len(row):
|
||||
v = clean(row[idx_dom])
|
||||
if v:
|
||||
fields["domaine"] = v[:100]
|
||||
if idx_env >= 0 and idx_env < len(row):
|
||||
v = clean(row[idx_env])
|
||||
if v:
|
||||
fields["environnement"] = v[:50]
|
||||
if idx_resp >= 0 and idx_resp < len(row):
|
||||
v = clean(row[idx_resp])
|
||||
if v:
|
||||
# Enleve les virgules multiples, normalise espaces
|
||||
v = re.sub(r"\s+", " ", v)
|
||||
fields["responsable_nom"] = v[:200]
|
||||
if idx_ref >= 0 and idx_ref < len(row):
|
||||
v = clean(row[idx_ref])
|
||||
if v:
|
||||
v = re.sub(r"\s+", " ", v)
|
||||
fields["referent_nom"] = v[:200]
|
||||
|
||||
if not fields:
|
||||
continue
|
||||
|
||||
srv = conn.execute(text(
|
||||
"SELECT id, domaine, environnement, responsable_nom, referent_nom "
|
||||
"FROM servers WHERE hostname=:h"
|
||||
), {"h": hostname}).fetchone()
|
||||
if not srv:
|
||||
stats["not_found"] += 1
|
||||
continue
|
||||
|
||||
# Calcule diff (pour ne pas ecraser si valeur identique)
|
||||
diff = {}
|
||||
for k, v in fields.items():
|
||||
current = getattr(srv, k, None)
|
||||
if current != v:
|
||||
diff[k] = (current, v)
|
||||
|
||||
if not diff:
|
||||
stats["unchanged"] += 1
|
||||
continue
|
||||
|
||||
if args.dry_run:
|
||||
changes_detail.append((hostname, diff))
|
||||
else:
|
||||
set_clauses = ", ".join(f"{k}=:{k}" for k in diff)
|
||||
params = {k: v[1] for k, v in diff.items()}
|
||||
params["sid"] = srv.id
|
||||
conn.execute(text(f"UPDATE servers SET {set_clauses} WHERE id=:sid"), params)
|
||||
stats["updated"] += 1
|
||||
|
||||
if args.dry_run and changes_detail:
|
||||
print(f"\n[DRY-RUN] Changements ({len(changes_detail)}) :")
|
||||
for hostname, diff in changes_detail[:30]:
|
||||
print(f" {hostname}:")
|
||||
for k, (old, new) in diff.items():
|
||||
print(f" {k}: {old!r} -> {new!r}")
|
||||
if len(changes_detail) > 30:
|
||||
print(f" ... ({len(changes_detail)-30} autres)")
|
||||
|
||||
print(f"\n[DONE] Maj: {stats['updated']} | Inchanges: {stats['unchanged']} "
|
||||
f"| Hors base: {stats['not_found']} | Skip: {stats['skipped']}")
|
||||
|
||||
conn.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Reference in New Issue
Block a user