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
This commit is contained in:
parent
1ff6b3fd4d
commit
d8a526368e
@ -103,6 +103,11 @@ def referentiel_page(request: Request, db=Depends(get_db),
|
|||||||
for r in rows:
|
for r in rows:
|
||||||
zone_srv_counts[r.id] = r.cnt
|
zone_srv_counts[r.id] = r.cnt
|
||||||
|
|
||||||
|
# Dernière synchro
|
||||||
|
from ..services.itop_service import get_last_sync
|
||||||
|
last_sync_from = get_last_sync(db, "from")
|
||||||
|
last_sync_to = get_last_sync(db, "to")
|
||||||
|
|
||||||
return templates.TemplateResponse("referentiel.html", {
|
return templates.TemplateResponse("referentiel.html", {
|
||||||
"request": request, "user": user, "perms": perms,
|
"request": request, "user": user, "perms": perms,
|
||||||
"can_modify": can_modify, "tab": tab,
|
"can_modify": can_modify, "tab": tab,
|
||||||
@ -111,6 +116,8 @@ def referentiel_page(request: Request, db=Depends(get_db),
|
|||||||
"dom_srv_counts": dom_srv_counts,
|
"dom_srv_counts": dom_srv_counts,
|
||||||
"env_srv_counts": env_srv_counts,
|
"env_srv_counts": env_srv_counts,
|
||||||
"zone_srv_counts": zone_srv_counts,
|
"zone_srv_counts": zone_srv_counts,
|
||||||
|
"last_sync_from": last_sync_from,
|
||||||
|
"last_sync_to": last_sync_to,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
"""Service iTop REST API — synchronisation bidirectionnelle"""
|
"""Service iTop REST API — synchronisation bidirectionnelle complète"""
|
||||||
import logging
|
import logging
|
||||||
import requests
|
import requests
|
||||||
import json
|
import json
|
||||||
@ -43,7 +43,6 @@ class ITopClient:
|
|||||||
|
|
||||||
|
|
||||||
def _upsert_ip(db, server_id, ip):
|
def _upsert_ip(db, server_id, ip):
|
||||||
"""Insert or update IP in server_ips"""
|
|
||||||
if not ip:
|
if not ip:
|
||||||
return
|
return
|
||||||
existing = db.execute(text(
|
existing = db.execute(text(
|
||||||
@ -58,337 +57,362 @@ def _upsert_ip(db, server_id, ip):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def sync_from_itop(db, itop_url, itop_user, itop_pass):
|
def _save_sync_timestamp(db, direction, stats):
|
||||||
"""Import complet depuis iTop: contacts, domaines, envs, zones, serveurs"""
|
"""Enregistre le timestamp et les stats de la dernière synchro"""
|
||||||
client = ITopClient(itop_url, itop_user, itop_pass)
|
key = f"last_sync_{direction}"
|
||||||
stats = {"contacts": 0, "domains": 0, "environments": 0, "zones": 0,
|
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
"servers_created": 0, "servers_updated": 0, "errors": []}
|
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": []}
|
||||||
|
|
||||||
# Reset any failed transaction
|
|
||||||
try:
|
try:
|
||||||
db.rollback()
|
db.rollback()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# ─── 1. Contacts (Person) ───
|
# ─── 1. Typologies: Environnements ───
|
||||||
persons = client.get_all("Person", "name,first_name,email,phone,org_name,function")
|
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:
|
for p in persons:
|
||||||
fullname = f"{p.get('first_name','')} {p.get('name','')}".strip()
|
fullname = f"{p.get('first_name','')} {p.get('name','')}".strip()
|
||||||
existing = db.execute(text("SELECT id FROM contacts WHERE LOWER(email) = LOWER(:e)"),
|
email = p.get("email", "")
|
||||||
{"e": p.get("email", "")}).fetchone()
|
if not email:
|
||||||
|
continue
|
||||||
|
existing = db.execute(text("SELECT id FROM contacts WHERE LOWER(email)=LOWER(:e)"), {"e": email}).fetchone()
|
||||||
if existing:
|
if existing:
|
||||||
db.execute(text("UPDATE contacts SET name=:n, updated_at=NOW() WHERE id=:id"),
|
db.execute(text("UPDATE contacts SET name=:n, updated_at=NOW() WHERE id=:id"),
|
||||||
{"id": existing.id, "n": fullname})
|
{"id": existing.id, "n": fullname})
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
db.execute(text("INSERT INTO contacts (name, email, role) VALUES (:n, :e, 'referent_technique')"),
|
db.execute(text("INSERT INTO contacts (name, email, role) VALUES (:n, :e, 'referent_technique')"),
|
||||||
{"n": fullname, "e": p.get("email", "")})
|
{"n": fullname, "e": email})
|
||||||
stats["contacts"] += 1
|
stats["contacts"] += 1
|
||||||
except Exception as e:
|
except Exception:
|
||||||
db.rollback()
|
db.rollback()
|
||||||
stats["errors"].append(f"Contact {fullname}: {e}")
|
|
||||||
|
|
||||||
# ─── 2. Domaines applicatifs (typology via VMs) ───
|
# ─── 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",
|
vms = client.get_all("VirtualMachine",
|
||||||
"name,description,status,managementip,osfamily_id_friendlyname,"
|
"name,description,status,managementip,osfamily_id_friendlyname,"
|
||||||
"osversion_id_friendlyname,organization_name,cpu,ram,"
|
"osversion_id_friendlyname,organization_name,cpu,ram,"
|
||||||
"responsable_serveur_name,responsable_domaine_name,"
|
"responsable_serveur_name,responsable_domaine_name,"
|
||||||
"environnement_name,"
|
"environnement_name,domaine_applicatif_name,zone_name,"
|
||||||
"domaine_applicatif_name,zone_name,"
|
"contacts_list,virtualhost_name,business_criticity,"
|
||||||
"contacts_list,"
|
|
||||||
"virtualhost_name,business_criticity,"
|
|
||||||
"tier_name,connexion_method_name,ssh_user_name,"
|
"tier_name,connexion_method_name,ssh_user_name,"
|
||||||
"patch_frequency_name,pref_patch_jour_name,patch_window,"
|
"patch_frequency_name,pref_patch_jour_name,patch_window,"
|
||||||
"patch_excludes,domain_ldap_name,last_patch_date")
|
"patch_excludes,domain_ldap_name,last_patch_date")
|
||||||
|
|
||||||
# Also get physical Servers
|
itop_status = {"production": "en_production", "stock": "stock",
|
||||||
phys = client.get_all("Server",
|
"implementation": "en_cours", "obsolete": "decommissionne"}
|
||||||
"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:
|
for v in vms:
|
||||||
hostname = v.get("name", "").split(".")[0].lower()
|
hostname = v.get("name", "").split(".")[0].lower()
|
||||||
if not hostname:
|
if not hostname:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
dom = v.get("domaine_applicatif_name", "")
|
# Resolve domain_env_id
|
||||||
env = v.get("environnement_name", "")
|
dom = v.get("domaine_applicatif_name", "").lower()
|
||||||
de_id = de_map.get((dom, env))
|
env = v.get("environnement_name", "").lower()
|
||||||
zone_id = zone_map.get(v.get("zone_name", ""))
|
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()
|
||||||
|
|
||||||
existing = db.execute(text("SELECT id FROM servers WHERE LOWER(hostname)=LOWER(:h)"),
|
zone_id = zone_map.get(v.get("zone_name", "").lower())
|
||||||
{"h": hostname}).fetchone()
|
|
||||||
|
|
||||||
# Map tier: "Tier 0" -> "tier0"
|
|
||||||
tier_raw = v.get("tier_name", "")
|
tier_raw = v.get("tier_name", "")
|
||||||
tier = tier_raw.lower().replace(" ", "") if tier_raw else "a_definir"
|
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"
|
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
|
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_jour = (v.get("pref_patch_jour_name", "") or "").lower() or "indifferent"
|
||||||
pref_heure = v.get("patch_window", "") or "indifferent"
|
pref_heure = v.get("patch_window", "") or "indifferent"
|
||||||
|
|
||||||
vals = {
|
vals = {
|
||||||
"hostname": hostname,
|
"hostname": hostname, "fqdn": v.get("name", hostname),
|
||||||
"fqdn": v.get("name", hostname),
|
|
||||||
"os_family": "linux" if "linux" in v.get("osfamily_id_friendlyname", "").lower() else "windows",
|
"os_family": "linux" if "linux" in v.get("osfamily_id_friendlyname", "").lower() else "windows",
|
||||||
"os_version": v.get("osversion_id_friendlyname", ""),
|
"os_version": v.get("osversion_id_friendlyname", ""),
|
||||||
"machine_type": "vm",
|
"machine_type": "vm",
|
||||||
"etat": itop_status_map.get(v.get("status", ""), "en_production"),
|
"etat": itop_status.get(v.get("status", ""), "en_production"),
|
||||||
"de_id": de_id,
|
"de_id": de_id, "zone_id": zone_id,
|
||||||
"zone_id": zone_id,
|
|
||||||
"resp_srv": v.get("responsable_serveur_name", ""),
|
"resp_srv": v.get("responsable_serveur_name", ""),
|
||||||
"resp_dom": v.get("responsable_domaine_name", ""),
|
"resp_dom": v.get("responsable_domaine_name", ""),
|
||||||
"desc": v.get("description", ""),
|
"desc": v.get("description", ""),
|
||||||
"ip": v.get("managementip", ""),
|
"ip": v.get("managementip", ""),
|
||||||
"criticity": v.get("business_criticity", ""),
|
"tier": tier, "ssh_method": ssh_method,
|
||||||
"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",
|
"ssh_user": v.get("ssh_user_name", "") or "root",
|
||||||
"patch_freq": patch_freq,
|
"patch_freq": patch_freq, "patch_excludes": v.get("patch_excludes", ""),
|
||||||
"patch_excludes": v.get("patch_excludes", ""),
|
|
||||||
"domain_ltd": v.get("domain_ldap_name", ""),
|
"domain_ltd": v.get("domain_ldap_name", ""),
|
||||||
"pref_jour": pref_jour,
|
"pref_jour": pref_jour, "pref_heure": pref_heure,
|
||||||
"pref_heure": pref_heure,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
existing = db.execute(text("SELECT id FROM servers WHERE LOWER(hostname)=LOWER(:h)"), {"h": hostname}).fetchone()
|
||||||
if existing:
|
if existing:
|
||||||
db.execute(text("""
|
db.execute(text("""UPDATE servers SET fqdn=:fqdn, os_family=:os_family, os_version=:os_version,
|
||||||
UPDATE servers SET fqdn=:fqdn, os_family=:os_family, os_version=:os_version,
|
etat=:etat, domain_env_id=:de_id, zone_id=:zone_id,
|
||||||
etat=:etat, domain_env_id=:de_id, zone_id=:zone_id,
|
responsable_nom=:resp_srv, referent_nom=:resp_dom, commentaire=:desc,
|
||||||
responsable_nom=:resp_srv, referent_nom=:resp_dom,
|
tier=:tier, ssh_method=:ssh_method, ssh_user=:ssh_user,
|
||||||
commentaire=:desc, tier=:tier, ssh_method=:ssh_method,
|
patch_frequency=:patch_freq, patch_excludes=:patch_excludes,
|
||||||
ssh_user=:ssh_user, patch_frequency=:patch_freq,
|
domain_ltd=:domain_ltd, pref_patch_jour=:pref_jour, pref_patch_heure=:pref_heure,
|
||||||
patch_excludes=:patch_excludes, domain_ltd=:domain_ltd,
|
updated_at=NOW() WHERE id=:sid"""), {**vals, "sid": existing.id})
|
||||||
pref_patch_jour=:pref_jour, pref_patch_heure=:pref_heure,
|
if vals["ip"]:
|
||||||
updated_at=NOW()
|
|
||||||
WHERE id=:sid
|
|
||||||
"""), {**vals, "sid": existing.id})
|
|
||||||
stats["servers_updated"] += 1
|
|
||||||
# Update IP
|
|
||||||
if vals["ip"] and existing:
|
|
||||||
_upsert_ip(db, existing.id, vals["ip"])
|
_upsert_ip(db, existing.id, vals["ip"])
|
||||||
|
stats["ips"] += 1
|
||||||
|
stats["servers_updated"] += 1
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
db.execute(text("""
|
db.execute(text("""INSERT INTO servers (hostname, fqdn, os_family, os_version, machine_type,
|
||||||
INSERT INTO servers (hostname, fqdn, os_family, os_version, machine_type,
|
etat, domain_env_id, zone_id, responsable_nom, referent_nom, commentaire,
|
||||||
etat, domain_env_id, zone_id, responsable_nom, referent_nom,
|
ssh_port, ssh_user, ssh_method, tier, patch_frequency, patch_excludes,
|
||||||
commentaire, ssh_port, ssh_user, ssh_method, tier,
|
domain_ltd, pref_patch_jour, pref_patch_heure)
|
||||||
patch_frequency, patch_excludes, domain_ltd,
|
|
||||||
pref_patch_jour, pref_patch_heure)
|
|
||||||
VALUES (:hostname, :fqdn, :os_family, :os_version, :machine_type,
|
VALUES (:hostname, :fqdn, :os_family, :os_version, :machine_type,
|
||||||
:etat, :de_id, :zone_id, :resp_srv, :resp_dom,
|
:etat, :de_id, :zone_id, :resp_srv, :resp_dom, :desc,
|
||||||
:desc, 22, :ssh_user, :ssh_method, :tier,
|
22, :ssh_user, :ssh_method, :tier, :patch_freq, :patch_excludes,
|
||||||
:patch_freq, :patch_excludes, :domain_ltd,
|
:domain_ltd, :pref_jour, :pref_heure)"""), vals)
|
||||||
:pref_jour, :pref_heure)
|
|
||||||
"""), vals)
|
|
||||||
db.flush()
|
db.flush()
|
||||||
# Get new server ID and insert IP
|
|
||||||
if vals["ip"]:
|
if vals["ip"]:
|
||||||
new_srv = db.execute(text("SELECT id FROM servers WHERE hostname=:h"), {"h": hostname}).fetchone()
|
new_srv = db.execute(text("SELECT id FROM servers WHERE hostname=:h"), {"h": hostname}).fetchone()
|
||||||
if new_srv:
|
if new_srv:
|
||||||
_upsert_ip(db, new_srv.id, vals["ip"])
|
_upsert_ip(db, new_srv.id, vals["ip"])
|
||||||
|
stats["ips"] += 1
|
||||||
stats["servers_created"] += 1
|
stats["servers_created"] += 1
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.rollback()
|
db.rollback()
|
||||||
stats["errors"].append(f"Server {hostname}: {e}")
|
stats["errors"].append(f"VM {hostname}: {str(e)[:80]}")
|
||||||
|
|
||||||
# ─── 8. Insert physical servers ───
|
# ─── 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:
|
for s in phys:
|
||||||
hostname = s.get("name", "").split(".")[0].lower()
|
hostname = s.get("name", "").split(".")[0].lower()
|
||||||
if not hostname:
|
if not hostname:
|
||||||
continue
|
continue
|
||||||
existing = db.execute(text("SELECT id FROM servers WHERE LOWER(hostname)=LOWER(:h)"),
|
existing = db.execute(text("SELECT id FROM servers WHERE LOWER(hostname)=LOWER(:h)"), {"h": hostname}).fetchone()
|
||||||
{"h": hostname}).fetchone()
|
|
||||||
if not existing:
|
if not existing:
|
||||||
try:
|
try:
|
||||||
contacts = s.get("contacts_list", [])
|
contacts = s.get("contacts_list", [])
|
||||||
resp = contacts[0].get("contact_id_friendlyname", "") if contacts else ""
|
resp = contacts[0].get("contact_id_friendlyname", "") if contacts else ""
|
||||||
db.execute(text("""
|
db.execute(text("""INSERT INTO servers (hostname, fqdn, os_family, os_version, machine_type,
|
||||||
INSERT INTO servers (hostname, fqdn, os_family, os_version, machine_type,
|
etat, responsable_nom, commentaire, site, ssh_port, ssh_user, ssh_method, tier)
|
||||||
etat, responsable_nom, commentaire, ssh_port, ssh_user, ssh_method, tier)
|
VALUES (:h, :f, :osf, :osv, 'physical', 'en_production', :resp, :desc, :site,
|
||||||
VALUES (:h, :f, :osf, :osv, 'physical', 'en_production', :resp, :desc,
|
22, 'root', 'ssh_key', 'tier0')"""),
|
||||||
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",
|
||||||
"h": hostname, "f": s.get("name", hostname),
|
"osv": s.get("osversion_id_friendlyname", ""),
|
||||||
"osf": "linux" if "linux" in s.get("osfamily_id_friendlyname", "").lower() else "windows",
|
"resp": resp, "desc": s.get("description", ""),
|
||||||
"osv": s.get("osversion_id_friendlyname", ""),
|
"site": s.get("location_name", "")})
|
||||||
"resp": resp, "desc": s.get("description", ""),
|
|
||||||
})
|
|
||||||
db.flush()
|
db.flush()
|
||||||
ip = s.get("managementip", "")
|
ip = s.get("managementip", "")
|
||||||
if ip:
|
if ip:
|
||||||
new_srv = db.execute(text("SELECT id FROM servers WHERE hostname=:h"), {"h": hostname}).fetchone()
|
new_srv = db.execute(text("SELECT id FROM servers WHERE hostname=:h"), {"h": hostname}).fetchone()
|
||||||
if new_srv:
|
if new_srv:
|
||||||
_upsert_ip(db, new_srv.id, ip)
|
_upsert_ip(db, new_srv.id, ip)
|
||||||
|
stats["ips"] += 1
|
||||||
stats["servers_created"] += 1
|
stats["servers_created"] += 1
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.rollback()
|
db.rollback()
|
||||||
stats["errors"].append(f"Physical {hostname}: {e}")
|
stats["errors"].append(f"Phys {hostname}: {str(e)[:80]}")
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
log.info(f"iTop full sync: {stats}")
|
_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
|
return stats
|
||||||
|
|
||||||
|
|
||||||
|
# ══════════════════════════════════════════════════════════
|
||||||
|
# EXPORT: PatchCenter → iTop
|
||||||
|
# ══════════════════════════════════════════════════════════
|
||||||
def sync_to_itop(db, itop_url, itop_user, itop_pass):
|
def sync_to_itop(db, itop_url, itop_user, itop_pass):
|
||||||
"""Exporte referentiel + serveurs de PatchCenter vers iTop"""
|
"""Exporte referentiel + serveurs + champs patching vers iTop"""
|
||||||
client = ITopClient(itop_url, itop_user, itop_pass)
|
client = ITopClient(itop_url, itop_user, itop_pass)
|
||||||
stats = {"updated": 0, "created": 0, "ref_created": 0, "errors": []}
|
stats = {"ref_created": 0, "servers_updated": 0, "servers_created": 0, "errors": []}
|
||||||
|
|
||||||
# ─── Sync referentiel: environments, domains, zones ───
|
# ─── 1. Sync typologies: create missing in iTop ───
|
||||||
for pc_table, itop_class in [("environments", "Environnement"), ("domains", "DomaineApplicatif"), ("zones", "Zone")]:
|
typo_map = [
|
||||||
# Get existing in iTop
|
("environments", "Environnement"),
|
||||||
existing_itop = set()
|
("domains", "DomaineApplicatif"),
|
||||||
for item in client.get_all(itop_class, "name"):
|
("zones", "Zone"),
|
||||||
existing_itop.add(item.get("name", "").lower())
|
]
|
||||||
|
for pc_table, itop_class in typo_map:
|
||||||
# Get PatchCenter values
|
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()
|
rows = db.execute(text(f"SELECT name FROM {pc_table} ORDER BY name")).fetchall()
|
||||||
for row in rows:
|
for row in rows:
|
||||||
name = row.name
|
if row.name.lower() not in existing_itop:
|
||||||
if name.lower() not in existing_itop:
|
r = client.create(itop_class, {"name": row.name, "org_id": "SELECT Organization WHERE name = 'MPCZ'"})
|
||||||
r = client.create(itop_class, {
|
|
||||||
"name": name,
|
|
||||||
"org_id": "SELECT Organization WHERE name = 'MPCZ'",
|
|
||||||
})
|
|
||||||
if r.get("code") == 0:
|
if r.get("code") == 0:
|
||||||
stats["ref_created"] += 1
|
stats["ref_created"] += 1
|
||||||
existing_itop.add(name.lower())
|
existing_itop.add(row.name.lower())
|
||||||
else:
|
else:
|
||||||
stats["errors"].append(f"{itop_class} '{name}': {r.get('message', '')[:60]}")
|
stats["errors"].append(f"{itop_class} '{row.name}': {r.get('message', '')[:60]}")
|
||||||
|
|
||||||
# Get iTop VMs indexed by short name
|
# ─── 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 = {}
|
itop_vms = {}
|
||||||
for v in client.get_all("VirtualMachine", "name"):
|
for v in client.get_all("VirtualMachine", "name"):
|
||||||
itop_vms[v["name"].split(".")[0].lower()] = v
|
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",
|
status_map = {"en_production": "production", "decommissionne": "obsolete",
|
||||||
"stock": "stock", "en_cours": "implementation"}
|
"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:
|
for srv in rows:
|
||||||
hostname = (srv.hostname or "").lower()
|
hostname = (srv.hostname or "").lower()
|
||||||
itop_vm = itop_vms.get(hostname)
|
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 itop_vm:
|
||||||
fields = {}
|
|
||||||
if srv.etat:
|
|
||||||
fields["status"] = status_map.get(srv.etat, "production")
|
|
||||||
if srv.commentaire:
|
|
||||||
fields["description"] = srv.commentaire
|
|
||||||
if fields:
|
if fields:
|
||||||
r = client.update("VirtualMachine", itop_vm["itop_id"], fields)
|
r = client.update("VirtualMachine", itop_vm["itop_id"], fields)
|
||||||
if r.get("code") == 0:
|
if r.get("code") == 0:
|
||||||
stats["updated"] += 1
|
stats["servers_updated"] += 1
|
||||||
else:
|
else:
|
||||||
stats["errors"].append(f"{hostname}: {r.get('message', '')[:60]}")
|
stats["errors"].append(f"Update {hostname}: {r.get('message', '')[:60]}")
|
||||||
else:
|
else:
|
||||||
# Create VM in iTop
|
fields["name"] = srv.hostname
|
||||||
r = client.create("VirtualMachine", {
|
fields["org_id"] = "SELECT Organization WHERE name = 'MPCZ'"
|
||||||
"name": srv.hostname,
|
if not fields.get("status"):
|
||||||
"org_id": "SELECT Organization WHERE name = 'MPCZ'",
|
fields["status"] = "production"
|
||||||
"status": status_map.get(srv.etat, "production"),
|
r = client.create("VirtualMachine", fields)
|
||||||
"description": srv.commentaire or f"OS: {srv.os_version or 'N/A'}",
|
|
||||||
})
|
|
||||||
if r.get("code") == 0:
|
if r.get("code") == 0:
|
||||||
stats["created"] += 1
|
stats["servers_created"] += 1
|
||||||
else:
|
else:
|
||||||
stats["errors"].append(f"Create {hostname}: {r.get('message', '')[:60]}")
|
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}")
|
log.info(f"iTop export: {stats}")
|
||||||
return stats
|
return stats
|
||||||
|
|||||||
@ -9,6 +9,54 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Bandeau Synchro iTop (visible sur tous les onglets) -->
|
||||||
|
{% if can_modify %}
|
||||||
|
<div class="card mb-4" style="padding:12px 16px;">
|
||||||
|
<div style="display:flex; align-items:center; gap:12px; flex-wrap:wrap;">
|
||||||
|
<span class="text-sm font-bold" style="color:#00d4ff; margin-right:4px">Synchronisation iTop</span>
|
||||||
|
<form method="POST" action="/referentiel/itop/sync-from" style="display:inline">
|
||||||
|
<button type="submit" class="btn-primary" style="padding:6px 16px;font-size:0.85rem"
|
||||||
|
onclick="this.disabled=true;this.textContent='Import en cours...';this.form.submit()">
|
||||||
|
⬇ Importer depuis iTop
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<form method="POST" action="/referentiel/itop/sync-to" style="display:inline">
|
||||||
|
<button type="submit" style="padding:6px 16px;font-size:0.85rem;background:#334155;color:#e2e8f0;border:1px solid #475569;border-radius:6px;cursor:pointer"
|
||||||
|
onclick="this.disabled=true;this.textContent='Export en cours...';this.form.submit()">
|
||||||
|
⬆ Exporter vers iTop
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{% if 'itop_from' in request.query_params.get('msg','') %}
|
||||||
|
<span class="text-sm text-green-400">✓ Import OK</span>
|
||||||
|
{% elif 'itop_to' in request.query_params.get('msg','') %}
|
||||||
|
<span class="text-sm text-green-400">✓ Export OK</span>
|
||||||
|
{% elif request.query_params.get('msg') == 'itop_noconfig' %}
|
||||||
|
<span class="text-sm text-red-400">✗ Configurer iTop dans Settings</span>
|
||||||
|
{% elif request.query_params.get('msg') == 'itop_error' %}
|
||||||
|
<span class="text-sm text-red-400">✗ Erreur synchro</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div style="display:flex; gap:24px; margin-top:6px;">
|
||||||
|
{% if last_sync_from %}
|
||||||
|
<span class="text-xs text-gray-500">Dernier import : <span style="color:#8f8">{{ last_sync_from.date }}</span>
|
||||||
|
— {{ last_sync_from.stats.servers_created|default(0) }} créés,
|
||||||
|
{{ last_sync_from.stats.servers_updated|default(0) }} maj,
|
||||||
|
{{ last_sync_from.stats.contacts|default(0) }} contacts</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-xs text-gray-500">Dernier import : <span style="color:#ff6b6b">jamais</span></span>
|
||||||
|
{% endif %}
|
||||||
|
{% if last_sync_to %}
|
||||||
|
<span class="text-xs text-gray-500">Dernier export : <span style="color:#8f8">{{ last_sync_to.date }}</span>
|
||||||
|
— {{ last_sync_to.stats.servers_updated|default(0) }} maj,
|
||||||
|
{{ last_sync_to.stats.servers_created|default(0) }} créés,
|
||||||
|
{{ last_sync_to.stats.ref_created|default(0) }} réf</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-xs text-gray-500">Dernier export : <span style="color:#ff6b6b">jamais</span></span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% set msg = request.query_params.get('msg', '') %}
|
{% set msg = request.query_params.get('msg', '') %}
|
||||||
{% set detail = request.query_params.get('detail', '') %}
|
{% set detail = request.query_params.get('detail', '') %}
|
||||||
{% if msg == 'added' %}
|
{% if msg == 'added' %}
|
||||||
@ -48,31 +96,6 @@
|
|||||||
<!-- ONGLET DOMAINES -->
|
<!-- ONGLET DOMAINES -->
|
||||||
<!-- ============================================================ -->
|
<!-- ============================================================ -->
|
||||||
{% if tab == 'domains' %}
|
{% if tab == 'domains' %}
|
||||||
<!-- Sync iTop -->
|
|
||||||
{% if can_modify %}
|
|
||||||
<div class="card mb-4" style="padding:12px 16px; display:flex; align-items:center; gap:12px; flex-wrap:wrap;">
|
|
||||||
<span class="text-sm text-gray-400" style="margin-right:8px">Synchronisation iTop :</span>
|
|
||||||
<form method="POST" action="/referentiel/itop/sync-from" style="display:inline">
|
|
||||||
<button type="submit" class="btn-primary" style="padding:6px 16px;font-size:0.85rem"
|
|
||||||
onclick="this.disabled=true;this.textContent='Sync en cours...';this.form.submit()">
|
|
||||||
Importer depuis iTop
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
<form method="POST" action="/referentiel/itop/sync-to" style="display:inline">
|
|
||||||
<button type="submit" style="padding:6px 16px;font-size:0.85rem;background:#334155;color:#e2e8f0;border:1px solid #475569;border-radius:6px;cursor:pointer"
|
|
||||||
onclick="this.disabled=true;this.textContent='Export en cours...';this.form.submit()">
|
|
||||||
Exporter vers iTop
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
{% if 'itop_from' in request.query_params.get('msg','') %}
|
|
||||||
<span class="text-sm text-green-400">Import iTop OK</span>
|
|
||||||
{% elif 'itop_to' in request.query_params.get('msg','') %}
|
|
||||||
<span class="text-sm text-green-400">Export iTop OK</span>
|
|
||||||
{% elif request.query_params.get('msg') == 'itop_noconfig' %}
|
|
||||||
<span class="text-sm text-red-400">Configurer iTop dans Settings</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<table class="table-cyber w-full">
|
<table class="table-cyber w-full">
|
||||||
<thead><tr>
|
<thead><tr>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user