patchcenter/app/services/itop_service.py
Admin MPCZ be6c5158b8 Sync iTop: tous les champs custom (tier, connexion, ssh_user, patch_freq, pref_jour/heure, domain_ldap, excludes)
Correspondance complete iTop <-> PatchCenter:
- tier_name -> tier (Tier 0 -> tier0)
- connexion_method_name -> ssh_method
- ssh_user_name -> ssh_user
- patch_frequency_name -> patch_frequency (Monthly -> monthly)
- pref_patch_jour_name -> pref_patch_jour
- pref_patch_heure_name -> pref_patch_heure
- patch_excludes -> patch_excludes
- domain_ldap_name -> domain_ltd
2026-04-11 02:28:39 +02:00

333 lines
15 KiB
Python

"""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": []}
# ─── 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=: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 (: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}")
# ─── 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,"
"tier_name,connexion_method_name,ssh_user_name,"
"patch_frequency_name,pref_patch_jour_name,pref_patch_heure_name,"
"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("pref_patch_heure_name", "") 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:
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:
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