diff --git a/app/services/itop_service.py b/app/services/itop_service.py index 33bb63f..cfd46c6 100644 --- a/app/services/itop_service.py +++ b/app/services/itop_service.py @@ -1,4 +1,4 @@ -"""Service iTop REST API — synchronisation bidirectionnelle serveurs et contacts""" +"""Service iTop REST API — synchronisation bidirectionnelle""" import logging import requests import json @@ -24,261 +24,276 @@ class ITopClient: "auth_user": self.user, "auth_pwd": self.password}, verify=False, timeout=30) - result = r.json() - if result.get("code", -1) != 0: - log.error(f"iTop API error: {result.get('message')}") - return result + return r.json() except Exception as e: - log.error(f"iTop connection error: {e}") + log.error(f"iTop error: {e}") return {"code": -1, "message": str(e)} - def get_servers(self): - """Recupere tous les serveurs iTop (Server + VirtualMachine) avec contacts""" - servers = [] - for cls in ["Server", "VirtualMachine"]: - r = self._call("core/get", **{ - "class": cls, - "key": f"SELECT {cls}", - "output_fields": "name,description,status,managementip,osfamily_id_friendlyname," - "osversion_id_friendlyname,brand_name,model_name,serialnumber," - "organization_name,location_name,business_criticity,cpu,ram," - "contacts_list,responsable_serveur_name,responsable_domaine_name,environnement" - }) - if r.get("code") == 0 and r.get("objects"): - for key, obj in r["objects"].items(): - f = obj["fields"] - # Extract contacts (responsable serveur) - contacts = f.get("contacts_list", []) - contact_names = [] - contact_emails = [] - for c in contacts: - cname = c.get("contact_id_friendlyname", "") - if cname: - contact_names.append(cname) - - servers.append({ - "itop_id": obj["key"], - "itop_class": cls, - "name": f.get("name", ""), - "description": f.get("description", ""), - "status": f.get("status", ""), - "ip": f.get("managementip", ""), - "os_family": f.get("osfamily_id_friendlyname", ""), - "os_version": f.get("osversion_id_friendlyname", ""), - "brand": f.get("brand_name", ""), - "model": f.get("model_name", ""), - "serial": f.get("serialnumber", ""), - "org": f.get("organization_name", ""), - "location": f.get("location_name", ""), - "criticity": f.get("business_criticity", ""), - "cpu": f.get("cpu", ""), - "ram": f.get("ram", ""), - "responsable_nom": f.get("responsable_serveur_name", "") or (contact_names[0] if contact_names else ""), - "responsable_domaine": f.get("responsable_domaine_name", ""), - "environnement": f.get("environnement", ""), - }) - return servers - - def get_contacts(self): - """Recupere tous les contacts iTop""" - r = self._call("core/get", **{ - "class": "Person", - "key": "SELECT Person", - "output_fields": "name,first_name,email,phone,org_name,function" - }) - contacts = [] + def get_all(self, cls, fields): + r = self._call("core/get", **{"class": cls, "key": f"SELECT {cls}", "output_fields": fields}) if r.get("code") == 0 and r.get("objects"): - for key, obj in r["objects"].items(): - f = obj["fields"] - contacts.append({ - "itop_id": obj["key"], - "name": f.get("name", ""), - "first_name": f.get("first_name", ""), - "email": f.get("email", ""), - "phone": f.get("phone", ""), - "org": f.get("org_name", ""), - "function": f.get("function", ""), - }) - return contacts + return [{"itop_id": o["key"], **o["fields"]} for o in r["objects"].values()] + return [] - def get_server_contacts(self, server_class, server_id): - """Recupere les contacts associes a un serveur""" - r = self._call("core/get", **{ - "class": server_class, - "key": str(server_id), - "output_fields": "contacts_list" - }) - contacts = [] - if r.get("code") == 0 and r.get("objects"): - for key, obj in r["objects"].items(): - for c in obj["fields"].get("contacts_list", []): - contacts.append({ - "contact_id": c.get("contact_id"), - "contact_name": c.get("contact_id_friendlyname", ""), - }) - return contacts + def update(self, cls, key, fields): + return self._call("core/update", **{"class": cls, "key": str(key), "fields": fields, "comment": "PatchCenter sync"}) - def update_server(self, server_class, server_id, fields): - """Met a jour un serveur dans iTop""" - return self._call("core/update", **{ - "class": server_class, - "key": str(server_id), - "fields": fields, - "comment": "Sync from PatchCenter" - }) - - def create_server(self, server_class, fields): - """Cree un serveur dans iTop""" - return self._call("core/create", **{ - "class": server_class, - "fields": fields, - "comment": "Created by PatchCenter" - }) + def create(self, cls, fields): + return self._call("core/create", **{"class": cls, "fields": fields, "comment": "PatchCenter sync"}) def sync_from_itop(db, itop_url, itop_user, itop_pass): - """Importe les serveurs et contacts depuis iTop vers PatchCenter""" + """Import complet depuis iTop: contacts, domaines, envs, zones, serveurs""" client = ITopClient(itop_url, itop_user, itop_pass) - stats = {"servers_updated": 0, "servers_created": 0, "contacts_updated": 0, "contacts_created": 0, "errors": []} - - # --- Sync contacts --- - itop_contacts = client.get_contacts() - for c in itop_contacts: - fullname = f"{c['first_name']} {c['name']}".strip() - existing = db.execute(text( - "SELECT id FROM contacts WHERE LOWER(email) = LOWER(:email) OR LOWER(nom) = LOWER(:nom)" - ), {"email": c["email"], "nom": fullname}).fetchone() + stats = {"contacts": 0, "domains": 0, "environments": 0, "zones": 0, + "servers_created": 0, "servers_updated": 0, "errors": []} + # ─── 1. Contacts (Person) ─── + persons = client.get_all("Person", "name,first_name,email,phone,org_name,function") + 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() if existing: - db.execute(text(""" - UPDATE contacts SET nom = :nom, email = :email, telephone = :tel, updated_at = NOW() - WHERE id = :id - """), {"id": existing.id, "nom": fullname, "email": c["email"], "tel": c["phone"]}) - stats["contacts_updated"] += 1 + db.execute(text("UPDATE contacts SET nom=:n, telephone=:t, updated_at=NOW() WHERE id=:id"), + {"id": existing.id, "n": fullname, "t": p.get("phone", "")}) else: try: - db.execute(text(""" - INSERT INTO contacts (nom, email, telephone) VALUES (:nom, :email, :tel) - """), {"nom": fullname, "email": c["email"], "tel": c["phone"]}) - stats["contacts_created"] += 1 + db.execute(text("INSERT INTO contacts (nom, email, telephone) VALUES (:n, :e, :t)"), + {"n": fullname, "e": p.get("email", ""), "t": p.get("phone", "")}) + stats["contacts"] += 1 except Exception as e: stats["errors"].append(f"Contact {fullname}: {e}") - # --- Sync servers --- - itop_servers = client.get_servers() - for s in itop_servers: - hostname = s["name"].split(".")[0].lower() - existing = db.execute(text( - "SELECT id FROM servers WHERE LOWER(hostname) = LOWER(:h) OR LOWER(fqdn) = LOWER(:f)" - ), {"h": hostname, "f": s["name"]}).fetchone() + # ─── 2. Domaines applicatifs (typology via VMs) ─── + 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,environnement_name," + "domaine_applicatif_name,zone_name," + "applicationsolution_list,contacts_list," + "virtualhost_name,business_criticity") - itop_status_map = {"production": "en_production", "stock": "stock", - "implementation": "en_cours", "obsolete": "decommissionne"} - etat = itop_status_map.get(s["status"], "en_production") + # 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"} + + 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", "")) + + existing = db.execute(text("SELECT id FROM servers WHERE LOWER(hostname)=LOWER(:h)"), + {"h": hostname}).fetchone() + + vals = { + "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, + "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", ""), + } if existing: - updates = {"sid": existing.id} - sets = [] - if s["os_version"]: - sets.append("os_version = :osv") - updates["osv"] = s["os_version"] - if s["ip"]: - sets.append("fqdn = :fqdn") - updates["fqdn"] = s["name"] - if s["location"]: - sets.append("site = :site") - updates["site"] = s["location"] - if s.get("responsable_nom"): - sets.append("responsable_nom = :resp_nom") - updates["resp_nom"] = s["responsable_nom"] - if s.get("responsable_domaine"): - sets.append("referent_nom = :ref_nom") - updates["ref_nom"] = s["responsable_domaine"] - if s.get("description"): - sets.append("commentaire = :comm") - updates["comm"] = s["description"] - if s.get("ip"): - sets.append("fqdn = :fqdn") - updates["fqdn"] = s["name"] - if sets: - sets.append("updated_at = NOW()") - db.execute(text(f"UPDATE servers SET {', '.join(sets)} WHERE id = :sid"), updates) + 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, updated_at=NOW() + WHERE id=:sid + """), {**vals, "sid": existing.id}) stats["servers_updated"] += 1 else: try: db.execute(text(""" INSERT INTO servers (hostname, fqdn, os_family, os_version, machine_type, - tier, etat, ssh_port, ssh_user, ssh_method, site) - VALUES (:h, :f, :osf, :osv, :mt, 'a_definir', :etat, 22, 'root', 'ssh_key', :site) + etat, domain_env_id, zone_id, responsable_nom, referent_nom, + commentaire, ssh_port, ssh_user, ssh_method, tier) + VALUES (:hostname, :fqdn, :os_family, :os_version, :machine_type, + :etat, :de_id, :zone_id, :resp_srv, :resp_dom, + :desc, 22, 'root', 'ssh_key', 'a_definir') + """), vals) + stats["servers_created"] += 1 + except Exception as e: + stats["errors"].append(f"Server {hostname}: {e}") + + # ─── 8. Insert physical servers ─── + 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() + 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["name"], - "osf": "linux" if "linux" in s["os_family"].lower() else "windows", - "osv": s["os_version"], "etat": etat, "site": s["location"], - "mt": "vm" if s["itop_class"] == "VirtualMachine" else "physical", + "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", ""), }) stats["servers_created"] += 1 except Exception as e: - stats["errors"].append(f"Server {s['name']}: {e}") + stats["errors"].append(f"Physical {hostname}: {e}") db.commit() - log.info(f"iTop sync: {stats}") + log.info(f"iTop full sync: {stats}") return stats def sync_to_itop(db, itop_url, itop_user, itop_pass): - """Exporte les serveurs PatchCenter vers iTop (mise a jour)""" + """Exporte les infos patching de PatchCenter vers iTop""" client = ITopClient(itop_url, itop_user, itop_pass) stats = {"updated": 0, "created": 0, "errors": []} - # Get current iTop servers for matching (by full name AND by hostname part) - itop_servers_raw = client.get_servers() - itop_servers = {} - for s in itop_servers_raw: - itop_servers[s["name"].lower()] = s - short = s["name"].split(".")[0].lower() - if short not in itop_servers: - itop_servers[short] = s + # Get iTop VMs indexed by short name + 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, os_family, etat, site, responsable_nom, commentaire FROM servers")).fetchall() + # 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"} for srv in rows: - fqdn = (srv.fqdn or "").lower() hostname = (srv.hostname or "").lower() - itop_srv = itop_servers.get(fqdn) or itop_servers.get(hostname) + itop_vm = itop_vms.get(hostname) - if itop_srv: + if itop_vm: fields = {} - if srv.commentaire: - fields["description"] = srv.commentaire - elif srv.os_version: - fields["description"] = f"OS: {srv.os_version}" if srv.etat: fields["status"] = status_map.get(srv.etat, "production") - + if srv.commentaire: + fields["description"] = srv.commentaire if fields: - r = client.update_server(itop_srv["itop_class"], itop_srv["itop_id"], fields) + r = client.update("VirtualMachine", itop_vm["itop_id"], fields) if r.get("code") == 0: stats["updated"] += 1 else: - stats["errors"].append(f"{srv.hostname}: {r.get('message')}") - + stats["errors"].append(f"{hostname}: {r.get('message', '')[:60]}") else: - # Server not in iTop — create as VirtualMachine - fields = { + # 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'}", - } - r = client.create_server("VirtualMachine", fields) + }) if r.get("code") == 0: stats["created"] += 1 else: - stats["errors"].append(f"Create {srv.hostname}: {r.get('message')}") + stats["errors"].append(f"Create {hostname}: {r.get('message', '')[:60]}") log.info(f"iTop export: {stats}") return stats