"""Service iTop REST API — synchronisation bidirectionnelle complète""" 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 _upsert_ip(db, server_id, ip): if not ip: return existing = db.execute(text( "SELECT id FROM server_ips WHERE server_id=:sid AND ip_address=:ip"), {"sid": server_id, "ip": ip}).fetchone() if not existing: try: db.execute(text( "INSERT INTO server_ips (server_id, ip_address, ip_type, is_ssh, description) VALUES (:sid, :ip, 'primary', true, 'itop')"), {"sid": server_id, "ip": ip}) except Exception: pass 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": []} try: db.rollback() except Exception: pass # ─── 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() 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": email}) stats["contacts"] += 1 except Exception: db.rollback() # ─── 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," "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") 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 # 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() 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" ssh_method = v.get("connexion_method_name", "") or "ssh_key" patch_freq = (v.get("patch_frequency_name", "") or "").lower() or None 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.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", ""), "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, } 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}) 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) 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) db.flush() 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"VM {hostname}: {str(e)[:80]}") # ─── 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() 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, 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"Phys {hostname}: {str(e)[:80]}") db.commit() _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 + champs patching vers iTop""" client = ITopClient(itop_url, itop_user, itop_pass) stats = {"ref_created": 0, "servers_updated": 0, "servers_created": 0, "errors": []} # ─── 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: 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(row.name.lower()) else: stats["errors"].append(f"{itop_class} '{row.name}': {r.get('message', '')[:60]}") # ─── 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 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: if fields: r = client.update("VirtualMachine", itop_vm["itop_id"], fields) if r.get("code") == 0: stats["servers_updated"] += 1 else: stats["errors"].append(f"Update {hostname}: {r.get('message', '')[:60]}") else: 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["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