- Admin applications: CRUD module (list/add/edit/delete/assign/multi-app) avec push iTop bidirectionnel (applications.py + 3 templates) - Correspondance prod<->hors-prod: migration vers server_correspondance globale, suppression ancien code quickwin, ajout filtre environnement et solution applicative, colonne environnement dans builder - Servers page: colonne application_name + equivalent(s) via get_links_bulk, filtre application_id, push iTop sur changement application - Patching: bulk_update_application, bulk_update_excludes, validations - Fix paramiko sftp.put (remote_path -> positional arg) - Tools: wiki_to_pdf.py (DokuWiki -> PDF) + generate_ppt.py (PPTX 19 slides DSI patching) + contenu source (processus_patching.txt, script_presentation.txt) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
679 lines
32 KiB
Python
679 lines
32 KiB
Python
"""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 _normalize_os_for_itop(os_string):
|
||
"""Normalise PRETTY_NAME vers un nom propre pour iTop OSVersion.
|
||
Ex: 'Debian GNU/Linux 12 (bookworm)' → 'Debian 12 (Bookworm)'
|
||
'CentOS Stream 9' → 'CentOS Stream 9'
|
||
'Red Hat Enterprise Linux 9.4 (Plow)' → 'RHEL 9.4 (Plow)'
|
||
"""
|
||
import re
|
||
s = os_string.strip()
|
||
# Debian GNU/Linux X (codename) → Debian X (Codename)
|
||
m = re.match(r'Debian GNU/Linux (\d+)\s*\((\w+)\)', s, re.I)
|
||
if m:
|
||
return f"Debian {m.group(1)} ({m.group(2).capitalize()})"
|
||
# Ubuntu X.Y LTS
|
||
m = re.match(r'Ubuntu (\d+\.\d+(?:\.\d+)?)\s*(LTS)?', s, re.I)
|
||
if m:
|
||
lts = " LTS" if m.group(2) else ""
|
||
return f"Ubuntu {m.group(1)}{lts}"
|
||
# Red Hat Enterprise Linux [Server] [release] X → RHEL X
|
||
m = re.match(r'Red Hat Enterprise Linux\s*(?:Server)?\s*(?:release)?\s*(\d+[\.\d]*)\s*\(?([\w]*)\)?', s, re.I)
|
||
if m:
|
||
codename = f" ({m.group(2).capitalize()})" if m.group(2) else ""
|
||
return f"RHEL {m.group(1)}{codename}"
|
||
# CentOS Stream release X → CentOS Stream X
|
||
m = re.match(r'CentOS Stream\s+release\s+(\d+[\.\d]*)', s, re.I)
|
||
if m:
|
||
return f"CentOS Stream {m.group(1)}"
|
||
# CentOS Linux X.Y → CentOS X.Y
|
||
m = re.match(r'CentOS\s+Linux\s+(\d+[\.\d]*)', s, re.I)
|
||
if m:
|
||
return f"CentOS {m.group(1)}"
|
||
# Rocky Linux X.Y (codename) → Rocky Linux X.Y
|
||
m = re.match(r'Rocky\s+Linux\s+(\d+[\.\d]*)', s, re.I)
|
||
if m:
|
||
return f"Rocky Linux {m.group(1)}"
|
||
# Oracle Linux / Oracle Enterprise Linux
|
||
m = re.match(r'Oracle\s+(?:Enterprise\s+)?Linux\s+(\d+[\.\d]*)', s, re.I)
|
||
if m:
|
||
return f"Oracle Linux {m.group(1)}"
|
||
# Fallback: remove "release" word
|
||
return re.sub(r'\s+release\s+', ' ', s).strip()
|
||
|
||
|
||
def _upsert_ip(db, server_id, ip):
|
||
if not ip:
|
||
return
|
||
# Remove all old itop-managed primary IPs for this server (keep only the current one)
|
||
db.execute(text(
|
||
"DELETE FROM server_ips WHERE server_id=:sid AND ip_type='primary' AND description='itop' AND ip_address != :ip"),
|
||
{"sid": server_id, "ip": ip})
|
||
# Check if this exact IP already exists
|
||
exact = db.execute(text(
|
||
"SELECT id FROM server_ips WHERE server_id=:sid AND ip_address=:ip"),
|
||
{"sid": server_id, "ip": ip}).fetchone()
|
||
if exact:
|
||
return
|
||
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 (filtre périmètre IT uniquement) ───
|
||
persons = client.get_all("Person", "name,first_name,email,phone,org_name,function,status")
|
||
|
||
# Périmètre IT : teams à synchroniser (configurable via settings)
|
||
from .secrets_service import get_secret as _gs
|
||
it_teams_raw = _gs(db, "itop_contact_teams")
|
||
it_teams_list = ["SecOps", "iPOP", "Externe", "DSI", "Admin DSI"]
|
||
if it_teams_raw:
|
||
it_teams_list = [t.strip() for t in it_teams_raw.split(",") if t.strip()]
|
||
|
||
# Map person → (itop_id, team)
|
||
person_info = {} # fullname_lower -> {itop_id, team}
|
||
team_members = {} # pour responsables domaine×env plus bas
|
||
|
||
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", "DSI": "referent_technique",
|
||
"Admin DSI": "referent_technique"}
|
||
|
||
# Set des itop_id et emails vus dans le périmètre IT (pour désactiver les autres)
|
||
seen_itop_ids = set()
|
||
seen_emails = set()
|
||
stats["contacts_deactivated"] = 0
|
||
|
||
for p in persons:
|
||
fullname = f"{p.get('first_name','')} {p.get('name','')}".strip()
|
||
email = p.get("email", "")
|
||
if not email:
|
||
continue
|
||
team = team_members.get(fullname.lower(), "")
|
||
# Filtre : ne synchroniser que les persons dans le périmètre IT
|
||
if team not in it_teams_list:
|
||
continue
|
||
role = team_role_map.get(team, "referent_technique")
|
||
itop_id = p.get("itop_id")
|
||
phone = p.get("phone", "")
|
||
function = p.get("function", "")
|
||
# Status iTop : active/inactive
|
||
itop_status = (p.get("status", "") or "").lower()
|
||
is_active = itop_status != "inactive"
|
||
|
||
if itop_id:
|
||
seen_itop_ids.add(int(itop_id))
|
||
seen_emails.add(email.lower())
|
||
|
||
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, itop_id=:iid,
|
||
telephone=:tel, team=:t, function=:f, is_active=:a, updated_at=NOW() WHERE id=:id"""),
|
||
{"id": existing.id, "n": fullname, "r": role, "iid": itop_id,
|
||
"tel": phone, "t": team, "f": function, "a": is_active})
|
||
else:
|
||
try:
|
||
db.execute(text("""INSERT INTO contacts (name, email, role, itop_id,
|
||
telephone, team, function, is_active) VALUES (:n, :e, :r, :iid, :tel, :t, :f, :a)"""),
|
||
{"n": fullname, "e": email, "r": role, "iid": itop_id,
|
||
"tel": phone, "t": team, "f": function, "a": is_active})
|
||
stats["contacts"] += 1
|
||
except Exception:
|
||
db.rollback()
|
||
|
||
# Désactiver les contacts iTop qui ne sont plus dans le périmètre (plus dans les teams IT)
|
||
# Critère : a un itop_id mais n'a pas été vu dans le sync
|
||
if seen_itop_ids:
|
||
placeholders = ",".join(str(i) for i in seen_itop_ids)
|
||
r = db.execute(text(f"""UPDATE contacts SET is_active=false, updated_at=NOW()
|
||
WHERE itop_id IS NOT NULL AND itop_id NOT IN ({placeholders}) AND is_active=true"""))
|
||
stats["contacts_deactivated"] = r.rowcount
|
||
|
||
# Désactiver les users PatchCenter liés à des contacts devenus inactifs
|
||
stats["users_deactivated"] = 0
|
||
r = db.execute(text("""UPDATE users SET is_active=false, updated_at=NOW()
|
||
WHERE is_active=true AND itop_person_id IN
|
||
(SELECT itop_id FROM contacts WHERE is_active=false AND itop_id IS NOT NULL)"""))
|
||
stats["users_deactivated"] = r.rowcount
|
||
|
||
# ─── 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": {}})
|
||
|
||
# ─── 7bis. ApplicationSolutions ───
|
||
crit_map = {"high": "haute", "critical": "critique", "medium": "standard", "low": "basse"}
|
||
itop_apps = client.get_all("ApplicationSolution", "name,description,business_criticity,status")
|
||
for app in itop_apps:
|
||
iid = app.get("itop_id")
|
||
name = (app.get("name") or "")[:50]
|
||
full = (app.get("name") or "")[:200]
|
||
desc = (app.get("description") or "")[:500]
|
||
crit = crit_map.get((app.get("business_criticity") or "").lower(), "basse")
|
||
st = (app.get("status") or "active")[:30]
|
||
try:
|
||
db.execute(text("""INSERT INTO applications (itop_id, nom_court, nom_complet, description, criticite, status)
|
||
VALUES (:iid, :n, :nc, :d, :c, :s)
|
||
ON CONFLICT (itop_id) DO UPDATE SET nom_court=EXCLUDED.nom_court,
|
||
nom_complet=EXCLUDED.nom_complet, description=EXCLUDED.description,
|
||
criticite=EXCLUDED.criticite, status=EXCLUDED.status, updated_at=NOW()"""),
|
||
{"iid": iid, "n": name, "nc": full, "d": desc, "c": crit, "s": st})
|
||
stats["applications"] = stats.get("applications", 0) + 1
|
||
except Exception:
|
||
db.rollback()
|
||
db.commit()
|
||
app_by_itop_id = {r.itop_id: r.id for r in db.execute(text(
|
||
"SELECT id, itop_id FROM applications WHERE itop_id IS NOT NULL")).fetchall()}
|
||
|
||
# ─── 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,"
|
||
"applicationsolution_list")
|
||
|
||
# PatchCenter etat = iTop status (meme enum: production, implementation, stock, obsolete, eol)
|
||
itop_status = {"production": "production", "stock": "stock",
|
||
"implementation": "implementation", "obsolete": "obsolete",
|
||
"eol": "eol"}
|
||
|
||
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", "")
|
||
|
||
# ApplicationSolution (première app si plusieurs)
|
||
app_id = None
|
||
app_name = None
|
||
apps_list = v.get("applicationsolution_list") or []
|
||
if apps_list:
|
||
first = apps_list[0]
|
||
try:
|
||
itop_aid = int(first.get("applicationsolution_id", 0))
|
||
app_id = app_by_itop_id.get(itop_aid)
|
||
app_name = first.get("applicationsolution_name", "")
|
||
except (ValueError, TypeError):
|
||
pass
|
||
|
||
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", ""), "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,
|
||
"app_id": app_id, "app_name": app_name,
|
||
}
|
||
|
||
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,
|
||
application_id=:app_id, application_name=:app_name,
|
||
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
|
||
contacts = s.get("contacts_list", [])
|
||
resp = contacts[0].get("contact_id_friendlyname", "") if contacts else ""
|
||
ip = s.get("managementip", "")
|
||
osf = "linux" if "linux" in s.get("osfamily_id_friendlyname", "").lower() else "windows"
|
||
osv = s.get("osversion_id_friendlyname", "")
|
||
|
||
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=:f, os_family=:osf, os_version=:osv,
|
||
responsable_nom=:resp, commentaire=:desc, site=:site, updated_at=NOW()
|
||
WHERE id=:sid"""),
|
||
{"f": s.get("name", hostname), "osf": osf, "osv": osv,
|
||
"resp": resp, "desc": s.get("description", ""),
|
||
"site": s.get("location_name", ""), "sid": existing.id})
|
||
if ip:
|
||
_upsert_ip(db, existing.id, ip)
|
||
stats["servers_updated"] += 1
|
||
else:
|
||
try:
|
||
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', 'production', :resp, :desc, :site,
|
||
22, 'root', 'ssh_key', 'tier0')"""),
|
||
{"h": hostname, "f": s.get("name", hostname), "osf": osf, "osv": osv,
|
||
"resp": resp, "desc": s.get("description", ""),
|
||
"site": s.get("location_name", "")})
|
||
db.flush()
|
||
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 = {"production": "production", "implementation": "implementation",
|
||
"stock": "stock", "obsolete": "obsolete", "eol": "eol"}
|
||
tier_map = {"tier0": "Tier 0", "tier1": "Tier 1", "tier2": "Tier 2", "tier3": "Tier 3"}
|
||
|
||
# Build OSVersion cache: name.lower() → itop_id
|
||
itop_osversions = {}
|
||
for ov in client.get_all("OSVersion", "name,osfamily_name"):
|
||
itop_osversions[ov["name"].lower()] = ov
|
||
|
||
# Build OSFamily cache
|
||
itop_osfamilies = {}
|
||
for of in client.get_all("OSFamily", "name"):
|
||
itop_osfamilies[of["name"].lower()] = of["itop_id"]
|
||
|
||
# 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 s.hostname, s.fqdn, s.os_version, s.os_family, s.etat, s.commentaire, s.tier,
|
||
s.ssh_method, s.ssh_user, s.patch_frequency, s.patch_excludes, s.domain_ltd,
|
||
s.pref_patch_jour, s.pref_patch_heure, s.responsable_nom, s.referent_nom,
|
||
s.application_id,
|
||
(SELECT si.ip_address::text FROM server_ips si WHERE si.server_id = s.id AND si.ip_type = 'primary' LIMIT 1) as mgmt_ip,
|
||
(SELECT sa.audit_date FROM server_audit sa WHERE sa.server_id = s.id ORDER BY sa.audit_date DESC LIMIT 1) as last_audit_date,
|
||
(SELECT a.itop_id FROM applications a WHERE a.id = s.application_id) as app_itop_id
|
||
FROM servers s WHERE s.machine_type='vm'""")).fetchall()
|
||
|
||
for srv in rows:
|
||
hostname = (srv.hostname or "").lower()
|
||
itop_vm = itop_vms.get(hostname)
|
||
|
||
fields = {}
|
||
if srv.mgmt_ip:
|
||
fields["managementip"] = srv.mgmt_ip.split("/")[0]
|
||
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}'"
|
||
# Date dernier audit
|
||
if srv.last_audit_date:
|
||
fields["last_patch_date"] = str(srv.last_audit_date)[:10]
|
||
# ApplicationSolution : pousser si défini (replace la liste)
|
||
if srv.app_itop_id:
|
||
fields["applicationsolution_list"] = [{"applicationsolution_id": int(srv.app_itop_id)}]
|
||
# OS version — chercher/créer dans iTop
|
||
if srv.os_version:
|
||
osv_name = _normalize_os_for_itop(srv.os_version)
|
||
osf_name = "Linux" if srv.os_family == "linux" else "Windows" if srv.os_family == "windows" else "Linux"
|
||
match = itop_osversions.get(osv_name.lower())
|
||
if match:
|
||
fields["osversion_id"] = match["itop_id"]
|
||
else:
|
||
# Créer l'OSVersion dans iTop
|
||
osf_id = itop_osfamilies.get(osf_name.lower())
|
||
if osf_id:
|
||
cr = client.create("OSVersion", {"name": osv_name, "osfamily_id": osf_id})
|
||
if cr.get("code") == 0 and cr.get("objects"):
|
||
new_id = list(cr["objects"].values())[0]["key"]
|
||
fields["osversion_id"] = new_id
|
||
itop_osversions[osv_name.lower()] = {"itop_id": new_id, "name": osv_name}
|
||
stats["ref_created"] += 1
|
||
# 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
|