Portail Infra : CMDB, générateur certificats & mapper VLAN switch

Ajouts :
- Snapshots serveur de la base CMDB (auto + manuel, restauration)
- Import/Export CSV de l'inventaire

Préparation partage public :
- Données réelles (cmdb.db, snapshots, certs/clés privées) exclues via .gitignore
- Suppression des infos en dur (proxy, VLAN, configs switch exemples)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Guillaume DENIS 2026-07-04 16:54:08 +02:00
commit bfc88f4afc
24 changed files with 13551 additions and 0 deletions

View file

@ -0,0 +1,174 @@
#!/usr/bin/env python3
"""
Serveur du Générateur de Certificats SSL.
Remplace `python -m http.server` : sert les pages ET écrit les certificats
sur le disque dans GENERATEUR_CERTIFICATES/LISTS_ALL_CERT/ (créé au besoin),
sans aucune sélection de dossier côté navigateur.
Usage :
python3 cert_server.py # http://127.0.0.1:8000/GENERATEUR_CERTIFICATES/dashboard.html
python3 cert_server.py 8080 # port custom
Sert la RACINE du projet (parent), donc les 3 apps + le switcher fonctionnent.
"""
import sys, os, json, base64, shutil, re
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
PORT = int(sys.argv[1]) if len(sys.argv) > 1 else 8000
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) # .../GENERATEUR_CERTIFICATES
ROOT = os.path.dirname(SCRIPT_DIR) # racine projet
WORK = os.path.join(SCRIPT_DIR, 'LISTS_ALL_CERT') # dossier de travail (disque)
ARCH = os.path.join(WORK, 'archives')
SAFE = re.compile(r'^[A-Za-z0-9._@\- ]+$') # noms de dossier/fichier autorisés
def safe_name(n):
n = (n or '').strip()
if not n or n in ('.', '..') or '/' in n or '\\' in n or not SAFE.match(n):
raise ValueError('nom invalide: %r' % n)
return n
def ensure_work():
os.makedirs(WORK, exist_ok=True)
class Handler(SimpleHTTPRequestHandler):
def _json(self, obj, code=200):
body = json.dumps(obj).encode('utf-8')
self.send_response(code)
self.send_header('Content-Type', 'application/json')
self.send_header('Content-Length', str(len(body)))
self.send_header('Cache-Control', 'no-store')
self.end_headers()
self.wfile.write(body)
def _err(self, msg, code=400):
self._json({'ok': False, 'error': str(msg)}, code)
def _body(self):
n = int(self.headers.get('Content-Length', 0))
return json.loads(self.rfile.read(n) or b'{}')
def _api(self):
p = self.path.split('?', 1)[0]
return p.split('/api/', 1)[1] if '/api/' in p else None
# ---------- GET : API list/ping, sinon fichiers statiques ----------
def do_GET(self):
api = self._api()
if api == 'ping':
return self._json({'ok': True, 'workdir': 'LISTS_ALL_CERT'})
if api == 'list':
return self._json(self.list_all())
return super().do_GET()
def do_POST(self):
api = self._api()
try:
if api == 'write': return self.write_text()
if api == 'write_bin': return self.write_bin()
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:
return self._err(e, 500)
self._err('route inconnue: %s' % self.path, 404)
# ---------- implémentations ----------
def list_all(self):
ensure_work()
certs, archives = [], []
for entry in sorted(os.listdir(WORK)):
full = os.path.join(WORK, entry)
if entry == 'archives':
if os.path.isdir(full):
for a in sorted(os.listdir(full)):
af = os.path.join(full, a)
if os.path.isfile(af):
st = os.stat(af)
archives.append({'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 write_text(self):
d = self._body()
dname = safe_name(d['dir'])
target = os.path.join(WORK, dname)
os.makedirs(target, exist_ok=True)
for fname, content in (d.get('files') or {}).items():
fn = safe_name(fname)
with open(os.path.join(target, fn), 'w', encoding='utf-8', newline='') as f:
f.write(content)
self._json({'ok': True, 'path': 'LISTS_ALL_CERT/%s/' % dname})
def write_bin(self):
d = self._body()
# path = "archives/<fichier>.zip" OU "<dir>/<fichier>"
parts = [safe_name(x) for x in d['path'].split('/') if x != '']
if not parts:
raise ValueError('path vide')
target = os.path.join(WORK, *parts)
os.makedirs(os.path.dirname(target), exist_ok=True)
with open(target, 'wb') as f:
f.write(base64.b64decode(d['b64']))
self._json({'ok': True, 'path': 'LISTS_ALL_CERT/%s' % '/'.join(parts)})
def delete_dir(self):
d = self._body()
dname = safe_name(d['dir'])
target = os.path.join(WORK, dname)
if os.path.isdir(target):
shutil.rmtree(target)
if d.get('archives'):
self._rm_archives(dname)
self._json({'ok': True})
def delete_file(self):
d = self._body()
target = os.path.join(WORK, safe_name(d['dir']), safe_name(d['name']))
if os.path.isfile(target):
os.remove(target)
self._json({'ok': True})
def delete_archives(self):
d = self._body()
self._rm_archives(safe_name(d['dir']))
self._json({'ok': True})
def _rm_archives(self, dname):
low = dname.lower()
if os.path.isdir(ARCH):
for a in os.listdir(ARCH):
if low in a.lower():
try: os.remove(os.path.join(ARCH, a))
except OSError: pass
def log_message(self, *a):
pass # silencieux
if __name__ == '__main__':
ensure_work()
os.chdir(ROOT)
url = 'http://127.0.0.1:%d/GENERATEUR_CERTIFICATES/dashboard.html' % PORT
print('Générateur Certificats : %s' % url)
print('Dossier disque : %s (créé automatiquement)' % WORK)
print('Ctrl+C pour arrêter.')
ThreadingHTTPServer(('0.0.0.0', PORT), Handler).serve_forever()

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long