267 lines
9.4 KiB
Python
267 lines
9.4 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_bundle(plan: Plan) -> dict[str, str]:
|
|
apply_config = bool(plan.project.apply_config)
|
|
return {
|
|
"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),
|
|
}
|
|
|
|
|
|
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),
|
|
}
|