Files
Enclume/enclume/generator.py
T
hcornet b063d59525
build / Garde-fou (pull_request) Failing after 7s
build / Images Harbor (catalog-sync, Dockerfile.catalog-sync) (pull_request) Skipped
build / Images Harbor (web, Dockerfile) (pull_request) Skipped
build / Garde-fou (push) Failing after 8s
build / Images Harbor (catalog-sync, Dockerfile.catalog-sync) (push) Skipped
build / Images Harbor (web, Dockerfile) (push) Skipped
Partage par lien, comptes Authentik, soumission et moderation de packs, prerequis des sondes ; corrige le lien casse vers le script de l'editeur
2026-09-10 16:28:26 +02:00

315 lines
11 KiB
Python

"""Traduction d'un projet en operations CLAPI, puis en fichiers.
Un seul parcours du projet produit la liste d'operations ; les quatre fichiers
livres a l'utilisateur ne sont que des rendus differents de cette liste.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Any
from .model import Macro, Project, validate
CLAPI_BIN = "/usr/share/centreon/bin/centreon"
@dataclass
class Op:
obj: str
action: str
values: list[str] = field(default_factory=list)
comment: str = ""
def import_line(self) -> str:
return ";".join([self.obj, self.action, *self.values])
def shell_line(self) -> str:
# Les valeurs contiennent des macros Centreon ($HOSTADDRESS$, $_SERVICEWARNING$).
# Guillemets simples pour que bash ne les interprete pas ; apostrophes echappees.
payload = ";".join(self.values).replace("'", "'\\''")
return f"clapi -o {self.obj} -a {self.action} -v '{payload}'"
@dataclass
class Plan:
project: Project
ops: list[Op] = field(default_factory=list)
rollback_ops: list[Op] = field(default_factory=list)
issues: list[dict[str, str]] = field(default_factory=list)
@property
def summary(self) -> dict[str, int]:
return {
"commandes": len(self.project.commands),
"modeles de service": len(self.project.service_templates),
"modeles d'hote": len(self.project.host_templates),
"hotes": len(self.project.hosts),
"operations": len([o for o in self.ops if o.obj != "#"]),
}
@property
def has_errors(self) -> bool:
return any(i["level"] == "error" for i in self.issues)
def _macro_values(target: str, macro: Macro, fields: int) -> list[str]:
if fields == 3:
return [target, macro.name, macro.value]
return [target, macro.name, macro.value, "1" if macro.is_password else "0", macro.description]
def build_plan(project: Project) -> Plan:
plan = Plan(project=project, issues=validate(project))
prefix = project.prefix
fields = project.macro_fields
def full(name: str) -> str:
return f"{prefix}{name}" if name else name
own_commands = {c.name for c in project.commands}
own_htpl = {h.name for h in project.host_templates}
def ref_command(name: str) -> str:
"""Un objet du projet est prefixe ; un objet deja present sur le central ne l'est pas."""
return full(name) if name in own_commands else name
def ref_htpl(name: str) -> str:
return full(name) if name in own_htpl else name
del_cmd: list[Op] = []
del_stpl: list[Op] = []
del_htpl: list[Op] = []
del_host: list[Op] = []
if project.commands:
plan.ops.append(Op("#", "#", comment="Commandes de controle"))
for command in project.commands:
line = command.line.replace("$CENTREONPLUGINS$", project.plugins_dir)
plan.ops.append(Op("CMD", "ADD", [full(command.name), command.type, line]))
if command.comment:
plan.ops.append(Op("CMD", "setparam", [full(command.name), "comment", command.comment]))
del_cmd.append(Op("CMD", "DEL", [full(command.name)]))
if project.service_templates:
plan.ops.append(Op("#", "#", comment="Modeles de service"))
for stpl in project.service_templates:
plan.ops.append(
Op(
"STPL",
"ADD",
[
full(stpl.name),
stpl.alias or stpl.name,
stpl.parent,
ref_command(stpl.command),
stpl.command_args,
str(stpl.check_interval),
str(stpl.retry_interval),
str(stpl.max_attempts),
str(stpl.active_checks),
str(stpl.passive_checks),
],
)
)
for macro in stpl.macros:
plan.ops.append(Op("STPL", "setmacro", _macro_values(full(stpl.name), macro, fields)))
del_stpl.append(Op("STPL", "DEL", [full(stpl.name)]))
if project.host_templates:
plan.ops.append(Op("#", "#", comment="Modeles d'hote"))
for htpl in project.host_templates:
parents = "|".join(htpl.parents)
plan.ops.append(Op("HTPL", "ADD", [full(htpl.name), htpl.alias or htpl.name, "", parents, "", ""]))
for param, value in (
("check_command", htpl.check_command),
("check_command_arguments", htpl.check_command_args),
("snmp_version", htpl.snmp_version),
("snmp_community", htpl.snmp_community),
("timezone", htpl.timezone),
):
if value:
plan.ops.append(Op("HTPL", "setparam", [full(htpl.name), param, value]))
for macro in htpl.macros:
plan.ops.append(Op("HTPL", "setmacro", _macro_values(full(htpl.name), macro, fields)))
del_htpl.append(Op("HTPL", "DEL", [full(htpl.name)]))
links = [(s, h) for s in project.service_templates for h in s.host_templates]
if links:
plan.ops.append(Op("#", "#", comment="Rattachement des modeles de service aux modeles d'hote"))
for stpl, htpl_name in links:
plan.ops.append(Op("STPL", "addhosttemplate", [full(stpl.name), ref_htpl(htpl_name)]))
if project.hosts:
plan.ops.append(Op("#", "#", comment="Hotes"))
for host in project.hosts:
plan.ops.append(
Op(
"HOST",
"ADD",
[
host.name,
host.alias or host.name,
host.address,
"|".join(ref_htpl(t) for t in host.templates),
host.poller or project.poller,
host.hostgroups,
],
)
)
for macro in host.macros:
plan.ops.append(Op("HOST", "setmacro", _macro_values(host.name, macro, fields)))
plan.ops.append(Op("HOST", "applytpl", [host.name]))
del_host.append(Op("HOST", "DEL", [host.name]))
plan.rollback_ops = [*del_host, *del_stpl, *del_htpl, *del_cmd]
return plan
# --------------------------------------------------------------------------- #
# Rendus
# --------------------------------------------------------------------------- #
def _header(title: str, plan: Plan) -> list[str]:
stamp = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
summary = ", ".join(f"{v} {k}" for k, v in plan.summary.items() if v)
return [
f"# {title} - {plan.project.name}",
f"# Genere le {stamp}",
f"# Cible : {plan.project.centreon_version} | {summary}",
"#",
]
def render_import(plan: Plan, ops: list[Op], title: str) -> str:
lines = _header(title, plan)
for op in ops:
lines.append(f"# {op.comment}" if op.obj == "#" else op.import_line())
return "\n".join(lines) + "\n"
def render_shell(plan: Plan, ops: list[Op], title: str, apply_config: bool) -> str:
lines = [
"#!/usr/bin/env bash",
"#",
*_header(title, plan),
"# Usage :",
"# export CENTREON_USER=admin",
"# ./deploy.sh --dry-run affiche les commandes sans les executer",
"# ./deploy.sh applique la configuration",
"",
"set -euo pipefail",
"",
f'CLAPI_BIN="${{CLAPI_BIN:-{CLAPI_BIN}}}"',
'CENTREON_USER="${CENTREON_USER:-admin}"',
"DRY_RUN=0",
'[ "${1:-}" = "--dry-run" ] && DRY_RUN=1',
"",
'if [ "$DRY_RUN" -eq 0 ] && [ -z "${CENTREON_PASS:-}" ]; then',
' read -r -s -p "Mot de passe Centreon pour $CENTREON_USER : " CENTREON_PASS',
" echo",
"fi",
"",
"clapi() {",
' if [ "$DRY_RUN" -eq 1 ]; then',
' local out="$CLAPI_BIN -u $CENTREON_USER -p *** " arg',
' for arg in "$@"; do',
' case "$arg" in',
" *[!A-Za-z0-9_.:/=-]*) out=\"$out '$arg'\" ;;",
' *) out="$out $arg" ;;',
" esac",
" done",
' echo "$out"',
" else",
' "$CLAPI_BIN" -u "$CENTREON_USER" -p "$CENTREON_PASS" "$@"',
" fi",
"}",
"",
]
for op in ops:
if op.obj == "#":
lines.extend(["", f"# {op.comment}"])
else:
lines.append(op.shell_line())
if apply_config:
lines.extend(
[
"",
"# Generation et deploiement de la configuration du collecteur",
f"clapi -a APPLYCFG -v '{plan.project.poller}'",
]
)
lines.extend(["", 'echo "Termine."', ""])
return "\n".join(lines)
def render_prerequis(plan: Plan) -> str:
"""Fichier separe : ce qui s'installe sur le collecteur, jamais du CLAPI."""
prerequis = plan.project.prerequis
lignes = [
"#!/usr/bin/env bash",
"#",
f"# Prerequis des sondes - {plan.project.name}",
"# Genere par Enclume. A RELIRE AVANT EXECUTION.",
"#",
"# Ce fichier n'est pas du CLAPI : il s'execute sur le collecteur qui",
"# porte les plugins, pas sur le serveur central. Les commandes sont",
"# commentees par distribution ; decommentez celle qui vous concerne.",
"#",
"set -euo pipefail",
"",
]
if prerequis.paquets_debian:
lignes += [
"# Debian et Ubuntu",
"# apt-get update",
"# apt-get install -y " + " ".join(prerequis.paquets_debian),
"",
]
if prerequis.paquets_rhel:
lignes += [
"# RHEL, Alma, Rocky",
"# dnf install -y " + " ".join(prerequis.paquets_rhel),
"",
]
if prerequis.notes:
lignes += ["# Notes de l'auteur du pack :"]
lignes += [f"# {ligne}" for ligne in prerequis.notes.splitlines()]
lignes += [""]
if prerequis.verifications:
lignes += ["# Verifications, sans effet de bord :"]
for verification in prerequis.verifications:
lignes.append(verification)
lignes.append("")
lignes += ['echo "Prerequis passes en revue."', ""]
return "\n".join(lignes)
def render_bundle(plan: Plan) -> dict[str, str]:
apply_config = bool(plan.project.apply_config)
fichiers = {
"deploy.clapi": render_import(plan, plan.ops, "Deploiement CLAPI"),
"deploy.sh": render_shell(plan, plan.ops, "Deploiement CLAPI", apply_config),
"rollback.clapi": render_import(plan, plan.rollback_ops, "Suppression des objets"),
"rollback.sh": render_shell(plan, plan.rollback_ops, "Suppression des objets", apply_config),
}
if not plan.project.prerequis.vide:
fichiers["prerequis.sh"] = render_prerequis(plan)
return fichiers
def generate(raw_project: dict[str, Any]) -> dict[str, Any]:
"""Point d'entree unique utilise par l'API et la CLI."""
project = Project.from_dict(raw_project)
plan = build_plan(project)
return {
"summary": plan.summary,
"issues": plan.issues,
"has_errors": plan.has_errors,
"files": render_bundle(plan),
}