patchcenter/app/services/itop_service.py

492 lines
23 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Service iTop REST API — synchronisation bidirectionnelle complète"""
import logging
import requests
import json
from datetime import datetime
from collections import defaultdict
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 + Teams ───
persons = client.get_all("Person", "name,first_name,email,phone,org_name")
# Get team memberships to determine role
team_members = {} # person_fullname_lower -> team_name
teams = client.get_all("Team", "name,persons_list")
for t in teams:
team_name = t.get("name", "")
for member in t.get("persons_list", []):
pname = member.get("person_id_friendlyname", "").lower()
if pname:
team_members[pname] = team_name
team_role_map = {"SecOps": "referent_technique", "iPOP": "responsable_applicatif", "Externe": "referent_technique"}
for p in persons:
fullname = f"{p.get('first_name','')} {p.get('name','')}".strip()
email = p.get("email", "")
if not email:
continue
# Determine role from team
team = team_members.get(fullname.lower(), "")
role = team_role_map.get(team, "referent_technique")
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, role=:r, updated_at=NOW() WHERE id=:id"),
{"id": existing.id, "n": fullname, "r": role})
else:
try:
db.execute(text("INSERT INTO contacts (name, email, role) VALUES (:n, :e, :r)"),
{"n": fullname, "e": email, "r": role})
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()}
# Person name → email lookup
person_email = {}
for p in persons:
fullname = f"{p.get('first_name','')} {p.get('name','')}".strip()
person_email[fullname.lower()] = p.get("email", "")
# ─── 7. Pre-collect responsables per domaine×env (most frequent wins) ───
# Will be populated during VM processing, then used to update domain_environments
de_responsables = defaultdict(lambda: {"resp_dom": defaultdict(int), "resp_dom_email": {},
"referent": defaultdict(int), "referent_email": {}})
# ─── 8. 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) VALUES (:d, :e)"),
{"d": did, "e": eid})
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()
# Collect responsables for this domain×env
if de_id:
resp_dom = v.get("responsable_domaine_name", "")
if resp_dom:
de_responsables[de_id]["resp_dom"][resp_dom] += 1
de_responsables[de_id]["resp_dom_email"][resp_dom] = person_email.get(resp_dom.lower(), "")
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"
resp_srv_name = v.get("responsable_serveur_name", "")
resp_dom_name = v.get("responsable_domaine_name", "")
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": resp_srv_name,
"resp_srv_email": person_email.get(resp_srv_name.lower(), ""),
"resp_dom": resp_dom_name,
"resp_dom_email": person_email.get(resp_dom_name.lower(), ""),
"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, responsable_email=:resp_srv_email,
referent_nom=:resp_dom, referent_email=:resp_dom_email, 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, responsable_email,
referent_nom, referent_email, 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_srv_email,
:resp_dom, :resp_dom_email, :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]}")
# ─── 8b. Update domain_environments with most frequent responsables ───
for de_id, resps in de_responsables.items():
updates = {}
if resps["resp_dom"]:
top_resp = max(resps["resp_dom"], key=resps["resp_dom"].get)
updates["responsable_nom"] = top_resp
updates["responsable_email"] = resps["resp_dom_email"].get(top_resp, "")
db.execute(text("""UPDATE domain_environments SET
responsable_nom=:responsable_nom, responsable_email=:responsable_email
WHERE id=:id"""),
{"id": de_id, **updates}) if updates else None
# ─── 9. 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})
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})
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"}
# Build Person name → itop_id lookup for responsable sync
itop_persons = {}
for p in client.get_all("Person", "name,first_name"):
fullname = f"{p.get('first_name','')} {p.get('name','')}".strip()
itop_persons[fullname.lower()] = p["itop_id"]
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, responsable_nom, referent_nom
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}'"
# Responsable serveur
if srv.responsable_nom:
pid = itop_persons.get(srv.responsable_nom.lower())
if pid:
fields["responsable_serveur_id"] = pid
# Responsable domaine
if srv.referent_nom:
pid = itop_persons.get(srv.referent_nom.lower())
if pid:
fields["responsable_domaine_id"] = pid
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