Compare commits

..

No commits in common. "7066a2feeb698227c4246874549494f28dce3d47" and "05df4c0dd86095ea69679b2e021dfece55013e4f" have entirely different histories.

8 changed files with 103 additions and 1591 deletions

4
.gitignore vendored
View file

@ -9,10 +9,6 @@ cmdb/snapshots/
# Certificats générés (contiennent des clés privées) # Certificats générés (contiennent des clés privées)
cert-generator/LISTS_ALL_CERT/ cert-generator/LISTS_ALL_CERT/
# Images et fichiers importés dans les templates email (contenu utilisateur)
password-generator/images/
password-generator/files/
# === Environnement / build === # === Environnement / build ===
.venv/ .venv/
__pycache__/ __pycache__/

View file

@ -5,9 +5,8 @@ Portail d'outils pour administrateurs systèmes et réseau, servi par un serveur
- **cmdb** — inventaire (machines, réseaux, IP/MAC, salles & baies), recherche Active Directory, import/export CSV, snapshots de la base - **cmdb** — inventaire (machines, réseaux, IP/MAC, salles & baies), recherche Active Directory, import/export CSV, snapshots de la base
- **cert-generator** — génération de certificats SSL (clés RSA, CSR) - **cert-generator** — génération de certificats SSL (clés RSA, CSR)
- **switch-config** — mapper de VLAN et export de configuration switch - **switch-config** — mapper de VLAN et export de configuration switch
- **password-generator** — génération de mots de passe aléatoires, passphrases et mots de passe temporaires (préfixe configurable, envoi par mail via l'AD + templates)
Le tout accessible depuis un seul serveur, avec navigation entre les apps. Le tout accessible depuis un seul serveur, avec navigation entre les 3 apps.
## Prérequis ## Prérequis

View file

@ -184,7 +184,6 @@
<a href="../cmdb/cmdb.html"><div class="psw-ico"></div><div class="psw-txt"><b>CMDB</b><small>Machines, réseaux, salles</small></div></a> <a href="../cmdb/cmdb.html"><div class="psw-ico"></div><div class="psw-txt"><b>CMDB</b><small>Machines, réseaux, salles</small></div></a>
<a class="active" href="dashboard.html"><div class="psw-ico">🔐</div><div class="psw-txt"><b>Générateur certificats</b><small>Clés RSA, CSR, SSL</small></div></a> <a class="active" href="dashboard.html"><div class="psw-ico">🔐</div><div class="psw-txt"><b>Générateur certificats</b><small>Clés RSA, CSR, SSL</small></div></a>
<a href="../switch-config/switch_vlan_mapper.html"><div class="psw-ico">🔌</div><div class="psw-txt"><b>Configuration switch</b><small>VLANs, export config</small></div></a> <a href="../switch-config/switch_vlan_mapper.html"><div class="psw-ico">🔌</div><div class="psw-txt"><b>Configuration switch</b><small>VLANs, export config</small></div></a>
<a href="../password-generator/password_generator.html"><div class="psw-ico">🔑</div><div class="psw-txt"><b>Mots de passe</b><small>Générateur &amp; envoi</small></div></a>
</div> </div>
<div class="psw" style="margin-top:auto;padding-top:14px;border-top:1px solid var(--line)"> <div class="psw" style="margin-top:auto;padding-top:14px;border-top:1px solid var(--line)">
<a href="../cmdb/preferences.html"><div class="psw-ico"></div><div class="psw-txt"><b>Préférences</b><small>Types, tickets, réglages</small></div></a> <a href="../cmdb/preferences.html"><div class="psw-ico"></div><div class="psw-txt"><b>Préférences</b><small>Types, tickets, réglages</small></div></a>

2
cmdb

@ -1 +1 @@
Subproject commit 805f0fe00b1314970a95c717c2945102130a0fd4 Subproject commit 18751d75c88a32790a50c1f0a458a2657db6a2ad

File diff suppressed because it is too large Load diff

355
server.py
View file

@ -1,6 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
Serveur unifié du portail Infra (CMDB + Certificats + Switch + Mots de passe). Serveur unifié du portail Infra (CMDB + Certificats + Switch).
Un seul serveur, lancé à la racine du projet : Un seul serveur, lancé à la racine du projet :
@ -11,15 +11,11 @@ Un seul serveur, lancé à la racine du projet :
- CMDB : base partagée cmdb.db sourcée automatiquement (/db, versionnée) - CMDB : base partagée cmdb.db sourcée automatiquement (/db, versionnée)
- Générateur de certificats : écrit sur le disque dans - Générateur de certificats : écrit sur le disque dans
cert-generator/LISTS_ALL_CERT/ (créé au besoin), aucun prompt cert-generator/LISTS_ALL_CERT/ (créé au besoin), aucun prompt
- Sert les 4 applications + le switcher (liens relatifs OK) - Sert les 3 applications + le switcher (liens relatifs OK)
- Recherche AD/LDAP de la CMDB conservée (nécessite `pip install ldap3`) - Recherche AD/LDAP de la CMDB conservée (nécessite `pip install ldap3`)
- Générateur de mots de passe : envoi de mails via POST /mail/send (SMTP
configuré dans la page, module standard smtplib)
""" """
import sys, os, json, base64, shutil, re, smtplib, threading, mimetypes, sqlite3 import sys, os, json, base64, shutil, re, threading
from email.message import EmailMessage
from email.utils import formataddr
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import urlparse, parse_qs from urllib.parse import urlparse, parse_qs
@ -31,13 +27,6 @@ CMDB_DIR = os.path.join(HERE, "cmdb")
CERT_DIR = os.path.join(HERE, "cert-generator") CERT_DIR = os.path.join(HERE, "cert-generator")
WORK = os.path.join(CERT_DIR, "LISTS_ALL_CERT") # dossier de travail certs (disque) WORK = os.path.join(CERT_DIR, "LISTS_ALL_CERT") # dossier de travail certs (disque)
ARCH = os.path.join(WORK, "archives") ARCH = os.path.join(WORK, "archives")
PWGEN_DIR = os.path.join(HERE, "password-generator")
PWGEN_IMG = os.path.join(PWGEN_DIR, "images") # images des templates email (disque)
PWGEN_IMG_URL = "/password-generator/images/" # préfixe d'URL servi (chdir HERE)
PWGEN_FILES = os.path.join(PWGEN_DIR, "files") # fichiers joignables (rdp, pdf…)
PWGEN_FILES_URL = "/password-generator/files/"
MAX_IMG_BYTES = 2 * 1024 * 1024 # 2 Mo par image
MAX_FILE_BYTES = 10 * 1024 * 1024 # 10 Mo par fichier joint
# réutilise la logique CMDB existante (base partagée cmdb.db + LDAP) # réutilise la logique CMDB existante (base partagée cmdb.db + LDAP)
sys.path.insert(0, CMDB_DIR) sys.path.insert(0, CMDB_DIR)
@ -57,175 +46,6 @@ def ensure_work():
os.makedirs(WORK, exist_ok=True) os.makedirs(WORK, exist_ok=True)
def read_vlans():
"""Lit les VLANs depuis la table `networks` de la base CMDB partagée
(cmdb.db). Un réseau devient un VLAN dès qu'il porte un numéro de VLAN.
Renvoie une liste triée : [{"vlan": int, "name": str, "color": str}, ...].
Source de vérité unique pour le mapper de switch (remplace vlan_database.js)."""
out = []
try:
con = sqlite3.connect("file:%s?mode=ro" % C.DB_PATH, uri=True)
except sqlite3.Error:
return out
try:
con.row_factory = sqlite3.Row
for r in con.execute(
"SELECT vlan, name, color FROM networks "
"WHERE vlan IS NOT NULL ORDER BY vlan"
):
out.append(
{
"vlan": r["vlan"],
"name": r["name"] or ("VLAN_%s" % r["vlan"]),
"color": r["color"] or "#8B5CF6",
}
)
except sqlite3.Error:
pass # table absente / base illisible → liste vide, repli côté client
finally:
con.close()
return out
# ---------------------------------------------------------------------------
# Cache du listing des certificats.
# Les certs sont des fichiers sur disque (LISTS_ALL_CERT/<dir>/<fichiers>).
# Sans cache, chaque GET /api/list re-scanne tout le disque (N×M syscalls) →
# lent quand il y a beaucoup de certs. On mémorise le résultat et on ne le
# reconstruit que si le contenu a changé :
# - empreinte légère (mtime des sous-dossiers) → détecte ajout/suppression
# et toute écriture passant par le serveur (le mtime du dossier bouge) ;
# - invalidation explicite après chaque write/delete → couvre l'écrasement
# d'un fichier en place (même nom), que le mtime du dossier ne voit pas.
# ---------------------------------------------------------------------------
_CERT_CACHE = {"sig": None, "payload": None}
_CERT_CACHE_LOCK = threading.Lock()
def invalidate_cert_cache():
with _CERT_CACHE_LOCK:
_CERT_CACHE["sig"] = None
_CERT_CACHE["payload"] = None
def _cert_signature():
"""Empreinte du dossier certs : (nom, mtime_ns) de chaque sous-dossier.
Ne descend pas dans les fichiers peu de syscalls. Renvoie None si le
dossier est illisible (force alors une reconstruction)."""
sig = []
try:
with os.scandir(WORK) as it:
for e in it:
try:
sig.append((e.name, e.stat().st_mtime_ns))
except OSError:
continue
except OSError:
return None
sig.sort()
return tuple(sig)
def _build_cert_list():
"""Reconstruit le listing complet via os.scandir (le stat est replié dans la
lecture du dossier bien moins de syscalls que listdir + stat séparés)."""
certs, archives = [], []
try:
with os.scandir(WORK) as it:
entries = sorted(it, key=lambda e: e.name)
except OSError:
entries = []
for entry in entries:
if entry.name == "archives":
if entry.is_dir():
try:
with os.scandir(entry.path) as ait:
for a in sorted(ait, key=lambda e: e.name):
if a.is_file():
st = a.stat()
archives.append(
{
"name": a.name,
"size": st.st_size,
"mtime": int(st.st_mtime * 1000),
}
)
except OSError:
pass
continue
if not entry.is_dir():
continue
files, date = [], 0
try:
with os.scandir(entry.path) as sit:
subs = sorted(sit, key=lambda e: e.name)
except OSError:
continue
for f in subs:
if not f.is_file():
continue
st = f.stat()
m = int(st.st_mtime * 1000)
date = max(date, m)
files.append(
{
"name": f.name,
"ext": (f.name.rsplit(".", 1)[-1] if "." in f.name else "").lower(),
"size": st.st_size,
"mtime": m,
}
)
if files:
certs.append({"dir": entry.name, "files": files, "date": date})
archives.sort(key=lambda a: -a["mtime"])
certs.sort(key=lambda c: -c["date"])
return {"ok": True, "certs": certs, "archives": archives}
# src d'images pointant vers le dossier local des templates (chemin absolu ou relatif)
_LOCAL_IMG_SRC = re.compile(r'src="([^"]*?/password-generator/images/([^"?#]+))"', re.I)
def inline_local_images(html):
"""Remplace les <img src="…/password-generator/images/x.png"> par des data
URI (lecture du fichier sur disque). Les images data:/http(s): sont laissées
telles quelles. Rend le mail HTML autonome au moment de l'envoi."""
def repl(m):
fname = os.path.basename(m.group(2)) # anti-traversal
path = os.path.join(PWGEN_IMG, fname)
if not os.path.isfile(path):
return m.group(0)
mime = mimetypes.guess_type(path)[0] or "application/octet-stream"
with open(path, "rb") as f:
data = base64.b64encode(f.read()).decode("ascii")
return 'src="data:%s;base64,%s"' % (mime, data)
return _LOCAL_IMG_SRC.sub(repl, html)
def attach_files(msg, urls):
"""Joint au mail les fichiers du dossier password-generator/files/ dont l'URL
figure dans la liste `urls` (pièces jointes du template). Le nom présenté au
destinataire est le nom d'origine (suffixe aléatoire de stockage retiré)."""
seen = set()
for url in urls or []:
fname = os.path.basename(str(url)) # anti-traversal
if not fname or fname in seen:
continue
seen.add(fname)
path = os.path.join(PWGEN_FILES, fname)
if not os.path.isfile(path):
continue
ctype = mimetypes.guess_type(path)[0] or "application/octet-stream"
maintype, _, subtype = ctype.partition("/")
disp = re.sub(r"-[0-9a-f]{8}(\.[^.]+)$", r"\1", fname) # nom d'origine lisible
with open(path, "rb") as f:
msg.add_attachment(
f.read(), maintype=maintype, subtype=subtype or "octet-stream", filename=disp
)
class Handler(SimpleHTTPRequestHandler): class Handler(SimpleHTTPRequestHandler):
def log_message(self, *a): def log_message(self, *a):
pass pass
@ -286,10 +106,6 @@ class Handler(SimpleHTTPRequestHandler):
return self._send_db(200, C.db_bytes(), C.db_version()) return self._send_db(200, C.db_bytes(), C.db_version())
if self.path == "/db/version": if self.path == "/db/version":
return self._json({"version": C.db_version()}) return self._json({"version": C.db_version()})
# VLANs = réseaux CMDB (table networks) → consommés par le mapper switch
if self.path == "/api/vlans":
with C.DB_LOCK:
return self._json(read_vlans())
if self.path == "/db/snapshots": if self.path == "/db/snapshots":
return self._json({"snapshots": C.list_snapshots()}) return self._json({"snapshots": C.list_snapshots()})
if self.path.startswith("/db/snapshot?"): if self.path.startswith("/db/snapshot?"):
@ -361,23 +177,6 @@ class Handler(SimpleHTTPRequestHandler):
) )
except Exception as ex: except Exception as ex:
return self._json({"error": str(ex)}, 500) return self._json({"error": str(ex)}, 500)
# envoi de mail (générateur de mots de passe)
if self.path == "/mail/send":
try:
return self.mail_send()
except Exception as e:
return self._err(e, 500)
# upload d'image / de fichier joint (générateur de mots de passe)
if self.path == "/pwgen/upload-image":
try:
return self.pwgen_upload_image()
except Exception as e:
return self._err(e, 500)
if self.path == "/pwgen/upload-file":
try:
return self.pwgen_upload_file()
except Exception as e:
return self._err(e, 500)
# API certs (écriture) # API certs (écriture)
api = self._cert_api() api = self._cert_api()
try: try:
@ -393,113 +192,49 @@ class Handler(SimpleHTTPRequestHandler):
return self._err(e, 500) return self._err(e, 500)
self._err("route inconnue: %s" % self.path, 404) self._err("route inconnue: %s" % self.path, 404)
# ---------- mail (générateur de mots de passe) ----------
def mail_send(self):
"""Envoie un mail texte via le serveur SMTP fourni par la page.
Body JSON : { smtp: {host, port, security, user, pass, from, from_name},
to, subject, body }
security : none | starttls | ssl
"""
d = self._body()
smtp = d.get("smtp") or {}
to = (d.get("to") or "").strip()
host = (smtp.get("host") or "").strip()
if not to or "@" not in to:
raise ValueError("destinataire invalide: %r" % to)
if not host:
raise ValueError("serveur SMTP non configuré")
security = (smtp.get("security") or "starttls").lower()
try:
port = int(smtp.get("port") or 0)
except (TypeError, ValueError):
port = 0
if not port:
port = {"ssl": 465, "starttls": 587}.get(security, 25)
sender = (smtp.get("from") or smtp.get("user") or "").strip()
if not sender:
raise ValueError("adresse expéditeur non configurée")
msg = EmailMessage()
from_name = (smtp.get("from_name") or "").strip()
msg["From"] = formataddr((from_name, sender)) if from_name else sender
msg["To"] = to
msg["Subject"] = d.get("subject") or ""
msg.set_content(d.get("body") or "")
html = (d.get("html") or "").strip()
if html:
# les images de templates stockées sur disque sont ré-intégrées en
# data URI pour que le mail reçu soit autonome (pas de lien vers le
# serveur, images visibles hors du réseau).
html = inline_local_images(html)
msg.add_alternative(html, subtype="html")
# pièces jointes du template (ex : .rdp de connexion)
attach_files(msg, d.get("attachments"))
if security == "ssl":
server = smtplib.SMTP_SSL(host, port, timeout=15)
else:
server = smtplib.SMTP(host, port, timeout=15)
try:
if security == "starttls":
server.starttls()
user = (smtp.get("user") or "").strip()
if user:
server.login(user, smtp.get("pass") or "")
server.send_message(msg)
finally:
try:
server.quit()
except Exception:
pass
self._json({"ok": True, "to": to})
# ---------- uploads de templates (disque réel) ----------
def _save_upload(self, dest_dir, url_prefix, max_bytes, default_ext, kind):
"""Stocke un fichier posté ({name, b64}) sur le disque avec un nom sûr
(radical assaini + suffixe aléatoire) et renvoie son URL servie."""
d = self._body()
raw = (d.get("name") or kind).strip()
try:
data = base64.b64decode(d.get("b64") or "", validate=True)
except (ValueError, TypeError):
raise ValueError("%s invalide (base64)" % kind)
if not data:
raise ValueError("%s vide" % kind)
if len(data) > max_bytes:
raise ValueError("%s trop lourd (max %d Mo)" % (kind, max_bytes // (1024 * 1024)))
base, _, ext = raw.rpartition(".")
ext = re.sub(r"[^A-Za-z0-9]", "", ext).lower()[:8] or default_ext
stem = re.sub(r"[^A-Za-z0-9._-]", "_", base or kind)[:40].strip("._-") or kind
fname = "%s-%s.%s" % (stem, os.urandom(4).hex(), ext)
os.makedirs(dest_dir, exist_ok=True)
with open(os.path.join(dest_dir, fname), "wb") as f:
f.write(data)
self._json({"ok": True, "url": url_prefix + fname, "name": fname})
def pwgen_upload_image(self):
"""Image de template → password-generator/images/ (ré-intégrée en data URI à l'envoi)."""
self._save_upload(PWGEN_IMG, PWGEN_IMG_URL, MAX_IMG_BYTES, "png", "image")
def pwgen_upload_file(self):
"""Fichier joignable (rdp, pdf…) → password-generator/files/ (joint au mail à l'envoi)."""
self._save_upload(PWGEN_FILES, PWGEN_FILES_URL, MAX_FILE_BYTES, "bin", "fichier")
# ---------- certificats (disque réel) ---------- # ---------- certificats (disque réel) ----------
def cert_list(self): def cert_list(self):
ensure_work() ensure_work()
sig = _cert_signature() certs, archives = [], []
with _CERT_CACHE_LOCK: for entry in sorted(os.listdir(WORK)):
if ( full = os.path.join(WORK, entry)
sig is not None if entry == "archives":
and sig == _CERT_CACHE["sig"] if os.path.isdir(full):
and _CERT_CACHE["payload"] is not None for a in sorted(os.listdir(full)):
): af = os.path.join(full, a)
return _CERT_CACHE["payload"] # inchangé → réponse mémorisée if os.path.isfile(af):
payload = _build_cert_list() st = os.stat(af)
_CERT_CACHE["sig"] = sig archives.append(
_CERT_CACHE["payload"] = payload {
return payload "name": a,
"size": st.st_size,
"mtime": int(st.st_mtime * 1000),
}
)
continue
if not os.path.isdir(full):
continue
files, date = [], 0
for fn in sorted(os.listdir(full)):
ff = os.path.join(full, fn)
if not os.path.isfile(ff):
continue
st = os.stat(ff)
m = int(st.st_mtime * 1000)
date = max(date, m)
files.append(
{
"name": fn,
"ext": (fn.rsplit(".", 1)[-1] if "." in fn else "").lower(),
"size": st.st_size,
"mtime": m,
}
)
if files:
certs.append({"dir": entry, "files": files, "date": date})
archives.sort(key=lambda a: -a["mtime"])
certs.sort(key=lambda c: -c["date"])
return {"ok": True, "certs": certs, "archives": archives}
def cert_write_text(self): def cert_write_text(self):
d = self._body() d = self._body()
@ -514,7 +249,6 @@ class Handler(SimpleHTTPRequestHandler):
newline="", newline="",
) as f: ) as f:
f.write(content) f.write(content)
invalidate_cert_cache()
self._json({"ok": True, "path": "LISTS_ALL_CERT/%s/" % dname}) self._json({"ok": True, "path": "LISTS_ALL_CERT/%s/" % dname})
def cert_write_bin(self): def cert_write_bin(self):
@ -526,7 +260,6 @@ class Handler(SimpleHTTPRequestHandler):
os.makedirs(os.path.dirname(target), exist_ok=True) os.makedirs(os.path.dirname(target), exist_ok=True)
with open(target, "wb") as f: with open(target, "wb") as f:
f.write(base64.b64decode(d["b64"])) f.write(base64.b64decode(d["b64"]))
invalidate_cert_cache()
self._json({"ok": True, "path": "LISTS_ALL_CERT/%s" % "/".join(parts)}) self._json({"ok": True, "path": "LISTS_ALL_CERT/%s" % "/".join(parts)})
def cert_delete_dir(self): def cert_delete_dir(self):
@ -537,12 +270,10 @@ class Handler(SimpleHTTPRequestHandler):
shutil.rmtree(target) shutil.rmtree(target)
if d.get("archives"): if d.get("archives"):
self._rm_archives(dname) self._rm_archives(dname)
invalidate_cert_cache()
self._json({"ok": True}) self._json({"ok": True})
def cert_delete_archives(self): def cert_delete_archives(self):
self._rm_archives(safe_name(self._body()["dir"])) self._rm_archives(safe_name(self._body()["dir"]))
invalidate_cert_cache()
self._json({"ok": True}) self._json({"ok": True})
def _rm_archives(self, dname): def _rm_archives(self, dname):

View file

@ -596,6 +596,7 @@ button:active { transform: scale(0.98); }
background: #101216; color: #d4d4d4; background: #101216; color: #d4d4d4;
} }
</style> </style>
<script src="vlan_database.js" defer></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
</head> </head>
<body> <body>
@ -607,7 +608,6 @@ button:active { transform: scale(0.98); }
<a href="../cmdb/cmdb.html"><div class="psw-ico"></div><div class="psw-txt"><b>CMDB</b><small>Machines, réseaux, salles</small></div></a> <a href="../cmdb/cmdb.html"><div class="psw-ico"></div><div class="psw-txt"><b>CMDB</b><small>Machines, réseaux, salles</small></div></a>
<a href="../cert-generator/dashboard.html"><div class="psw-ico">🔐</div><div class="psw-txt"><b>Générateur certificats</b><small>Clés RSA, CSR, SSL</small></div></a> <a href="../cert-generator/dashboard.html"><div class="psw-ico">🔐</div><div class="psw-txt"><b>Générateur certificats</b><small>Clés RSA, CSR, SSL</small></div></a>
<a class="active" href="switch_vlan_mapper.html"><div class="psw-ico">🔌</div><div class="psw-txt"><b>Configuration switch</b><small>VLANs, export config</small></div></a> <a class="active" href="switch_vlan_mapper.html"><div class="psw-ico">🔌</div><div class="psw-txt"><b>Configuration switch</b><small>VLANs, export config</small></div></a>
<a href="../password-generator/password_generator.html"><div class="psw-ico">🔑</div><div class="psw-txt"><b>Mots de passe</b><small>Générateur &amp; envoi</small></div></a>
</div> </div>
<div class="psw" style="margin-top:auto;padding-top:14px;border-top:1px solid var(--border)"> <div class="psw" style="margin-top:auto;padding-top:14px;border-top:1px solid var(--border)">
<a href="../cmdb/preferences.html"><div class="psw-ico"></div><div class="psw-txt"><b>Préférences</b><small>Types, tickets, réglages</small></div></a> <a href="../cmdb/preferences.html"><div class="psw-ico"></div><div class="psw-txt"><b>Préférences</b><small>Types, tickets, réglages</small></div></a>
@ -810,35 +810,51 @@ const DEFAULT_VLAN_DATABASE = `1 = Native = #8B5CF6
200 = VLAN200 = #16A34A 200 = VLAN200 = #16A34A
300 = VLAN300 = #0EA5E9`; 300 = VLAN300 = #0EA5E9`;
// Charger la base de données VLAN au démarrage. // Charger la base de données VLAN au démarrage
// Source de vérité : la table `networks` de la CMDB (SQL), exposée par le function loadVlanDatabase() {
// portail via GET /api/vlans. Chaque réseau CMDB portant un numéro de VLAN // 1⃣ Chercher d'abord dans localStorage (changements sauvegardés par l'utilisateur)
// devient une entrée (nom + couleur). Remplace l'ancien vlan_database.js. const savedDbText = localStorage.getItem('vlanDatabaseText');
// Replis (mode file:// ou serveur injoignable) : base par défaut intégrée. if (savedDbText) {
async function loadVlanDatabase() { parseVlanDatabase(savedDbText);
if (!location.href.startsWith('file://')) { console.log('✓ Base de données chargée depuis le stockage local');
try {
const res = await fetch('/api/vlans', { cache: 'no-store' });
if (res.ok) {
const rows = await res.json();
vlanDatabase = {};
rows.forEach(r => {
if (r.vlan === null || r.vlan === undefined || r.vlan === '') return;
vlanDatabase[String(r.vlan)] = {
name: r.name || `VLAN_${r.vlan}`,
color: r.color || '#8B5CF6'
};
});
console.log(`✓ ${Object.keys(vlanDatabase).length} VLAN(s) chargés depuis la CMDB (table networks)`);
return; return;
} }
console.warn('CMDB /api/vlans a répondu', res.status, '→ base VLAN par défaut');
} catch (error) { // 2⃣ Vérifier si vlan_database.js a été chargé (fonctionne en mode file://)
console.warn('CMDB injoignable, base VLAN par défaut :', error); if (typeof VLAN_DATABASE_FILE !== 'undefined') {
parseVlanDatabase(VLAN_DATABASE_FILE);
localStorage.setItem('vlanDatabaseText', VLAN_DATABASE_FILE);
console.log('✓ Base de données chargée depuis vlan_database.js');
return;
} }
}
// Mode file:// ou serveur indisponible : base par défaut intégrée // 3⃣ Essayer de charger depuis le fichier externe (serveur HTTP uniquement)
if (!location.href.startsWith('file://')) {
const xhr = new XMLHttpRequest();
xhr.open('GET', 'vlan_database.txt', true);
xhr.onload = function() {
if (xhr.status === 200) {
parseVlanDatabase(xhr.responseText);
localStorage.setItem('vlanDatabaseText', xhr.responseText);
console.log('✓ Base de données chargée depuis vlan_database.txt');
} else {
loadDefaultDatabase(); loadDefaultDatabase();
}
};
xhr.onerror = function() {
loadDefaultDatabase();
};
try {
xhr.send();
return;
} catch (error) {
loadDefaultDatabase();
}
} else {
// Mode file:// et aucun vlan_database.js trouvé - utiliser DB par défaut
loadDefaultDatabase();
}
} }
function loadDefaultDatabase() { function loadDefaultDatabase() {
@ -2459,10 +2475,8 @@ window.addEventListener('resize', () => {
}); });
// Charger la base de données au démarrage // Charger la base de données au démarrage
window.addEventListener('load', async () => { window.addEventListener('load', () => {
// Attendre les VLANs (table networks CMDB) avant toute analyse : une conf loadVlanDatabase();
// reçue via le hash s'analyse aussitôt et a besoin des noms/couleurs.
await loadVlanDatabase();
syncCmdbDbStatus(); // version de la base CMDB (partagée via localStorage) syncCmdbDbStatus(); // version de la base CMDB (partagée via localStorage)
// Config transmise par la CMDB via l'URL (#cfg=...) : pré-remplir et analyser // Config transmise par la CMDB via l'URL (#cfg=...) : pré-remplir et analyser

View file

@ -0,0 +1,11 @@
// Fichier de base de données VLAN - À charger dans le HTML
// Placer ce fichier dans le même dossier que switch_vlan_mapper.html
// Format : <id> = <nom> = <couleur hex> (exemple générique, à adapter)
const VLAN_DATABASE_FILE = `10 = MGMT = #6366f1
20 = SERVERS = #22c55e
30 = USERS = #06b6d4
40 = VOICE = #2dd4bf
50 = DMZ = #8b5cf6
99 = GUEST = #f97316
`;