Files
Enclume/enclume/catalog.py
T
hcornet de7a691861
Construction et publication / verifier (push) Successful in 41s
Construction et publication / verifier (pull_request) Successful in 11s
Construction et publication / publier (pull_request) Skipped
Construction et publication / publier (push) Failing after 6m3s
Enclume : editeur CLAPI, catalogue de 26 packs, empaquetage Docker
2026-09-10 11:32:03 +02:00

181 lines
5.7 KiB
Python

"""Chargement du catalogue de packs de supervision.
Un pack est un fichier YAML decrivant, pour une technologie donnee :
- un modele d'hote (host template) et ses macros,
- une liste de services, chacun portant sa ligne de commande et ses macros.
Le catalogue est volontairement en donnees et non en code : ajouter un pack
revient a deposer un fichier YAML dans catalog/<categorie>/.
"""
from __future__ import annotations
import os
from dataclasses import dataclass, field
from typing import Any
import yaml
CATEGORIES = {
"system": "Systemes d'exploitation",
"hardware": "Materiel",
"application": "Applications",
}
@dataclass
class Macro:
name: str
value: str = ""
description: str = ""
is_password: bool = False
@classmethod
def from_dict(cls, raw: dict[str, Any]) -> "Macro":
return cls(
name=str(raw["name"]).strip().upper(),
value="" if raw.get("value") is None else str(raw.get("value")),
description=str(raw.get("description", "")),
is_password=bool(raw.get("is_password", False)),
)
@dataclass
class Service:
name: str
alias: str
description: str
line: str
macros: list[Macro] = field(default_factory=list)
enabled_by_default: bool = True
check_interval: int | None = None
retry_interval: int | None = None
max_attempts: int | None = None
@classmethod
def from_dict(cls, raw: dict[str, Any]) -> "Service":
return cls(
name=str(raw["name"]).strip(),
alias=str(raw.get("alias") or raw["name"]).strip(),
description=str(raw.get("description", "")),
line=str(raw["line"]).strip(),
macros=[Macro.from_dict(m) for m in raw.get("macros") or []],
enabled_by_default=bool(raw.get("default", True)),
check_interval=raw.get("check_interval"),
retry_interval=raw.get("retry_interval"),
max_attempts=raw.get("max_attempts"),
)
@dataclass
class HostTemplate:
name: str
alias: str
parents: list[str] = field(default_factory=list)
check_command: str = ""
check_command_args: str = ""
macros: list[Macro] = field(default_factory=list)
@classmethod
def from_dict(cls, raw: dict[str, Any]) -> "HostTemplate":
return cls(
name=str(raw["name"]).strip(),
alias=str(raw.get("alias") or raw["name"]).strip(),
parents=[str(p).strip() for p in raw.get("parents") or []],
check_command=str(raw.get("check_command", "")),
check_command_args=str(raw.get("check_command_args", "")),
macros=[Macro.from_dict(m) for m in raw.get("macros") or []],
)
@dataclass
class Pack:
id: str
name: str
category: str
description: str
plugin: str
status: str
host_template: HostTemplate
services: list[Service]
source_file: str = ""
@property
def prefix(self) -> str:
"""Racine des noms de modeles de service, deduite du modele d'hote.
OS-Linux-SNMP-custom -> OS-Linux-SNMP
"""
base = self.host_template.name
return base[: -len("-custom")] if base.endswith("-custom") else base
@property
def category_label(self) -> str:
return CATEGORIES.get(self.category, self.category)
def service(self, name: str) -> Service | None:
for svc in self.services:
if svc.name == name:
return svc
return None
@classmethod
def from_dict(cls, raw: dict[str, Any], source_file: str = "") -> "Pack":
return cls(
id=str(raw["id"]).strip(),
name=str(raw["name"]).strip(),
category=str(raw.get("category", "system")).strip(),
description=str(raw.get("description", "")),
plugin=str(raw.get("plugin", "")),
status=str(raw.get("status", "to-verify")),
host_template=HostTemplate.from_dict(raw["host_template"]),
services=[Service.from_dict(s) for s in raw.get("services") or []],
source_file=source_file,
)
class Catalog:
def __init__(self, root: str):
self.root = root
self.packs: dict[str, Pack] = {}
self.errors: list[str] = []
self.load()
def load(self) -> None:
self.packs = {}
self.errors = []
for dirpath, _dirnames, filenames in os.walk(self.root):
for filename in sorted(filenames):
if not filename.endswith((".yml", ".yaml")):
continue
path = os.path.join(dirpath, filename)
try:
with open(path, encoding="utf-8") as handle:
raw = yaml.safe_load(handle)
pack = Pack.from_dict(raw, source_file=path)
except Exception as exc: # noqa: BLE001 - remonte tel quel a l'UI
self.errors.append(f"{path} : {exc}")
continue
if pack.id in self.packs:
self.errors.append(
f"{path} : identifiant deja utilise par "
f"{self.packs[pack.id].source_file}"
)
continue
self.packs[pack.id] = pack
def get(self, pack_id: str) -> Pack | None:
return self.packs.get(pack_id)
def by_category(self) -> dict[str, list[Pack]]:
grouped: dict[str, list[Pack]] = {key: [] for key in CATEGORIES}
for pack in self.packs.values():
grouped.setdefault(pack.category, []).append(pack)
for packs in grouped.values():
packs.sort(key=lambda p: p.name.lower())
return grouped
@property
def service_count(self) -> int:
return sum(len(p.services) for p in self.packs.values())