164 lines
5.9 KiB
Python
164 lines
5.9 KiB
Python
"""Routes de l'application.
|
|
|
|
Le serveur est volontairement sans etat a ce stade : il valide, il genere, il
|
|
ne conserve rien. Le projet vit dans le navigateur. Le partage, les projets
|
|
enregistres et la connexion viendront s'ajouter par-dessus sans changer ce
|
|
noyau.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import io
|
|
import json
|
|
import os
|
|
import zipfile
|
|
|
|
from flask import Flask, Response, jsonify, render_template, request
|
|
|
|
from .catalog import CATEGORIES, Catalog
|
|
from .generator import build_plan, generate, render_bundle
|
|
from .model import CENTREON_VERSIONS, COMMAND_TYPES, Project, detect_macros, strip_secrets
|
|
from .packs import pack_to_project
|
|
|
|
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
CATALOG_DIR = os.environ.get("ENCLUME_CATALOG", os.path.join(BASE_DIR, "catalog"))
|
|
MAX_BODY = 2 * 1024 * 1024 # un projet honnete pese quelques dizaines de ko
|
|
|
|
|
|
def create_app() -> Flask:
|
|
app = Flask(__name__)
|
|
app.config["MAX_CONTENT_LENGTH"] = MAX_BODY
|
|
app.config["CATALOG"] = Catalog(CATALOG_DIR)
|
|
|
|
def catalog() -> Catalog:
|
|
return app.config["CATALOG"]
|
|
|
|
def project_from_request() -> Project:
|
|
payload = request.get_json(force=True, silent=True) or {}
|
|
return Project.from_dict(payload.get("project", payload))
|
|
|
|
@app.after_request
|
|
def security_headers(response: Response) -> Response:
|
|
response.headers.setdefault(
|
|
"Content-Security-Policy",
|
|
"default-src 'self'; img-src 'self' data:; style-src 'self'; script-src 'self'; "
|
|
"frame-ancestors 'none'; base-uri 'none'; form-action 'self'",
|
|
)
|
|
response.headers.setdefault("X-Content-Type-Options", "nosniff")
|
|
response.headers.setdefault("Referrer-Policy", "no-referrer")
|
|
return response
|
|
|
|
# ----------------------------------------------------------------- pages
|
|
|
|
@app.context_processor
|
|
def contexte_commun() -> dict:
|
|
return {"version": os.environ.get("ENCLUME_VERSION", "dev")}
|
|
|
|
@app.get("/")
|
|
def editor() -> str:
|
|
cat = catalog()
|
|
return render_template(
|
|
"editeur.html",
|
|
versions=CENTREON_VERSIONS,
|
|
command_types=COMMAND_TYPES,
|
|
categories=CATEGORIES,
|
|
grouped=cat.by_category(),
|
|
pack_count=len(cat.packs),
|
|
service_count=cat.service_count,
|
|
)
|
|
|
|
@app.get("/sante")
|
|
def health() -> Response:
|
|
cat = catalog()
|
|
return jsonify(
|
|
{
|
|
"statut": "ok",
|
|
"version": os.environ.get("ENCLUME_VERSION", "dev"),
|
|
"revision": os.environ.get("ENCLUME_COMMIT", "inconnu"),
|
|
"packs": len(cat.packs),
|
|
"erreurs_catalogue": cat.errors,
|
|
}
|
|
)
|
|
|
|
# ------------------------------------------------------------------- api
|
|
|
|
@app.get("/api/packs")
|
|
def api_packs() -> Response:
|
|
return jsonify(
|
|
[
|
|
{
|
|
"id": p.id,
|
|
"name": p.name,
|
|
"category": p.category,
|
|
"category_label": p.category_label,
|
|
"status": p.status,
|
|
"plugin": p.plugin,
|
|
"description": p.description,
|
|
"host_template": p.host_template.name,
|
|
"services": [
|
|
{"name": s.name, "description": s.description, "default": s.enabled_by_default}
|
|
for s in p.services
|
|
],
|
|
}
|
|
for p in sorted(catalog().packs.values(), key=lambda p: (p.category, p.name))
|
|
]
|
|
)
|
|
|
|
@app.get("/api/packs/<pack_id>/projet")
|
|
def api_pack_project(pack_id: str) -> Response:
|
|
pack = catalog().get(pack_id)
|
|
if pack is None:
|
|
return jsonify({"erreur": "pack inconnu"}), 404
|
|
only_defaults = request.args.get("tout") != "1"
|
|
return jsonify(pack_to_project(pack, only_defaults=only_defaults).to_dict())
|
|
|
|
@app.post("/api/valider")
|
|
def api_validate() -> Response:
|
|
plan = build_plan(project_from_request())
|
|
return jsonify({"summary": plan.summary, "issues": plan.issues, "has_errors": plan.has_errors})
|
|
|
|
@app.post("/api/generer")
|
|
def api_generate() -> Response:
|
|
payload = request.get_json(force=True, silent=True) or {}
|
|
return jsonify(generate(payload.get("project", payload)))
|
|
|
|
@app.post("/api/macros")
|
|
def api_macros() -> Response:
|
|
payload = request.get_json(force=True, silent=True) or {}
|
|
return jsonify(detect_macros(str(payload.get("line", ""))))
|
|
|
|
@app.post("/telecharger")
|
|
def download_zip() -> Response:
|
|
raw = request.form.get("project") or "{}"
|
|
project = Project.from_dict(json.loads(raw))
|
|
plan = build_plan(project)
|
|
files = render_bundle(plan)
|
|
buffer = io.BytesIO()
|
|
with zipfile.ZipFile(buffer, "w", zipfile.ZIP_DEFLATED) as archive:
|
|
for name, content in files.items():
|
|
archive.writestr(name, content)
|
|
# Le projet joint a l'archive ne contient aucun mot de passe.
|
|
archive.writestr(
|
|
"projet.json",
|
|
json.dumps(
|
|
strip_secrets(Project.from_dict(json.loads(raw))).to_dict(),
|
|
indent=2,
|
|
ensure_ascii=False,
|
|
),
|
|
)
|
|
buffer.seek(0)
|
|
slug = "".join(c if c.isalnum() or c in "-_" else "-" for c in project.name).strip("-") or "projet"
|
|
return Response(
|
|
buffer.read(),
|
|
mimetype="application/zip",
|
|
headers={"Content-Disposition": f'attachment; filename="{slug}-clapi.zip"'},
|
|
)
|
|
|
|
@app.post("/recharger")
|
|
def reload_catalog() -> Response:
|
|
app.config["CATALOG"] = Catalog(CATALOG_DIR)
|
|
cat = catalog()
|
|
return jsonify({"packs": len(cat.packs), "erreurs": cat.errors})
|
|
|
|
return app
|