chore(server): reformat code and improve mise tooling config

- Normalise le style (guillemets doubles, black/ruff)
- mise.toml : ajoute [tools], tâches dev/clean, port par défaut
- server.py : fallback PORT via variable d'environnement
This commit is contained in:
Guillaume DENIS 2026-07-04 18:12:34 +02:00 committed by TheHackdes
parent 3654cba41d
commit 05df4c0dd8
5 changed files with 245 additions and 172 deletions

View file

@ -11,15 +11,11 @@ Le tout accessible depuis un seul serveur, avec navigation entre les 3 apps.
## Prérequis ## Prérequis
- [mise](https://mise.jdx.dev/) — lanceur de tâches - [mise](https://mise.jdx.dev/) — lanceur de tâches
- [uv](https://docs.astral.sh/uv/) — gestion des dépendances et de Python
`uv` installe automatiquement **Python 3.13** et les dépendances (`ldap3`) au premier lancement — rien d'autre à installer. Installer mise :
Installer mise et uv :
```bash ```bash
curl https://mise.run | sh # mise curl https://mise.run | sh # mise
curl -LsSf https://astral.sh/uv/install.sh | sh # uv
``` ```
## Installation ## Installation
@ -27,8 +23,8 @@ curl -LsSf https://astral.sh/uv/install.sh | sh # uv
Le dossier `cmdb` est un sous-module git — cloner avec `--recurse-submodules` : Le dossier `cmdb` est un sous-module git — cloner avec `--recurse-submodules` :
```bash ```bash
git clone --recurse-submodules <url-du-repo> git clone --recurse-submodules https://git.thehackdes.com/TheHackdes/Dashboard_Admin_Sys.git
cd VERSION2 cd Dashboard_Admin_Sys
``` ```
Sous-module déjà cloné sans l'option : Sous-module déjà cloné sans l'option :

View file

@ -12,6 +12,7 @@ Usage :
Sert la RACINE du projet (parent), donc les 3 apps + le switcher fonctionnent. Sert la RACINE du projet (parent), donc les 3 apps + le switcher fonctionnent.
""" """
import sys, os, json, base64, shutil, re import sys, os, json, base64, shutil, re
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
@ -19,61 +20,69 @@ PORT = int(sys.argv[1]) if len(sys.argv) > 1 else 8000
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) # .../cert-generator SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) # .../cert-generator
ROOT = os.path.dirname(SCRIPT_DIR) # racine projet ROOT = os.path.dirname(SCRIPT_DIR) # racine projet
WORK = os.path.join(SCRIPT_DIR, 'LISTS_ALL_CERT') # dossier de travail (disque) WORK = os.path.join(SCRIPT_DIR, "LISTS_ALL_CERT") # dossier de travail (disque)
ARCH = os.path.join(WORK, 'archives') ARCH = os.path.join(WORK, "archives")
SAFE = re.compile(r"^[A-Za-z0-9._@\- ]+$") # noms de dossier/fichier autorisés
SAFE = re.compile(r'^[A-Za-z0-9._@\- ]+$') # noms de dossier/fichier autorisés
def safe_name(n): def safe_name(n):
n = (n or '').strip() n = (n or "").strip()
if not n or n in ('.', '..') or '/' in n or '\\' in n or not SAFE.match(n): if not n or n in (".", "..") or "/" in n or "\\" in n or not SAFE.match(n):
raise ValueError('nom invalide: %r' % n) raise ValueError("nom invalide: %r" % n)
return n return n
def ensure_work(): def ensure_work():
os.makedirs(WORK, exist_ok=True) os.makedirs(WORK, exist_ok=True)
class Handler(SimpleHTTPRequestHandler): class Handler(SimpleHTTPRequestHandler):
def _json(self, obj, code=200): def _json(self, obj, code=200):
body = json.dumps(obj).encode('utf-8') body = json.dumps(obj).encode("utf-8")
self.send_response(code) self.send_response(code)
self.send_header('Content-Type', 'application/json') self.send_header("Content-Type", "application/json")
self.send_header('Content-Length', str(len(body))) self.send_header("Content-Length", str(len(body)))
self.send_header('Cache-Control', 'no-store') self.send_header("Cache-Control", "no-store")
self.end_headers() self.end_headers()
self.wfile.write(body) self.wfile.write(body)
def _err(self, msg, code=400): def _err(self, msg, code=400):
self._json({'ok': False, 'error': str(msg)}, code) self._json({"ok": False, "error": str(msg)}, code)
def _body(self): def _body(self):
n = int(self.headers.get('Content-Length', 0)) n = int(self.headers.get("Content-Length", 0))
return json.loads(self.rfile.read(n) or b'{}') return json.loads(self.rfile.read(n) or b"{}")
def _api(self): def _api(self):
p = self.path.split('?', 1)[0] p = self.path.split("?", 1)[0]
return p.split('/api/', 1)[1] if '/api/' in p else None return p.split("/api/", 1)[1] if "/api/" in p else None
# ---------- GET : API list/ping, sinon fichiers statiques ---------- # ---------- GET : API list/ping, sinon fichiers statiques ----------
def do_GET(self): def do_GET(self):
api = self._api() api = self._api()
if api == 'ping': if api == "ping":
return self._json({'ok': True, 'workdir': 'LISTS_ALL_CERT'}) return self._json({"ok": True, "workdir": "LISTS_ALL_CERT"})
if api == 'list': if api == "list":
return self._json(self.list_all()) return self._json(self.list_all())
return super().do_GET() return super().do_GET()
def do_POST(self): def do_POST(self):
api = self._api() api = self._api()
try: try:
if api == 'write': return self.write_text() if api == "write":
if api == 'write_bin': return self.write_bin() return self.write_text()
if api == 'delete': return self.delete_dir() if api == "write_bin":
if api == 'delete_file': return self.delete_file() return self.write_bin()
if api == 'delete_archives': return self.delete_archives() if api == "delete":
return self.delete_dir()
if api == "delete_file":
return self.delete_file()
if api == "delete_archives":
return self.delete_archives()
except Exception as e: except Exception as e:
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)
# ---------- implémentations ---------- # ---------- implémentations ----------
def list_all(self): def list_all(self):
@ -81,14 +90,19 @@ class Handler(SimpleHTTPRequestHandler):
certs, archives = [], [] certs, archives = [], []
for entry in sorted(os.listdir(WORK)): for entry in sorted(os.listdir(WORK)):
full = os.path.join(WORK, entry) full = os.path.join(WORK, entry)
if entry == 'archives': if entry == "archives":
if os.path.isdir(full): if os.path.isdir(full):
for a in sorted(os.listdir(full)): for a in sorted(os.listdir(full)):
af = os.path.join(full, a) af = os.path.join(full, a)
if os.path.isfile(af): if os.path.isfile(af):
st = os.stat(af) st = os.stat(af)
archives.append({'name': a, 'size': st.st_size, archives.append(
'mtime': int(st.st_mtime * 1000)}) {
"name": a,
"size": st.st_size,
"mtime": int(st.st_mtime * 1000),
}
)
continue continue
if not os.path.isdir(full): if not os.path.isdir(full):
continue continue
@ -100,75 +114,84 @@ class Handler(SimpleHTTPRequestHandler):
st = os.stat(ff) st = os.stat(ff)
m = int(st.st_mtime * 1000) m = int(st.st_mtime * 1000)
date = max(date, m) date = max(date, m)
files.append({'name': fn, 'ext': (fn.rsplit('.', 1)[-1] if '.' in fn else '').lower(), files.append(
'size': st.st_size, 'mtime': m}) {
"name": fn,
"ext": (fn.rsplit(".", 1)[-1] if "." in fn else "").lower(),
"size": st.st_size,
"mtime": m,
}
)
if files: if files:
certs.append({'dir': entry, 'files': files, 'date': date}) certs.append({"dir": entry, "files": files, "date": date})
archives.sort(key=lambda a: -a['mtime']) archives.sort(key=lambda a: -a["mtime"])
certs.sort(key=lambda c: -c['date']) certs.sort(key=lambda c: -c["date"])
return {'ok': True, 'certs': certs, 'archives': archives} return {"ok": True, "certs": certs, "archives": archives}
def write_text(self): def write_text(self):
d = self._body() d = self._body()
dname = safe_name(d['dir']) dname = safe_name(d["dir"])
target = os.path.join(WORK, dname) target = os.path.join(WORK, dname)
os.makedirs(target, exist_ok=True) os.makedirs(target, exist_ok=True)
for fname, content in (d.get('files') or {}).items(): for fname, content in (d.get("files") or {}).items():
fn = safe_name(fname) fn = safe_name(fname)
with open(os.path.join(target, fn), 'w', encoding='utf-8', newline='') as f: with open(os.path.join(target, fn), "w", encoding="utf-8", newline="") as f:
f.write(content) f.write(content)
self._json({'ok': True, 'path': 'LISTS_ALL_CERT/%s/' % dname}) self._json({"ok": True, "path": "LISTS_ALL_CERT/%s/" % dname})
def write_bin(self): def write_bin(self):
d = self._body() d = self._body()
# path = "archives/<fichier>.zip" OU "<dir>/<fichier>" # path = "archives/<fichier>.zip" OU "<dir>/<fichier>"
parts = [safe_name(x) for x in d['path'].split('/') if x != ''] parts = [safe_name(x) for x in d["path"].split("/") if x != ""]
if not parts: if not parts:
raise ValueError('path vide') raise ValueError("path vide")
target = os.path.join(WORK, *parts) target = os.path.join(WORK, *parts)
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"]))
self._json({'ok': True, 'path': 'LISTS_ALL_CERT/%s' % '/'.join(parts)}) self._json({"ok": True, "path": "LISTS_ALL_CERT/%s" % "/".join(parts)})
def delete_dir(self): def delete_dir(self):
d = self._body() d = self._body()
dname = safe_name(d['dir']) dname = safe_name(d["dir"])
target = os.path.join(WORK, dname) target = os.path.join(WORK, dname)
if os.path.isdir(target): if os.path.isdir(target):
shutil.rmtree(target) shutil.rmtree(target)
if d.get('archives'): if d.get("archives"):
self._rm_archives(dname) self._rm_archives(dname)
self._json({'ok': True}) self._json({"ok": True})
def delete_file(self): def delete_file(self):
d = self._body() d = self._body()
target = os.path.join(WORK, safe_name(d['dir']), safe_name(d['name'])) target = os.path.join(WORK, safe_name(d["dir"]), safe_name(d["name"]))
if os.path.isfile(target): if os.path.isfile(target):
os.remove(target) os.remove(target)
self._json({'ok': True}) self._json({"ok": True})
def delete_archives(self): def delete_archives(self):
d = self._body() d = self._body()
self._rm_archives(safe_name(d['dir'])) self._rm_archives(safe_name(d["dir"]))
self._json({'ok': True}) self._json({"ok": True})
def _rm_archives(self, dname): def _rm_archives(self, dname):
low = dname.lower() low = dname.lower()
if os.path.isdir(ARCH): if os.path.isdir(ARCH):
for a in os.listdir(ARCH): for a in os.listdir(ARCH):
if low in a.lower(): if low in a.lower():
try: os.remove(os.path.join(ARCH, a)) try:
except OSError: pass os.remove(os.path.join(ARCH, a))
except OSError:
pass
def log_message(self, *a): def log_message(self, *a):
pass # silencieux pass # silencieux
if __name__ == '__main__':
if __name__ == "__main__":
ensure_work() ensure_work()
os.chdir(ROOT) os.chdir(ROOT)
url = 'http://127.0.0.1:%d/cert-generator/dashboard.html' % PORT url = "http://127.0.0.1:%d/cert-generator/dashboard.html" % PORT
print('Générateur Certificats : %s' % url) print("Générateur Certificats : %s" % url)
print('Dossier disque : %s (créé automatiquement)' % WORK) print("Dossier disque : %s (créé automatiquement)" % WORK)
print('Ctrl+C pour arrêter.') print("Ctrl+C pour arrêter.")
ThreadingHTTPServer(('0.0.0.0', PORT), Handler).serve_forever() ThreadingHTTPServer(("0.0.0.0", PORT), Handler).serve_forever()

2
cmdb

@ -1 +1 @@
Subproject commit b06252daad728064a212411a684e2e95a1a5006e Subproject commit 18751d75c88a32790a50c1f0a458a2657db6a2ad

View file

@ -1,5 +1,22 @@
# Portail Infra — mise config
# https://mise.jdx.dev
[tools]
python = "3.13"
uv = "latest"
[env]
# Port par défaut du serveur (surchargé par l'argument CLI de server.py)
PORT = "8000"
[tasks.run] [tasks.run]
description = "Run server" description = "Lancer le serveur du portail (CMDB + Certificats + Switch)"
run = ''' run = "uv run server.py {{arg(name='port', default='')}}"
uv run server.py
''' [tasks.dev]
description = "Lancer le serveur en watch (redémarre à chaque modif .py)"
run = "uv run --with watchfiles watchfiles 'python server.py' server.py cmdb"
[tasks.clean]
description = "Nettoyer les caches Python"
run = "find . -type d -name __pycache__ -prune -exec rm -rf {} + ; rm -rf .ruff_cache"

235
server.py
View file

@ -14,30 +14,34 @@ Un seul serveur, lancé à la racine du projet :
- Sert les 3 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`)
""" """
import sys, os, json, base64, shutil, re, threading import sys, os, json, base64, shutil, re, threading
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
PORT = int(sys.argv[1]) if len(sys.argv) > 1 else 8000 _arg = sys.argv[1].strip() if len(sys.argv) > 1 else ""
PORT = int(_arg) if _arg else int(os.environ.get("PORT", 8000))
HERE = os.path.dirname(os.path.abspath(__file__)) # racine projet HERE = os.path.dirname(os.path.abspath(__file__)) # racine projet
CMDB_DIR = os.path.join(HERE, 'cmdb') 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")
# 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)
import cmdb_server as C # ne démarre rien (guarded __main__) import cmdb_server as C # ne démarre rien (guarded __main__)
SAFE = re.compile(r'^[A-Za-z0-9._@\- ]+$') SAFE = re.compile(r"^[A-Za-z0-9._@\- ]+$")
def safe_name(n): def safe_name(n):
n = (n or '').strip() n = (n or "").strip()
if not n or n in ('.', '..') or '/' in n or '\\' in n or not SAFE.match(n): if not n or n in (".", "..") or "/" in n or "\\" in n or not SAFE.match(n):
raise ValueError('nom invalide: %r' % n) raise ValueError("nom invalide: %r" % n)
return n return n
def ensure_work(): def ensure_work():
os.makedirs(WORK, exist_ok=True) os.makedirs(WORK, exist_ok=True)
@ -48,130 +52,145 @@ class Handler(SimpleHTTPRequestHandler):
# ---------- helpers ---------- # ---------- helpers ----------
def _json(self, obj, code=200): def _json(self, obj, code=200):
body = json.dumps(obj).encode('utf-8') body = json.dumps(obj).encode("utf-8")
self.send_response(code) self.send_response(code)
self.send_header('Content-Type', 'application/json') self.send_header("Content-Type", "application/json")
self.send_header('Content-Length', str(len(body))) self.send_header("Content-Length", str(len(body)))
self.send_header('Cache-Control', 'no-store') self.send_header("Cache-Control", "no-store")
self.end_headers() self.end_headers()
self.wfile.write(body) self.wfile.write(body)
def _err(self, msg, code=400): def _err(self, msg, code=400):
self._json({'ok': False, 'error': str(msg)}, code) self._json({"ok": False, "error": str(msg)}, code)
def _send_db(self, code, data, version): def _send_db(self, code, data, version):
self.send_response(code) self.send_response(code)
self.send_header('Content-Type', 'application/octet-stream') self.send_header("Content-Type", "application/octet-stream")
self.send_header('X-DB-Version', str(version)) self.send_header("X-DB-Version", str(version))
self.send_header('Cache-Control', 'no-store') self.send_header("Cache-Control", "no-store")
self.send_header('Content-Length', str(len(data))) self.send_header("Content-Length", str(len(data)))
self.end_headers() self.end_headers()
if data: if data:
self.wfile.write(data) self.wfile.write(data)
def _send_download(self, data, filename): def _send_download(self, data, filename):
self.send_response(200) self.send_response(200)
self.send_header('Content-Type', 'application/octet-stream') self.send_header("Content-Type", "application/octet-stream")
self.send_header('Content-Disposition', 'attachment; filename="%s"' % filename) self.send_header("Content-Disposition", 'attachment; filename="%s"' % filename)
self.send_header('Cache-Control', 'no-store') self.send_header("Cache-Control", "no-store")
self.send_header('Content-Length', str(len(data))) self.send_header("Content-Length", str(len(data)))
self.end_headers() self.end_headers()
self.wfile.write(data) self.wfile.write(data)
def _body(self): def _body(self):
n = int(self.headers.get('Content-Length', 0)) n = int(self.headers.get("Content-Length", 0))
return json.loads(self.rfile.read(n) or b'{}') return json.loads(self.rfile.read(n) or b"{}")
def _cert_api(self): def _cert_api(self):
p = self.path.split('?', 1)[0] p = self.path.split("?", 1)[0]
if '/cert-generator/api/' in p: if "/cert-generator/api/" in p:
return p.split('/api/', 1)[1] return p.split("/api/", 1)[1]
return None return None
# ---------- GET ---------- # ---------- GET ----------
def do_GET(self): def do_GET(self):
# racine → CMDB # racine → CMDB
if self.path in ('/', '/index.html'): if self.path in ("/", "/index.html"):
self.send_response(302) self.send_response(302)
self.send_header('Location', '/cmdb/cmdb.html') self.send_header("Location", "/cmdb/cmdb.html")
self.end_headers() self.end_headers()
return return
# base CMDB partagée # base CMDB partagée
if self.path == '/db': if self.path == "/db":
with C.DB_LOCK: with C.DB_LOCK:
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()})
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?"):
name = (parse_qs(urlparse(self.path).query).get('name') or [''])[0] name = (parse_qs(urlparse(self.path).query).get("name") or [""])[0]
try: try:
data = C.snapshot_bytes(name) data = C.snapshot_bytes(name)
except (ValueError, OSError): except (ValueError, OSError):
return self._json({'error': 'snapshot introuvable'}, 404) return self._json({"error": "snapshot introuvable"}, 404)
return self._send_download(data, os.path.basename(name)) return self._send_download(data, os.path.basename(name))
# API certs (lecture) # API certs (lecture)
api = self._cert_api() api = self._cert_api()
if api == 'ping': if api == "ping":
return self._json({'ok': True, 'workdir': 'LISTS_ALL_CERT'}) return self._json({"ok": True, "workdir": "LISTS_ALL_CERT"})
if api == 'list': if api == "list":
return self._json(self.cert_list()) return self._json(self.cert_list())
return super().do_GET() return super().do_GET()
# ---------- POST ---------- # ---------- POST ----------
def do_POST(self): def do_POST(self):
# base CMDB # base CMDB
if self.path == '/db': if self.path == "/db":
data = self.rfile.read(int(self.headers.get('Content-Length', 0))) data = self.rfile.read(int(self.headers.get("Content-Length", 0)))
base = int(self.headers.get('X-Base-Version', '0') or 0) base = int(self.headers.get("X-Base-Version", "0") or 0)
cur = C.db_version() cur = C.db_version()
if base != cur: if base != cur:
return self._send_db(409, C.db_bytes(), cur) return self._send_db(409, C.db_bytes(), cur)
return self._send_db(200, b'', C.db_write_and_snapshot(data)) return self._send_db(200, b"", C.db_write_and_snapshot(data))
# snapshots CMDB (création / suppression / restauration) # snapshots CMDB (création / suppression / restauration)
if self.path in ('/db/snapshot', '/db/snapshot/delete', '/db/restore'): if self.path in ("/db/snapshot", "/db/snapshot/delete", "/db/restore"):
body = self._body() body = self._body()
if self.path == '/db/snapshot': if self.path == "/db/snapshot":
meta = C.create_snapshot(label=body.get('label') or 'manual') meta = C.create_snapshot(label=body.get("label") or "manual")
if not meta: if not meta:
return self._json({'error': 'base vide — rien à sauvegarder'}, 400) return self._json({"error": "base vide — rien à sauvegarder"}, 400)
return self._json(meta) return self._json(meta)
if self.path == '/db/snapshot/delete': if self.path == "/db/snapshot/delete":
try: try:
C.delete_snapshot(body.get('name') or '') C.delete_snapshot(body.get("name") or "")
except (ValueError, OSError): except (ValueError, OSError):
return self._json({'error': 'nom de snapshot invalide'}, 400) return self._json({"error": "nom de snapshot invalide"}, 400)
return self._json({'ok': True}) return self._json({"ok": True})
try: try:
C.restore_snapshot(body.get('name') or '') C.restore_snapshot(body.get("name") or "")
except (ValueError, FileNotFoundError): except (ValueError, FileNotFoundError):
return self._json({'error': 'snapshot introuvable'}, 404) return self._json({"error": "snapshot introuvable"}, 404)
with C.DB_LOCK: with C.DB_LOCK:
return self._send_db(200, C.db_bytes(), C.db_version()) return self._send_db(200, C.db_bytes(), C.db_version())
# LDAP (délégué à cmdb_server) # LDAP (délégué à cmdb_server)
if self.path in ('/ldap/search', '/ldap/computer'): if self.path in ("/ldap/search", "/ldap/computer"):
if not C.HAS_LDAP: if not C.HAS_LDAP:
return self._json({'error': "module ldap3 manquant — pip install ldap3"}, 500) return self._json(
{"error": "module ldap3 manquant — pip install ldap3"}, 500
)
try: try:
body = json.loads(self.rfile.read(int(self.headers.get('Content-Length', 0)))) body = json.loads(
fn = C.ldap_computer if self.path == '/ldap/computer' else C.ldap_search self.rfile.read(int(self.headers.get("Content-Length", 0)))
if self.path == '/ldap/search': )
return self._json(fn(body.get('config') or {}, (body.get('query') or '').strip(), fn = C.ldap_computer if self.path == "/ldap/computer" else C.ldap_search
int(body.get('limit') or 25))) if self.path == "/ldap/search":
return self._json(fn(body.get('config') or {}, (body.get('query') or '').strip())) return self._json(
fn(
body.get("config") or {},
(body.get("query") or "").strip(),
int(body.get("limit") or 25),
)
)
return self._json(
fn(body.get("config") or {}, (body.get("query") or "").strip())
)
except Exception as ex: except Exception as ex:
return self._json({'error': str(ex)}, 500) return self._json({"error": str(ex)}, 500)
# API certs (écriture) # API certs (écriture)
api = self._cert_api() api = self._cert_api()
try: try:
if api == 'write': return self.cert_write_text() if api == "write":
if api == 'write_bin': return self.cert_write_bin() return self.cert_write_text()
if api == 'delete': return self.cert_delete_dir() if api == "write_bin":
if api == 'delete_archives': return self.cert_delete_archives() return self.cert_write_bin()
if api == "delete":
return self.cert_delete_dir()
if api == "delete_archives":
return self.cert_delete_archives()
except Exception as e: except Exception as e:
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)
# ---------- certificats (disque réel) ---------- # ---------- certificats (disque réel) ----------
def cert_list(self): def cert_list(self):
@ -179,14 +198,19 @@ class Handler(SimpleHTTPRequestHandler):
certs, archives = [], [] certs, archives = [], []
for entry in sorted(os.listdir(WORK)): for entry in sorted(os.listdir(WORK)):
full = os.path.join(WORK, entry) full = os.path.join(WORK, entry)
if entry == 'archives': if entry == "archives":
if os.path.isdir(full): if os.path.isdir(full):
for a in sorted(os.listdir(full)): for a in sorted(os.listdir(full)):
af = os.path.join(full, a) af = os.path.join(full, a)
if os.path.isfile(af): if os.path.isfile(af):
st = os.stat(af) st = os.stat(af)
archives.append({'name': a, 'size': st.st_size, archives.append(
'mtime': int(st.st_mtime * 1000)}) {
"name": a,
"size": st.st_size,
"mtime": int(st.st_mtime * 1000),
}
)
continue continue
if not os.path.isdir(full): if not os.path.isdir(full):
continue continue
@ -198,65 +222,78 @@ class Handler(SimpleHTTPRequestHandler):
st = os.stat(ff) st = os.stat(ff)
m = int(st.st_mtime * 1000) m = int(st.st_mtime * 1000)
date = max(date, m) date = max(date, m)
files.append({'name': fn, 'ext': (fn.rsplit('.', 1)[-1] if '.' in fn else '').lower(), files.append(
'size': st.st_size, 'mtime': m}) {
"name": fn,
"ext": (fn.rsplit(".", 1)[-1] if "." in fn else "").lower(),
"size": st.st_size,
"mtime": m,
}
)
if files: if files:
certs.append({'dir': entry, 'files': files, 'date': date}) certs.append({"dir": entry, "files": files, "date": date})
archives.sort(key=lambda a: -a['mtime']) archives.sort(key=lambda a: -a["mtime"])
certs.sort(key=lambda c: -c['date']) certs.sort(key=lambda c: -c["date"])
return {'ok': True, 'certs': certs, 'archives': archives} return {"ok": True, "certs": certs, "archives": archives}
def cert_write_text(self): def cert_write_text(self):
d = self._body() d = self._body()
dname = safe_name(d['dir']) dname = safe_name(d["dir"])
target = os.path.join(WORK, dname) target = os.path.join(WORK, dname)
os.makedirs(target, exist_ok=True) os.makedirs(target, exist_ok=True)
for fname, content in (d.get('files') or {}).items(): for fname, content in (d.get("files") or {}).items():
with open(os.path.join(target, safe_name(fname)), 'w', encoding='utf-8', newline='') as f: with open(
os.path.join(target, safe_name(fname)),
"w",
encoding="utf-8",
newline="",
) as f:
f.write(content) f.write(content)
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):
d = self._body() d = self._body()
parts = [safe_name(x) for x in d['path'].split('/') if x != ''] parts = [safe_name(x) for x in d["path"].split("/") if x != ""]
if not parts: if not parts:
raise ValueError('path vide') raise ValueError("path vide")
target = os.path.join(WORK, *parts) target = os.path.join(WORK, *parts)
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"]))
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):
d = self._body() d = self._body()
dname = safe_name(d['dir']) dname = safe_name(d["dir"])
target = os.path.join(WORK, dname) target = os.path.join(WORK, dname)
if os.path.isdir(target): if os.path.isdir(target):
shutil.rmtree(target) shutil.rmtree(target)
if d.get('archives'): if d.get("archives"):
self._rm_archives(dname) self._rm_archives(dname)
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"]))
self._json({'ok': True}) self._json({"ok": True})
def _rm_archives(self, dname): def _rm_archives(self, dname):
low = dname.lower() low = dname.lower()
if os.path.isdir(ARCH): if os.path.isdir(ARCH):
for a in os.listdir(ARCH): for a in os.listdir(ARCH):
if low in a.lower(): if low in a.lower():
try: os.remove(os.path.join(ARCH, a)) try:
except OSError: pass os.remove(os.path.join(ARCH, a))
except OSError:
pass
if __name__ == '__main__': if __name__ == "__main__":
ensure_work() ensure_work()
os.chdir(HERE) os.chdir(HERE)
if not C.HAS_LDAP: if not C.HAS_LDAP:
print("⚠ ldap3 absent : recherche AD de la CMDB désactivée (pip install ldap3)") print("⚠ ldap3 absent : recherche AD de la CMDB désactivée (pip install ldap3)")
print('Portail Infra : http://127.0.0.1:%d → CMDB' % PORT) print("Portail Infra : http://127.0.0.1:%d → CMDB" % PORT)
print('CMDB base : %s' % C.DB_PATH) print("CMDB base : %s" % C.DB_PATH)
print('Certificats : %s (créé automatiquement)' % WORK) print("Certificats : %s (créé automatiquement)" % WORK)
print('Ctrl+C pour arrêter.') print("Ctrl+C pour arrêter.")
ThreadingHTTPServer(('0.0.0.0', PORT), Handler).serve_forever() ThreadingHTTPServer(("0.0.0.0", PORT), Handler).serve_forever()