patchcenter/app/services/itop_service.py
Admin MPCZ d8a526368e Refonte synchro iTop bidirectionnelle complète
- 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
2026-04-11 13:19:10 +02:00

419 lines
20 KiB
Python

"""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