"""Service iTop REST API — synchronisation bidirectionnelle""" import logging import requests import json from datetime import datetime from sqlalchemy import text log = logging.getLogger(__name__) class ITopClient: """Client REST iTop v1.3""" def __init__(self, url, user, password): self.url = url.rstrip("/") + "/webservices/rest.php?version=1.3" self.user = user self.password = password def _call(self, operation, **kwargs): data = {"operation": operation, **kwargs} try: r = requests.post(self.url, data={"json_data": json.dumps(data), "auth_user": self.user, "auth_pwd": self.password}, verify=False, timeout=30) return r.json() except Exception as e: log.error(f"iTop error: {e}") return {"code": -1, "message": str(e)} 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"): return [{"itop_id": o["key"], **o["fields"]} for o in r["objects"].values()] return [] def update(self, cls, key, fields): return self._call("core/update", **{"class": cls, "key": str(key), "fields": fields, "comment": "PatchCenter sync"}) 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): """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": []} # 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") 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 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", "")}) stats["contacts"] += 1 except Exception as e: db.rollback() stats["errors"].append(f"Contact {fullname}: {e}") # ─── 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_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"} 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() # Map tier: "Tier 0" -> "tier0" 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), "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", ""), "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", ""), "domain_ltd": v.get("domain_ldap_name", ""), "pref_jour": pref_jour, "pref_heure": pref_heure, } 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 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) 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) stats["servers_created"] += 1 except Exception as e: db.rollback() 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.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: db.rollback() stats["errors"].append(f"Physical {hostname}: {e}") db.commit() log.info(f"iTop full sync: {stats}") return stats def sync_to_itop(db, itop_url, itop_user, itop_pass): """Exporte les infos patching de PatchCenter vers iTop""" client = ITopClient(itop_url, itop_user, itop_pass) stats = {"updated": 0, "created": 0, "errors": []} # 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, etat, commentaire, tier FROM servers" )).fetchall() status_map = {"en_production": "production", "decommissionne": "obsolete", "stock": "stock", "en_cours": "implementation"} for srv in rows: hostname = (srv.hostname or "").lower() itop_vm = itop_vms.get(hostname) 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 else: stats["errors"].append(f"{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'}", }) if r.get("code") == 0: stats["created"] += 1 else: stats["errors"].append(f"Create {hostname}: {r.get('message', '')[:60]}") log.info(f"iTop export: {stats}") return stats