From d8a526368ec3a516e8769ecfd6a07e5df48d0494 Mon Sep 17 00:00:00 2001 From: Admin MPCZ Date: Sat, 11 Apr 2026 13:19:10 +0200 Subject: [PATCH] =?UTF-8?q?Refonte=20synchro=20iTop=20bidirectionnelle=20c?= =?UTF-8?q?ompl=C3=A8te?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Import: typologies directement depuis iTop (Environnement, DomaineApplicatif, Zone, DomainLdap) - Import: contacts, VMs avec tous champs custom, serveurs physiques, IPs - Export: typologies + serveurs avec champs patching (tier, ssh, freq, window, excludes) - Timestamp dernière synchro import/export - Bandeau synchro visible sur TOUS les onglets du referentiel --- app/routers/referentiel.py | 7 + app/services/itop_service.py | 458 +++++++++++++++++---------------- app/templates/referentiel.html | 73 ++++-- 3 files changed, 296 insertions(+), 242 deletions(-) diff --git a/app/routers/referentiel.py b/app/routers/referentiel.py index eb05c44..89f3797 100644 --- a/app/routers/referentiel.py +++ b/app/routers/referentiel.py @@ -103,6 +103,11 @@ def referentiel_page(request: Request, db=Depends(get_db), for r in rows: zone_srv_counts[r.id] = r.cnt + # Dernière synchro + from ..services.itop_service import get_last_sync + last_sync_from = get_last_sync(db, "from") + last_sync_to = get_last_sync(db, "to") + return templates.TemplateResponse("referentiel.html", { "request": request, "user": user, "perms": perms, "can_modify": can_modify, "tab": tab, @@ -111,6 +116,8 @@ def referentiel_page(request: Request, db=Depends(get_db), "dom_srv_counts": dom_srv_counts, "env_srv_counts": env_srv_counts, "zone_srv_counts": zone_srv_counts, + "last_sync_from": last_sync_from, + "last_sync_to": last_sync_to, }) diff --git a/app/services/itop_service.py b/app/services/itop_service.py index b79e816..0a9a3ed 100644 --- a/app/services/itop_service.py +++ b/app/services/itop_service.py @@ -1,4 +1,4 @@ -"""Service iTop REST API — synchronisation bidirectionnelle""" +"""Service iTop REST API — synchronisation bidirectionnelle complète""" import logging import requests import json @@ -43,7 +43,6 @@ class ITopClient: def _upsert_ip(db, server_id, ip): - """Insert or update IP in server_ips""" if not ip: return existing = db.execute(text( @@ -58,337 +57,362 @@ def _upsert_ip(db, server_id, ip): pass -def sync_from_itop(db, itop_url, itop_user, itop_pass): - """Import complet depuis iTop: contacts, domaines, envs, zones, serveurs""" - client = ITopClient(itop_url, itop_user, itop_pass) - stats = {"contacts": 0, "domains": 0, "environments": 0, "zones": 0, - "servers_created": 0, "servers_updated": 0, "errors": []} +def _save_sync_timestamp(db, direction, stats): + """Enregistre le timestamp et les stats de la dernière synchro""" + key = f"last_sync_{direction}" + now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + val = json.dumps({"date": now, "stats": stats}) + existing = db.execute(text("SELECT key FROM settings WHERE key=:k"), {"k": key}).fetchone() + if existing: + db.execute(text("UPDATE settings SET value=:v WHERE key=:k"), {"k": key, "v": val}) + else: + db.execute(text("INSERT INTO settings (key, value) VALUES (:k, :v)"), {"k": key, "v": val}) + + +def get_last_sync(db, direction="from"): + """Récupère la date et stats de la dernière synchro""" + key = f"last_sync_{direction}" + row = db.execute(text("SELECT value FROM settings WHERE key=:k"), {"k": key}).fetchone() + if row: + try: + return json.loads(row.value) + except Exception: + pass + return None + + +# ══════════════════════════════════════════════════════════ +# IMPORT: iTop → PatchCenter +# ══════════════════════════════════════════════════════════ +def sync_from_itop(db, itop_url, itop_user, itop_pass): + """Import complet depuis iTop: typologies, contacts, serveurs""" + client = ITopClient(itop_url, itop_user, itop_pass) + stats = {"contacts": 0, "environments": 0, "domains": 0, "zones": 0, + "servers_created": 0, "servers_updated": 0, "ips": 0, "errors": []} - # Reset any failed transaction try: db.rollback() except Exception: pass - # ─── 1. Contacts (Person) ─── - persons = client.get_all("Person", "name,first_name,email,phone,org_name,function") + # ─── 1. Typologies: Environnements ─── + for item in client.get_all("Environnement", "name"): + name = item.get("name", "") + if not name: + continue + existing = db.execute(text("SELECT id FROM environments WHERE LOWER(name)=LOWER(:n)"), {"n": name}).fetchone() + if not existing: + try: + db.execute(text("INSERT INTO environments (name, code) VALUES (:n, :c)"), + {"n": name, "c": name[:10].upper().replace(" ", "").replace("-", "")}) + db.commit() + stats["environments"] += 1 + except Exception: + db.rollback() + + # ─── 2. Typologies: Domaines applicatifs ─── + for item in client.get_all("DomaineApplicatif", "name"): + name = item.get("name", "") + if not name: + continue + existing = db.execute(text("SELECT id FROM domains WHERE LOWER(name)=LOWER(:n)"), {"n": name}).fetchone() + if not existing: + try: + db.execute(text("INSERT INTO domains (name, code) VALUES (:n, :c)"), + {"n": name, "c": name[:10].upper().replace(" ", "")}) + db.commit() + stats["domains"] += 1 + except Exception: + db.rollback() + + # ─── 3. Typologies: Zones ─── + for item in client.get_all("Zone", "name"): + name = item.get("name", "") + if not name: + continue + existing = db.execute(text("SELECT id FROM zones WHERE LOWER(name)=LOWER(:n)"), {"n": name}).fetchone() + if not existing: + try: + db.execute(text("INSERT INTO zones (name, is_dmz) VALUES (:n, :d)"), + {"n": name, "d": "dmz" in name.lower()}) + db.commit() + stats["zones"] += 1 + except Exception: + db.rollback() + + # ─── 4. Typologies: DomainLdap → domain_ltd_list ─── + for item in client.get_all("DomainLdap", "name"): + name = item.get("name", "") + if not name: + continue + existing = db.execute(text("SELECT id FROM domain_ltd_list WHERE LOWER(name)=LOWER(:n)"), {"n": name}).fetchone() + if not existing: + try: + db.execute(text("INSERT INTO domain_ltd_list (name) VALUES (:n)"), {"n": name}) + db.commit() + except Exception: + db.rollback() + + # ─── 5. Contacts ─── + persons = client.get_all("Person", "name,first_name,email,phone,org_name") for p in persons: fullname = f"{p.get('first_name','')} {p.get('name','')}".strip() - existing = db.execute(text("SELECT id FROM contacts WHERE LOWER(email) = LOWER(:e)"), - {"e": p.get("email", "")}).fetchone() + email = p.get("email", "") + if not email: + continue + existing = db.execute(text("SELECT id FROM contacts WHERE LOWER(email)=LOWER(:e)"), {"e": email}).fetchone() if existing: db.execute(text("UPDATE contacts SET name=:n, updated_at=NOW() WHERE id=:id"), {"id": existing.id, "n": fullname}) else: try: db.execute(text("INSERT INTO contacts (name, email, role) VALUES (:n, :e, 'referent_technique')"), - {"n": fullname, "e": p.get("email", "")}) + {"n": fullname, "e": email}) stats["contacts"] += 1 - except Exception as e: + except Exception: db.rollback() - stats["errors"].append(f"Contact {fullname}: {e}") - # ─── 2. Domaines applicatifs (typology via VMs) ─── + # ─── 6. Build lookup maps ─── + domain_map = {r.name.lower(): r.id for r in db.execute(text("SELECT id, name FROM domains")).fetchall()} + env_map = {r.name.lower(): r.id for r in db.execute(text("SELECT id, name FROM environments")).fetchall()} + zone_map = {r.name.lower(): r.id for r in db.execute(text("SELECT id, name FROM zones")).fetchall()} + + # ─── 7. VirtualMachines ─── vms = client.get_all("VirtualMachine", "name,description,status,managementip,osfamily_id_friendlyname," "osversion_id_friendlyname,organization_name,cpu,ram," "responsable_serveur_name,responsable_domaine_name," - "environnement_name," - "domaine_applicatif_name,zone_name," - "contacts_list," - "virtualhost_name,business_criticity," + "environnement_name,domaine_applicatif_name,zone_name," + "contacts_list,virtualhost_name,business_criticity," "tier_name,connexion_method_name,ssh_user_name," "patch_frequency_name,pref_patch_jour_name,patch_window," "patch_excludes,domain_ldap_name,last_patch_date") - # Also get physical Servers - phys = client.get_all("Server", - "name,description,status,managementip,osfamily_id_friendlyname," - "osversion_id_friendlyname,organization_name,cpu,ram," - "contacts_list,location_name,brand_name,model_name") - - # Collect unique domains, envs, zones from VMs - domain_set = set() - env_set = set() - zone_set = set() - for v in vms: - if v.get("domaine_applicatif_name"): domain_set.add(v["domaine_applicatif_name"]) - if v.get("environnement_name"): env_set.add(v["environnement_name"]) - if v.get("zone_name"): zone_set.add(v["zone_name"]) - - # ─── 3. Insert domains ─── - domain_map = {} # name -> id - for d in sorted(domain_set): - existing = db.execute(text("SELECT id FROM domains WHERE LOWER(name)=LOWER(:n)"), {"n": d}).fetchone() - if existing: - domain_map[d] = existing.id - else: - db.execute(text("INSERT INTO domains (name, code) VALUES (:n, :c)"), - {"n": d, "c": d[:10].upper().replace(" ", "")}) - db.commit() - row = db.execute(text("SELECT id FROM domains WHERE name=:n"), {"n": d}).fetchone() - domain_map[d] = row.id - stats["domains"] += 1 - - # ─── 4. Insert environments ─── - env_map = {} - for e in sorted(env_set): - existing = db.execute(text("SELECT id FROM environments WHERE LOWER(name)=LOWER(:n)"), {"n": e}).fetchone() - if existing: - env_map[e] = existing.id - else: - db.execute(text("INSERT INTO environments (name, code) VALUES (:n, :c)"), - {"n": e, "c": e[:10].upper().replace(" ", "")}) - db.commit() - row = db.execute(text("SELECT id FROM environments WHERE name=:n"), {"n": e}).fetchone() - env_map[e] = row.id - stats["environments"] += 1 - - # ─── 5. Insert zones ─── - zone_map = {} - for z in sorted(zone_set): - existing = db.execute(text("SELECT id FROM zones WHERE LOWER(name)=LOWER(:n)"), {"n": z}).fetchone() - if existing: - zone_map[z] = existing.id - else: - db.execute(text("INSERT INTO zones (name, is_dmz) VALUES (:n, :d)"), - {"n": z, "d": "dmz" in z.lower()}) - db.commit() - row = db.execute(text("SELECT id FROM zones WHERE name=:n"), {"n": z}).fetchone() - zone_map[z] = row.id - stats["zones"] += 1 - - # ─── 6. Create domain_environments associations ─── - de_map = {} # (domain, env) -> id - for v in vms: - dom = v.get("domaine_applicatif_name", "") - env = v.get("environnement_name", "") - if dom and env and dom in domain_map and env in env_map: - key = (dom, env) - if key not in de_map: - did, eid = domain_map[dom], env_map[env] - existing = db.execute(text( - "SELECT id FROM domain_environments WHERE domain_id=:d AND environment_id=:e"), - {"d": did, "e": eid}).fetchone() - if existing: - de_map[key] = existing.id - else: - db.execute(text( - "INSERT INTO domain_environments (domain_id, environment_id, responsable_nom, responsable_email) VALUES (:d, :e, :rn, :re)"), - {"d": did, "e": eid, - "rn": v.get("responsable_domaine_name", ""), - "re": ""}) - db.commit() - row = db.execute(text( - "SELECT id FROM domain_environments WHERE domain_id=:d AND environment_id=:e"), - {"d": did, "e": eid}).fetchone() - de_map[key] = row.id - - # ─── 7. Insert servers (VMs) ─── - itop_status_map = {"production": "en_production", "stock": "stock", - "implementation": "en_cours", "obsolete": "decommissionne"} + itop_status = {"production": "en_production", "stock": "stock", + "implementation": "en_cours", "obsolete": "decommissionne"} for v in vms: hostname = v.get("name", "").split(".")[0].lower() if not hostname: continue - dom = v.get("domaine_applicatif_name", "") - env = v.get("environnement_name", "") - de_id = de_map.get((dom, env)) - zone_id = zone_map.get(v.get("zone_name", "")) + # Resolve domain_env_id + dom = v.get("domaine_applicatif_name", "").lower() + env = v.get("environnement_name", "").lower() + de_id = None + if dom in domain_map and env in env_map: + did, eid = domain_map[dom], env_map[env] + row = db.execute(text("SELECT id FROM domain_environments WHERE domain_id=:d AND environment_id=:e"), + {"d": did, "e": eid}).fetchone() + if row: + de_id = row.id + else: + try: + db.execute(text("INSERT INTO domain_environments (domain_id, environment_id, responsable_nom) VALUES (:d, :e, :r)"), + {"d": did, "e": eid, "r": v.get("responsable_domaine_name", "")}) + db.commit() + row = db.execute(text("SELECT id FROM domain_environments WHERE domain_id=:d AND environment_id=:e"), + {"d": did, "e": eid}).fetchone() + de_id = row.id if row else None + except Exception: + db.rollback() - existing = db.execute(text("SELECT id FROM servers WHERE LOWER(hostname)=LOWER(:h)"), - {"h": hostname}).fetchone() - - # Map tier: "Tier 0" -> "tier0" + zone_id = zone_map.get(v.get("zone_name", "").lower()) tier_raw = v.get("tier_name", "") tier = tier_raw.lower().replace(" ", "") if tier_raw else "a_definir" - - # Map connexion_method -> ssh_method ssh_method = v.get("connexion_method_name", "") or "ssh_key" - - # Map patch_frequency: "Monthly" -> "monthly" patch_freq = (v.get("patch_frequency_name", "") or "").lower() or None - - # Map pref_patch_jour/heure pref_jour = (v.get("pref_patch_jour_name", "") or "").lower() or "indifferent" pref_heure = v.get("patch_window", "") or "indifferent" vals = { - "hostname": hostname, - "fqdn": v.get("name", hostname), + "hostname": hostname, "fqdn": v.get("name", hostname), "os_family": "linux" if "linux" in v.get("osfamily_id_friendlyname", "").lower() else "windows", "os_version": v.get("osversion_id_friendlyname", ""), "machine_type": "vm", - "etat": itop_status_map.get(v.get("status", ""), "en_production"), - "de_id": de_id, - "zone_id": zone_id, + "etat": itop_status.get(v.get("status", ""), "en_production"), + "de_id": de_id, "zone_id": zone_id, "resp_srv": v.get("responsable_serveur_name", ""), "resp_dom": v.get("responsable_domaine_name", ""), "desc": v.get("description", ""), "ip": v.get("managementip", ""), - "criticity": v.get("business_criticity", ""), - "cpu": v.get("cpu", ""), - "ram": v.get("ram", ""), - "hypervisor": v.get("virtualhost_name", ""), - "tier": tier, - "ssh_method": ssh_method, + "tier": tier, "ssh_method": ssh_method, "ssh_user": v.get("ssh_user_name", "") or "root", - "patch_freq": patch_freq, - "patch_excludes": v.get("patch_excludes", ""), + "patch_freq": patch_freq, "patch_excludes": v.get("patch_excludes", ""), "domain_ltd": v.get("domain_ldap_name", ""), - "pref_jour": pref_jour, - "pref_heure": pref_heure, + "pref_jour": pref_jour, "pref_heure": pref_heure, } + existing = db.execute(text("SELECT id FROM servers WHERE LOWER(hostname)=LOWER(:h)"), {"h": hostname}).fetchone() if existing: - db.execute(text(""" - UPDATE servers SET fqdn=:fqdn, os_family=:os_family, os_version=:os_version, - etat=:etat, domain_env_id=:de_id, zone_id=:zone_id, - responsable_nom=:resp_srv, referent_nom=:resp_dom, - commentaire=:desc, tier=:tier, ssh_method=:ssh_method, - ssh_user=:ssh_user, patch_frequency=:patch_freq, - patch_excludes=:patch_excludes, domain_ltd=:domain_ltd, - pref_patch_jour=:pref_jour, pref_patch_heure=:pref_heure, - updated_at=NOW() - WHERE id=:sid - """), {**vals, "sid": existing.id}) - stats["servers_updated"] += 1 - # Update IP - if vals["ip"] and existing: + db.execute(text("""UPDATE servers SET fqdn=:fqdn, os_family=:os_family, os_version=:os_version, + etat=:etat, domain_env_id=:de_id, zone_id=:zone_id, + responsable_nom=:resp_srv, referent_nom=:resp_dom, commentaire=:desc, + tier=:tier, ssh_method=:ssh_method, ssh_user=:ssh_user, + patch_frequency=:patch_freq, patch_excludes=:patch_excludes, + domain_ltd=:domain_ltd, pref_patch_jour=:pref_jour, pref_patch_heure=:pref_heure, + updated_at=NOW() WHERE id=:sid"""), {**vals, "sid": existing.id}) + if vals["ip"]: _upsert_ip(db, existing.id, vals["ip"]) + stats["ips"] += 1 + stats["servers_updated"] += 1 else: try: - db.execute(text(""" - INSERT INTO servers (hostname, fqdn, os_family, os_version, machine_type, - etat, domain_env_id, zone_id, responsable_nom, referent_nom, - commentaire, ssh_port, ssh_user, ssh_method, tier, - patch_frequency, patch_excludes, domain_ltd, - pref_patch_jour, pref_patch_heure) + db.execute(text("""INSERT INTO servers (hostname, fqdn, os_family, os_version, machine_type, + etat, domain_env_id, zone_id, responsable_nom, referent_nom, commentaire, + ssh_port, ssh_user, ssh_method, tier, patch_frequency, patch_excludes, + domain_ltd, pref_patch_jour, pref_patch_heure) VALUES (:hostname, :fqdn, :os_family, :os_version, :machine_type, - :etat, :de_id, :zone_id, :resp_srv, :resp_dom, - :desc, 22, :ssh_user, :ssh_method, :tier, - :patch_freq, :patch_excludes, :domain_ltd, - :pref_jour, :pref_heure) - """), vals) + :etat, :de_id, :zone_id, :resp_srv, :resp_dom, :desc, + 22, :ssh_user, :ssh_method, :tier, :patch_freq, :patch_excludes, + :domain_ltd, :pref_jour, :pref_heure)"""), vals) db.flush() - # Get new server ID and insert IP if vals["ip"]: new_srv = db.execute(text("SELECT id FROM servers WHERE hostname=:h"), {"h": hostname}).fetchone() if new_srv: _upsert_ip(db, new_srv.id, vals["ip"]) + stats["ips"] += 1 stats["servers_created"] += 1 except Exception as e: db.rollback() - stats["errors"].append(f"Server {hostname}: {e}") + stats["errors"].append(f"VM {hostname}: {str(e)[:80]}") - # ─── 8. Insert physical servers ─── + # ─── 8. Physical Servers ─── + phys = client.get_all("Server", + "name,description,status,managementip,osfamily_id_friendlyname," + "osversion_id_friendlyname,contacts_list,location_name") for s in phys: hostname = s.get("name", "").split(".")[0].lower() if not hostname: continue - existing = db.execute(text("SELECT id FROM servers WHERE LOWER(hostname)=LOWER(:h)"), - {"h": hostname}).fetchone() + existing = db.execute(text("SELECT id FROM servers WHERE LOWER(hostname)=LOWER(:h)"), {"h": hostname}).fetchone() if not existing: try: contacts = s.get("contacts_list", []) resp = contacts[0].get("contact_id_friendlyname", "") if contacts else "" - db.execute(text(""" - INSERT INTO servers (hostname, fqdn, os_family, os_version, machine_type, - etat, responsable_nom, commentaire, ssh_port, ssh_user, ssh_method, tier) - VALUES (:h, :f, :osf, :osv, 'physical', 'en_production', :resp, :desc, - 22, 'root', 'ssh_key', 'tier0') - """), { - "h": hostname, "f": s.get("name", hostname), - "osf": "linux" if "linux" in s.get("osfamily_id_friendlyname", "").lower() else "windows", - "osv": s.get("osversion_id_friendlyname", ""), - "resp": resp, "desc": s.get("description", ""), - }) + db.execute(text("""INSERT INTO servers (hostname, fqdn, os_family, os_version, machine_type, + etat, responsable_nom, commentaire, site, ssh_port, ssh_user, ssh_method, tier) + VALUES (:h, :f, :osf, :osv, 'physical', 'en_production', :resp, :desc, :site, + 22, 'root', 'ssh_key', 'tier0')"""), + {"h": hostname, "f": s.get("name", hostname), + "osf": "linux" if "linux" in s.get("osfamily_id_friendlyname", "").lower() else "windows", + "osv": s.get("osversion_id_friendlyname", ""), + "resp": resp, "desc": s.get("description", ""), + "site": s.get("location_name", "")}) db.flush() ip = s.get("managementip", "") if ip: new_srv = db.execute(text("SELECT id FROM servers WHERE hostname=:h"), {"h": hostname}).fetchone() if new_srv: _upsert_ip(db, new_srv.id, ip) + stats["ips"] += 1 stats["servers_created"] += 1 except Exception as e: db.rollback() - stats["errors"].append(f"Physical {hostname}: {e}") + stats["errors"].append(f"Phys {hostname}: {str(e)[:80]}") db.commit() - log.info(f"iTop full sync: {stats}") + _save_sync_timestamp(db, "from", {k: v for k, v in stats.items() if k != "errors"}) + db.commit() + log.info(f"iTop import: {stats}") return stats +# ══════════════════════════════════════════════════════════ +# EXPORT: PatchCenter → iTop +# ══════════════════════════════════════════════════════════ def sync_to_itop(db, itop_url, itop_user, itop_pass): - """Exporte referentiel + serveurs de PatchCenter vers iTop""" + """Exporte referentiel + serveurs + champs patching vers iTop""" client = ITopClient(itop_url, itop_user, itop_pass) - stats = {"updated": 0, "created": 0, "ref_created": 0, "errors": []} + stats = {"ref_created": 0, "servers_updated": 0, "servers_created": 0, "errors": []} - # ─── Sync referentiel: environments, domains, zones ─── - for pc_table, itop_class in [("environments", "Environnement"), ("domains", "DomaineApplicatif"), ("zones", "Zone")]: - # Get existing in iTop - existing_itop = set() - for item in client.get_all(itop_class, "name"): - existing_itop.add(item.get("name", "").lower()) - - # Get PatchCenter values + # ─── 1. Sync typologies: create missing in iTop ─── + typo_map = [ + ("environments", "Environnement"), + ("domains", "DomaineApplicatif"), + ("zones", "Zone"), + ] + for pc_table, itop_class in typo_map: + existing_itop = {item.get("name", "").lower() for item in client.get_all(itop_class, "name")} rows = db.execute(text(f"SELECT name FROM {pc_table} ORDER BY name")).fetchall() for row in rows: - name = row.name - if name.lower() not in existing_itop: - r = client.create(itop_class, { - "name": name, - "org_id": "SELECT Organization WHERE name = 'MPCZ'", - }) + if row.name.lower() not in existing_itop: + r = client.create(itop_class, {"name": row.name, "org_id": "SELECT Organization WHERE name = 'MPCZ'"}) if r.get("code") == 0: stats["ref_created"] += 1 - existing_itop.add(name.lower()) + existing_itop.add(row.name.lower()) else: - stats["errors"].append(f"{itop_class} '{name}': {r.get('message', '')[:60]}") + stats["errors"].append(f"{itop_class} '{row.name}': {r.get('message', '')[:60]}") - # Get iTop VMs indexed by short name + # ─── 2. Sync domain_ltd → DomainLdap ─── + existing_ldap = {item.get("name", "").lower() for item in client.get_all("DomainLdap", "name")} + rows = db.execute(text("SELECT name FROM domain_ltd_list ORDER BY name")).fetchall() + for row in rows: + if row.name.lower() not in existing_ldap: + r = client.create("DomainLdap", {"name": row.name, "org_id": "SELECT Organization WHERE name = 'MPCZ'"}) + if r.get("code") == 0: + stats["ref_created"] += 1 + + # ─── 3. Sync servers → VirtualMachine ─── itop_vms = {} for v in client.get_all("VirtualMachine", "name"): itop_vms[v["name"].split(".")[0].lower()] = v - # Get PatchCenter servers - rows = db.execute(text( - "SELECT hostname, fqdn, os_version, etat, commentaire, tier FROM servers" - )).fetchall() - status_map = {"en_production": "production", "decommissionne": "obsolete", "stock": "stock", "en_cours": "implementation"} + tier_map = {"tier0": "Tier 0", "tier1": "Tier 1", "tier2": "Tier 2", "tier3": "Tier 3"} + + rows = db.execute(text("""SELECT hostname, fqdn, os_version, etat, commentaire, tier, + ssh_method, ssh_user, patch_frequency, patch_excludes, domain_ltd, + pref_patch_jour, pref_patch_heure FROM servers WHERE machine_type='vm'""")).fetchall() for srv in rows: hostname = (srv.hostname or "").lower() itop_vm = itop_vms.get(hostname) + fields = {} + if srv.etat: + fields["status"] = status_map.get(srv.etat, "production") + if srv.commentaire: + fields["description"] = srv.commentaire + if srv.patch_excludes: + fields["patch_excludes"] = srv.patch_excludes + if srv.tier and srv.tier in tier_map: + fields["tier_id"] = f"SELECT Tier WHERE name = '{tier_map[srv.tier]}'" + if srv.ssh_method: + fields["connexion_method_id"] = f"SELECT ConnexionMethod WHERE name = '{srv.ssh_method}'" + if srv.ssh_user: + fields["ssh_user_id"] = f"SELECT SshUser WHERE name = '{srv.ssh_user}'" + if srv.patch_frequency: + freq = srv.patch_frequency.capitalize() + fields["patch_frequency_id"] = f"SELECT PatchFrequency WHERE name = '{freq}'" + if srv.pref_patch_jour and srv.pref_patch_jour != "indifferent": + fields["pref_patch_jour_id"] = f"SELECT PrefPatchJour WHERE name = '{srv.pref_patch_jour.capitalize()}'" + if srv.pref_patch_heure and srv.pref_patch_heure != "indifferent": + fields["patch_window"] = srv.pref_patch_heure + if srv.domain_ltd: + fields["domain_ldap_id"] = f"SELECT DomainLdap WHERE name = '{srv.domain_ltd}'" + if itop_vm: - fields = {} - if srv.etat: - fields["status"] = status_map.get(srv.etat, "production") - if srv.commentaire: - fields["description"] = srv.commentaire if fields: r = client.update("VirtualMachine", itop_vm["itop_id"], fields) if r.get("code") == 0: - stats["updated"] += 1 + stats["servers_updated"] += 1 else: - stats["errors"].append(f"{hostname}: {r.get('message', '')[:60]}") + stats["errors"].append(f"Update {hostname}: {r.get('message', '')[:60]}") else: - # Create VM in iTop - r = client.create("VirtualMachine", { - "name": srv.hostname, - "org_id": "SELECT Organization WHERE name = 'MPCZ'", - "status": status_map.get(srv.etat, "production"), - "description": srv.commentaire or f"OS: {srv.os_version or 'N/A'}", - }) + fields["name"] = srv.hostname + fields["org_id"] = "SELECT Organization WHERE name = 'MPCZ'" + if not fields.get("status"): + fields["status"] = "production" + r = client.create("VirtualMachine", fields) if r.get("code") == 0: - stats["created"] += 1 + stats["servers_created"] += 1 else: stats["errors"].append(f"Create {hostname}: {r.get('message', '')[:60]}") + db.commit() + _save_sync_timestamp(db, "to", {k: v for k, v in stats.items() if k != "errors"}) + db.commit() log.info(f"iTop export: {stats}") return stats diff --git a/app/templates/referentiel.html b/app/templates/referentiel.html index 74dc22b..46dfd02 100644 --- a/app/templates/referentiel.html +++ b/app/templates/referentiel.html @@ -9,6 +9,54 @@ + +{% if can_modify %} +
+
+ Synchronisation iTop +
+ +
+
+ +
+ {% if 'itop_from' in request.query_params.get('msg','') %} + ✓ Import OK + {% elif 'itop_to' in request.query_params.get('msg','') %} + ✓ Export OK + {% elif request.query_params.get('msg') == 'itop_noconfig' %} + ✗ Configurer iTop dans Settings + {% elif request.query_params.get('msg') == 'itop_error' %} + ✗ Erreur synchro + {% endif %} +
+
+ {% if last_sync_from %} + Dernier import : {{ last_sync_from.date }} + — {{ last_sync_from.stats.servers_created|default(0) }} créés, + {{ last_sync_from.stats.servers_updated|default(0) }} maj, + {{ last_sync_from.stats.contacts|default(0) }} contacts + {% else %} + Dernier import : jamais + {% endif %} + {% if last_sync_to %} + Dernier export : {{ last_sync_to.date }} + — {{ last_sync_to.stats.servers_updated|default(0) }} maj, + {{ last_sync_to.stats.servers_created|default(0) }} créés, + {{ last_sync_to.stats.ref_created|default(0) }} réf + {% else %} + Dernier export : jamais + {% endif %} +
+
+{% endif %} + {% set msg = request.query_params.get('msg', '') %} {% set detail = request.query_params.get('detail', '') %} {% if msg == 'added' %} @@ -48,31 +96,6 @@ {% if tab == 'domains' %} - -{% if can_modify %} -
- Synchronisation iTop : -
- -
-
- -
- {% if 'itop_from' in request.query_params.get('msg','') %} - Import iTop OK - {% elif 'itop_to' in request.query_params.get('msg','') %} - Export iTop OK - {% elif request.query_params.get('msg') == 'itop_noconfig' %} - Configurer iTop dans Settings - {% endif %} -
-{% endif %}