Masquer les packs deja importes dans la fenetre d'import
This commit is contained in:
1 parent
2187a1971e
commit
ec195cac8f
71 files changed
+796
-30
No files matched your search
@@ -115,6 +115,7 @@ class Pack:
|
||||
services: list[Service]
|
||||
prerequis: dict[str, Any] = field(default_factory=dict)
|
||||
notification: dict[str, Any] = field(default_factory=dict)
|
||||
groupe_hote: dict[str, Any] = field(default_factory=dict)
|
||||
source_file: str = ""
|
||||
|
||||
@property
|
||||
@@ -149,6 +150,7 @@ class Pack:
|
||||
services=[Service.from_dict(s) for s in raw.get("services") or []],
|
||||
prerequis=raw.get("prerequis") or {},
|
||||
notification=raw.get("notification") or {},
|
||||
groupe_hote=raw.get("groupe_hote") or {},
|
||||
source_file=source_file,
|
||||
)
|
||||
|
||||
|
||||
@@ -252,6 +252,13 @@ def build_plan(project: Project) -> Plan:
|
||||
],
|
||||
)
|
||||
)
|
||||
for param, valeur in (
|
||||
("check_command", ref_command(host.check_command) if host.check_command else ""),
|
||||
("check_command_arguments", host.check_command_args),
|
||||
("timezone", host.timezone),
|
||||
):
|
||||
if valeur:
|
||||
plan.ops.append(Op("HOST", "setparam", [host.name, param, valeur]))
|
||||
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]))
|
||||
|
||||
@@ -193,6 +193,9 @@ class Host:
|
||||
templates: list[str] = field(default_factory=list)
|
||||
poller: str = "Central"
|
||||
hostgroups: list[str] = field(default_factory=list)
|
||||
check_command: str = ""
|
||||
check_command_args: str = ""
|
||||
timezone: str = ""
|
||||
macros: list[Macro] = field(default_factory=list)
|
||||
|
||||
@classmethod
|
||||
@@ -205,6 +208,9 @@ class Host:
|
||||
poller=_clean(raw.get("poller")) or "Central",
|
||||
# Ancien format : une chaine unique separee par | ou par virgule.
|
||||
hostgroups=_liste(raw.get("hostgroups")),
|
||||
check_command=_clean(raw.get("check_command")),
|
||||
check_command_args=_clean(raw.get("check_command_args")),
|
||||
timezone=_clean(raw.get("timezone")),
|
||||
macros=[Macro.from_dict(m) for m in raw.get("macros") or []],
|
||||
)
|
||||
|
||||
@@ -468,6 +474,16 @@ class Project:
|
||||
# Validation
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
def fuseaux_horaires() -> list[str]:
|
||||
"""Fuseaux connus du systeme, au format attendu par Centreon."""
|
||||
try:
|
||||
import zoneinfo
|
||||
|
||||
return sorted(zoneinfo.available_timezones())
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
def validate(project: Project) -> list[dict[str, str]]:
|
||||
"""Retourne la liste des problemes, du bloquant au simple avertissement."""
|
||||
|
||||
@@ -532,6 +548,11 @@ def validate(project: Project) -> list[dict[str, str]]:
|
||||
for htpl in project.host_templates:
|
||||
_check_macros(htpl.macros, f"Modele d'hote {htpl.name}", issues)
|
||||
|
||||
connus = set(fuseaux_horaires())
|
||||
for objet in (*project.host_templates, *project.hosts):
|
||||
if objet.timezone and connus and objet.timezone.lstrip(":") not in connus:
|
||||
error(f"{objet.name} : fuseau horaire inconnu, {objet.timezone}")
|
||||
|
||||
groupes = {g.name for g in project.groupes_hotes}
|
||||
for host in project.hosts:
|
||||
for groupe in host.hostgroups:
|
||||
|
||||
@@ -10,6 +10,7 @@ from .catalog import Pack
|
||||
from .model import (
|
||||
Calendrier,
|
||||
Command,
|
||||
GroupeHote,
|
||||
HostTemplate,
|
||||
Macro,
|
||||
Perimetre,
|
||||
@@ -76,6 +77,17 @@ def pack_to_project(
|
||||
htpl.snmp_community = "public"
|
||||
project.host_templates = [htpl]
|
||||
|
||||
# --- Groupe d'hotes livre avec le pack ----------------------------------
|
||||
# Un hote sans groupe devient introuvable des que le parc grandit, et les
|
||||
# droits comme les filtres s'appuient dessus. Le pack en propose donc un.
|
||||
if pack.groupe_hote:
|
||||
project.groupes_hotes = [
|
||||
GroupeHote(
|
||||
name=str(pack.groupe_hote.get("name") or ""),
|
||||
alias=str(pack.groupe_hote.get("alias") or pack.name),
|
||||
)
|
||||
]
|
||||
|
||||
# --- Chaine de notification --------------------------------------------
|
||||
notification = pack.notification or {}
|
||||
perimetre_nom = ""
|
||||
|
||||
+176
-20
@@ -173,7 +173,9 @@ function rendreRail() {
|
||||
className: 'rail-reglages' + (selection.type === 'reglages' ? ' actif' : ''),
|
||||
type: 'button',
|
||||
textContent: 'Reglages du projet',
|
||||
onclick: function () { selection = { type: 'reglages', index: 0 }; rendre(); }
|
||||
onclick: function () { selection = { type: 'reglages', index: 0 }; chargerStandards();
|
||||
|
||||
rendre(); }
|
||||
}));
|
||||
|
||||
Object.keys(LISTES).forEach(function (type) {
|
||||
@@ -278,8 +280,8 @@ function rendreReglages(main) {
|
||||
}));
|
||||
var grille = elt('div', { className: 'grille' });
|
||||
grille.appendChild(champ('Prefixe applique aux objets crees', projet, 'prefix', { placeholder: 'ex. ACME-' }));
|
||||
grille.appendChild(champ('Repertoire des plugins', projet, 'plugins_dir'));
|
||||
grille.appendChild(champ('Collecteur cible', projet, 'poller'));
|
||||
grille.appendChild(champPropose('Repertoire des plugins', projet, 'plugins_dir', 'repertoires_plugins'));
|
||||
grille.appendChild(champPropose('Collecteur cible', projet, 'poller', 'collecteurs'));
|
||||
grille.appendChild(champ('Repertoire des scripts de notification', projet, 'notif_dir'));
|
||||
main.appendChild(grille);
|
||||
main.appendChild(caseACocher("Ajouter APPLYCFG a la fin des scripts", projet, 'apply_config'));
|
||||
@@ -354,7 +356,7 @@ function rendreStpl(main, stpl) {
|
||||
var grille = elt('div', { className: 'grille' });
|
||||
grille.appendChild(champ('Nom du modele', stpl, 'name', { rail: true }));
|
||||
grille.appendChild(champ('Alias', stpl, 'alias'));
|
||||
grille.appendChild(champ('Modele parent', stpl, 'parent'));
|
||||
grille.appendChild(champPropose('Modele parent', stpl, 'parent', 'modeles_service'));
|
||||
grille.appendChild(champSelectListe('Commande de controle', stpl, 'command',
|
||||
projet.commands.map(function (c) { return c.name; })));
|
||||
main.appendChild(grille);
|
||||
@@ -392,14 +394,15 @@ function rendreHtpl(main, htpl) {
|
||||
var grille = elt('div', { className: 'grille' });
|
||||
grille.appendChild(champ('Nom du modele', htpl, 'name', { rail: true, placeholder: 'OS-Linux-SNMP-custom' }));
|
||||
grille.appendChild(champ('Alias', htpl, 'alias'));
|
||||
grille.appendChild(champListe('Modeles parents', htpl, 'parents'));
|
||||
grille.appendChild(champ('Fuseau horaire', htpl, 'timezone', { placeholder: ':Europe/Paris' }));
|
||||
grille.appendChild(champListeSuggeree('Modeles parents', htpl, 'parents',
|
||||
(STANDARDS && STANDARDS.modeles_hote) || ['generic-active-host-custom'], 'liste-modeles-hote'));
|
||||
grille.appendChild(champFuseau('Fuseau horaire', htpl, 'timezone'));
|
||||
main.appendChild(grille);
|
||||
|
||||
var grille2 = elt('div', { className: 'grille' });
|
||||
grille2.appendChild(champ('Commande de verification', htpl, 'check_command', { placeholder: 'check_centreon_ping' }));
|
||||
grille2.appendChild(champCommandeVerification('Commande de verification', htpl, 'check_command'));
|
||||
grille2.appendChild(champ('Arguments', htpl, 'check_command_args', { placeholder: '!3!200,20%!400,50%' }));
|
||||
grille2.appendChild(champ('Version SNMP', htpl, 'snmp_version', { placeholder: '2c' }));
|
||||
grille2.appendChild(champPropose('Version SNMP', htpl, 'snmp_version', 'versions_snmp', { placeholder: '2c' }));
|
||||
grille2.appendChild(champ('Communaute SNMP', htpl, 'snmp_community'));
|
||||
main.appendChild(grille2);
|
||||
|
||||
@@ -426,7 +429,14 @@ function rendreHost(main, host) {
|
||||
grille.appendChild(champ("Nom de l'hote", host, 'name', { rail: true }));
|
||||
grille.appendChild(champ('Alias', host, 'alias'));
|
||||
grille.appendChild(champ('Adresse IP ou DNS', host, 'address'));
|
||||
grille.appendChild(champ('Collecteur', host, 'poller'));
|
||||
grille.appendChild(champPropose('Collecteur', host, 'poller', 'collecteurs'));
|
||||
grille.appendChild(champCommandeVerification('Commande de verification', host, 'check_command'));
|
||||
grille.appendChild(champ('Arguments de la commande', host, 'check_command_args', { placeholder: '!3!200,20%!400,50%' }));
|
||||
grille.appendChild(champFuseau('Fuseau horaire', host, 'timezone'));
|
||||
if (!host.hostgroups.length && projet.groupes_hotes.length) {
|
||||
// L'hote cree apres l'import d'un pack rejoint naturellement son groupe.
|
||||
host.hostgroups = [projet.groupes_hotes[projet.groupes_hotes.length - 1].name];
|
||||
}
|
||||
grille.appendChild(champListeSuggeree("Groupes d'hotes", host, 'hostgroups',
|
||||
projet.groupes_hotes.map(function (g) { return g.name; }), 'liste-groupes-hotes'));
|
||||
main.appendChild(grille);
|
||||
@@ -499,7 +509,7 @@ function rendrePerimetre(main, perimetre) {
|
||||
var contexte = elt('div', { className: 'grille' });
|
||||
contexte.appendChild(champ('Application', perimetre, 'application', { placeholder: 'Facturation' }));
|
||||
contexte.appendChild(champ('Responsable', perimetre, 'responsable', { placeholder: 'Equipe finance' }));
|
||||
contexte.appendChild(champ('Criticite', perimetre, 'criticite', { placeholder: 'Majeure' }));
|
||||
contexte.appendChild(champPropose('Criticite', perimetre, 'criticite', 'criticites'));
|
||||
contexte.appendChild(champ('Impact metier', perimetre, 'impact', { placeholder: 'Facturation client bloquee' }));
|
||||
contexte.appendChild(champ('Procedure de reprise (URL)', perimetre, 'procedure', { placeholder: 'https://wiki/...' }));
|
||||
contexte.appendChild(champ('Couleur du bandeau', perimetre, 'couleur', { placeholder: '#2f6f9f' }));
|
||||
@@ -535,6 +545,108 @@ function rendrePerimetre(main, perimetre) {
|
||||
main.appendChild(apercu);
|
||||
}
|
||||
|
||||
var STANDARDS = null;
|
||||
|
||||
function chargerStandards() {
|
||||
if (STANDARDS) return Promise.resolve(STANDARDS);
|
||||
return fetch('/api/valeurs-standards').then(function (r) { return r.json(); })
|
||||
.then(function (v) { STANDARDS = v || {}; return STANDARDS; })
|
||||
.catch(function () { STANDARDS = {}; return STANDARDS; });
|
||||
}
|
||||
|
||||
// Champ a valeurs proposees : la liste vient du serveur, la saisie reste
|
||||
// possible pour ce qui existe deja sur un central particulier.
|
||||
function champPropose(libelle, objet, cle, nomListe, options) {
|
||||
options = options || {};
|
||||
var label = elt('label', { className: 'field' });
|
||||
label.appendChild(elt('span', { textContent: libelle }));
|
||||
var identifiant = 'liste-' + nomListe;
|
||||
var input = elt('input', { type: 'text', value: objet[cle] || '' });
|
||||
if (options.placeholder) input.placeholder = options.placeholder;
|
||||
input.setAttribute('list', identifiant);
|
||||
input.addEventListener('input', function () { objet[cle] = input.value; sauver(); });
|
||||
label.appendChild(input);
|
||||
|
||||
var datalist = elt('datalist', { id: identifiant });
|
||||
label.appendChild(datalist);
|
||||
chargerStandards().then(function (valeurs) {
|
||||
(valeurs[nomListe] || []).forEach(function (v) {
|
||||
datalist.appendChild(elt('option', { value: v }));
|
||||
});
|
||||
(options.enPlus || []).forEach(function (v) {
|
||||
datalist.appendChild(elt('option', { value: v }));
|
||||
});
|
||||
});
|
||||
return label;
|
||||
}
|
||||
|
||||
var FUSEAUX = null;
|
||||
var COMMANDES_USUELLES = null;
|
||||
|
||||
function chargerFuseaux() {
|
||||
if (FUSEAUX) return Promise.resolve(FUSEAUX);
|
||||
return fetch('/api/fuseaux').then(function (r) { return r.json(); })
|
||||
.then(function (liste) { FUSEAUX = Array.isArray(liste) ? liste : []; return FUSEAUX; })
|
||||
.catch(function () { FUSEAUX = []; return FUSEAUX; });
|
||||
}
|
||||
|
||||
function chargerCommandesUsuelles() {
|
||||
if (COMMANDES_USUELLES) return Promise.resolve(COMMANDES_USUELLES);
|
||||
return fetch('/api/commandes-usuelles').then(function (r) { return r.json(); })
|
||||
.then(function (liste) { COMMANDES_USUELLES = Array.isArray(liste) ? liste : []; return COMMANDES_USUELLES; })
|
||||
.catch(function () { COMMANDES_USUELLES = []; return COMMANDES_USUELLES; });
|
||||
}
|
||||
|
||||
// Champ ferme : la valeur est choisie dans une liste, jamais saisie. Un fuseau
|
||||
// mal orthographie ne se voit qu'a l'execution sur le central.
|
||||
function champFuseau(libelle, objet, cle) {
|
||||
var label = elt('label', { className: 'field' });
|
||||
label.appendChild(elt('span', { textContent: libelle }));
|
||||
var select = elt('select');
|
||||
select.appendChild(elt('option', { value: '', textContent: 'Celui du serveur central' }));
|
||||
label.appendChild(select);
|
||||
|
||||
chargerFuseaux().then(function (liste) {
|
||||
liste.forEach(function (fuseau) {
|
||||
var option = elt('option', { value: fuseau, textContent: fuseau });
|
||||
if (objet[cle] === fuseau) option.selected = true;
|
||||
select.appendChild(option);
|
||||
});
|
||||
// Une valeur heritee d'un ancien projet doit rester visible.
|
||||
if (objet[cle] && liste.indexOf(objet[cle]) === -1) {
|
||||
var inconnu = elt('option', { value: objet[cle], textContent: objet[cle] + ' (inconnu)' });
|
||||
inconnu.selected = true;
|
||||
select.appendChild(inconnu);
|
||||
}
|
||||
});
|
||||
|
||||
select.addEventListener('change', function () { objet[cle] = select.value; sauver(); });
|
||||
return label;
|
||||
}
|
||||
|
||||
// Commande de verification : celles du projet et celles livrees par Centreon.
|
||||
function champCommandeVerification(libelle, objet, cle) {
|
||||
var label = elt('label', { className: 'field' });
|
||||
label.appendChild(elt('span', { textContent: libelle }));
|
||||
var input = elt('input', { type: 'text', value: objet[cle], placeholder: 'check_centreon_ping' });
|
||||
input.setAttribute('list', 'liste-commandes-verification');
|
||||
input.addEventListener('input', function () { objet[cle] = input.value; sauver(); });
|
||||
label.appendChild(input);
|
||||
|
||||
var datalist = elt('datalist', { id: 'liste-commandes-verification' });
|
||||
label.appendChild(datalist);
|
||||
chargerCommandesUsuelles().then(function (liste) {
|
||||
datalist.innerHTML = '';
|
||||
liste.forEach(function (c) {
|
||||
datalist.appendChild(elt('option', { value: c.nom, label: c.description }));
|
||||
});
|
||||
projet.commands.forEach(function (c) {
|
||||
datalist.appendChild(elt('option', { value: c.name, label: 'commande du projet' }));
|
||||
});
|
||||
});
|
||||
return label;
|
||||
}
|
||||
|
||||
var MACROS_CENTREON = null;
|
||||
|
||||
function chargerMacros() {
|
||||
@@ -1038,6 +1150,11 @@ function titreCatalogue(packs) {
|
||||
return packs ? 'Importer un pack (' + packs.length + ')' : 'Importer un pack';
|
||||
}
|
||||
|
||||
// Un pack est deja dans le projet si son modele d'hote y figure.
|
||||
function packImporte(pack) {
|
||||
return projet.host_templates.some(function (h) { return h.name === pack.host_template; });
|
||||
}
|
||||
|
||||
function majTitreModale(texte) {
|
||||
var titre = document.getElementById('modal-titre');
|
||||
if (titre) titre.textContent = texte;
|
||||
@@ -1047,11 +1164,33 @@ function ouvrirCataloguePacks() {
|
||||
ouvrirModale(titreCatalogue(packsCache), function (corps) {
|
||||
var recherche = elt('input', { type: 'text', placeholder: 'Filtrer par nom, technologie, plugin...' });
|
||||
corps.appendChild(recherche);
|
||||
var note = elt('div', { className: 'note-masques' });
|
||||
corps.appendChild(note);
|
||||
var resultats = elt('div', { className: 'packs' });
|
||||
corps.appendChild(resultats);
|
||||
|
||||
// Les packs deja dans le projet sont masques : les reproposer n'aurait
|
||||
// aucun effet, l'import les ignorerait.
|
||||
var montrerImportes = false;
|
||||
|
||||
function afficher(packs) {
|
||||
resultats.innerHTML = '';
|
||||
note.innerHTML = '';
|
||||
|
||||
if (Array.isArray(packs) && !montrerImportes) {
|
||||
var importes = packs.filter(packImporte);
|
||||
packs = packs.filter(function (p) { return !packImporte(p); });
|
||||
if (importes.length) {
|
||||
note.appendChild(elt('span', {
|
||||
textContent: importes.length + ' pack(s) deja dans le projet, masque(s). '
|
||||
}));
|
||||
note.appendChild(elt('button', {
|
||||
type: 'button', className: 'small', textContent: 'Les afficher',
|
||||
onclick: function () { montrerImportes = true; filtrer(); }
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(packs) && packsCache) {
|
||||
majTitreModale(
|
||||
packs.length === packsCache.length
|
||||
@@ -1067,9 +1206,13 @@ function ouvrirCataloguePacks() {
|
||||
return;
|
||||
}
|
||||
packs.forEach(function (pack) {
|
||||
var ligne = elt('div', { className: 'pack-ligne' });
|
||||
var deja = packImporte(pack);
|
||||
var ligne = elt('div', { className: 'pack-ligne' + (deja ? ' importe' : '') });
|
||||
var texte = elt('div');
|
||||
texte.appendChild(elt('b', { textContent: pack.name }));
|
||||
if (deja) {
|
||||
texte.appendChild(elt('span', { className: 'tag ok', textContent: 'deja importe' }));
|
||||
}
|
||||
if (pack.status !== 'verified') {
|
||||
texte.appendChild(elt('span', { className: 'tag warn', textContent: 'a valider' }));
|
||||
}
|
||||
@@ -1077,18 +1220,27 @@ function ouvrirCataloguePacks() {
|
||||
texte.appendChild(elt('div', { className: 'plugin', textContent: pack.plugin + ' | ' + pack.services.length + ' sondes' }));
|
||||
ligne.appendChild(texte);
|
||||
var actions = elt('div', { className: 'barre' });
|
||||
actions.appendChild(elt('button', {
|
||||
type: 'button', className: 'small', textContent: 'Sondes par defaut',
|
||||
onclick: function () { importerPack(pack.id, false); }
|
||||
}));
|
||||
actions.appendChild(elt('button', {
|
||||
type: 'button', className: 'small', textContent: 'Toutes les sondes',
|
||||
onclick: function () { importerPack(pack.id, true); }
|
||||
}));
|
||||
if (!deja) {
|
||||
actions.appendChild(elt('button', {
|
||||
type: 'button', className: 'small', textContent: 'Sondes par defaut',
|
||||
onclick: function () { importerPack(pack.id, false); }
|
||||
}));
|
||||
actions.appendChild(elt('button', {
|
||||
type: 'button', className: 'small', textContent: 'Toutes les sondes',
|
||||
onclick: function () { importerPack(pack.id, true); }
|
||||
}));
|
||||
}
|
||||
ligne.appendChild(actions);
|
||||
resultats.appendChild(ligne);
|
||||
});
|
||||
if (!packs.length) resultats.appendChild(elt('p', { className: 'lede', textContent: 'Aucun pack ne correspond.' }));
|
||||
if (!packs.length) {
|
||||
resultats.appendChild(elt('p', {
|
||||
className: 'lede',
|
||||
textContent: montrerImportes
|
||||
? 'Aucun pack ne correspond.'
|
||||
: 'Aucun pack a importer ne correspond.'
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
function filtrer() {
|
||||
@@ -1193,6 +1345,10 @@ document.addEventListener('keydown', function (e) {
|
||||
});
|
||||
|
||||
window.notifier = notifier;
|
||||
// Lecture du projet en cours, pour le test d'interface et le diagnostic depuis
|
||||
// la console. Une fonction plutot qu'une variable : le projet est remplace a
|
||||
// chaque ouverture ou import.
|
||||
window.projetCourant = function () { return projet; };
|
||||
|
||||
rendre();
|
||||
|
||||
|
||||
@@ -593,3 +593,14 @@ pre.code {
|
||||
@media (max-width: 900px) {
|
||||
.bloc-ligne { grid-template-columns: 1fr 1fr; }
|
||||
}
|
||||
|
||||
/* Packs deja dans le projet : visibles a la demande, sans action possible. */
|
||||
.note-masques {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: var(--ink-dim);
|
||||
font-size: 12.5px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.pack-ligne.importe { opacity: 0.55; }
|
||||
@@ -1,15 +1,16 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Enclume — Générateur de configuration CLAPI pour Centreon{% endblock %}
|
||||
{% block title %}Forge Centreon — Générateur de configuration pour Centreon avec CLAPI{% endblock %}
|
||||
{% block body %}
|
||||
<div class="accueil">
|
||||
|
||||
<section class="manchette">
|
||||
<h2>Créez votre configuration Centreon, repartez avec le CLAPI</h2>
|
||||
<p>Commandes de contrôle, modèles de service, modèles d'hôte : construisez-les dans votre
|
||||
navigateur, à partir de zéro ou d'un des {{ pack_count }} packs du catalogue. Enclume produit
|
||||
navigateur, à partir de zéro ou d'un des {{ pack_count }} packs du catalogue. Forge Centreon produit
|
||||
les fichiers à jouer sur votre central — que vous relisez avant de les exécuter.</p>
|
||||
<div class="barre">
|
||||
<a class="btn primaire" href="/editeur">Ouvrir l'éditeur</a>
|
||||
<a class="btn" href="/aide/snmp">Installer SNMP</a>
|
||||
<a class="btn" href="#fonctionnement">Comment ça marche</a>
|
||||
</div>
|
||||
<p class="petit">Sans inscription. Rien n'est installé. Projet indépendant de Centreon SAS.</p>
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{% block title %}Enclume — Générateur de configuration CLAPI pour Centreon{% endblock %}</title>
|
||||
<title>{% block title %}Forge Centreon — Générateur de configuration pour Centreon avec CLAPI{% endblock %}</title>
|
||||
<meta name="description" content="Créez vos commandes de contrôle, modèles de service et modèles d'hôte Centreon, et repartez avec le code CLAPI prêt à jouer sur votre central. Gratuit, sans inscription.">
|
||||
<link rel="canonical" href="{{ request.url_root }}">
|
||||
<meta property="og:title" content="Enclume — Générateur de configuration CLAPI pour Centreon">
|
||||
<meta property="og:title" content="Forge Centreon — Générateur de configuration pour Centreon avec CLAPI">
|
||||
<meta property="og:description" content="Créez vos commandes, modèles de service et modèles d'hôte, repartez avec le code CLAPI.">
|
||||
<meta property="og:type" content="website">
|
||||
<link rel="icon" href="{{ url_for('static', filename='enclume.svg') }}" type="image/svg+xml">
|
||||
@@ -14,14 +14,15 @@
|
||||
</head>
|
||||
<body>
|
||||
<header class="topbar">
|
||||
<h1><a href="/">Enclume</a></h1>
|
||||
<span class="meta">Générateur de configuration CLAPI pour Centreon</span>
|
||||
<h1><a href="/">Forge Centreon</a></h1>
|
||||
<span class="meta">Générateur de configuration pour Centreon avec CLAPI</span>
|
||||
<span class="version">{{ version }}</span>
|
||||
</header>
|
||||
{% block body %}{% endblock %}
|
||||
<footer class="pied">
|
||||
<span>Projet indépendant, sans lien avec Centreon SAS. Centreon est une marque déposée de son propriétaire.</span>
|
||||
<span class="grow"></span>
|
||||
<a href="/aide/snmp">Installer SNMP</a>
|
||||
<a href="/mentions">Mentions légales</a>
|
||||
<a href="{{ depot }}">Code source</a>
|
||||
<a href="{{ depot }}/src/branch/main/CONTRIBUTING.md">Contribuer</a>
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Installer et sécuriser SNMP — Forge Centreon{% endblock %}
|
||||
{% block body %}
|
||||
<div class="accueil texte">
|
||||
|
||||
<h2>Installer et sécuriser SNMP</h2>
|
||||
<p>La plupart des packs du catalogue interrogent leurs équipements en SNMP. Cette page
|
||||
donne, par système, une configuration de départ qui fonctionne et qui ne laisse pas votre
|
||||
parc ouvert au premier venu.</p>
|
||||
|
||||
<div class="notice">
|
||||
<b>Deux règles avant de commencer</b>
|
||||
<p>SNMP v1 et v2c transportent la communauté <b>en clair</b> sur le réseau : quiconque
|
||||
écoute peut la lire et interroger l'équipement. Préférez la <b>version 3</b> partout où
|
||||
elle est disponible.</p>
|
||||
<p style="margin-top:8px">Si vous restez en v2c — souvent le cas sur du matériel ancien —
|
||||
restreignez au moins l'accès à l'adresse du collecteur et n'autorisez que la lecture.</p>
|
||||
</div>
|
||||
|
||||
<h3>Debian et Ubuntu</h3>
|
||||
<pre class="code">apt install snmpd</pre>
|
||||
|
||||
<p>Remplacez le contenu de <code>/etc/snmp/snmpd.conf</code> par ceci, en v3 :</p>
|
||||
<pre class="code"># N'ecoute que sur l'interface d'administration, jamais sur toutes
|
||||
agentaddress udp:10.0.4.20:161
|
||||
|
||||
# Ce que le compte a le droit de lire : tout l'arbre standard, rien d'autre
|
||||
view lecture included .1.3.6.1.2.1
|
||||
view lecture included .1.3.6.1.4.1
|
||||
|
||||
# Compte de supervision, lecture seule, sur la vue ci-dessus
|
||||
rouser centreon authPriv -V lecture
|
||||
|
||||
# Informations d'inventaire, facultatives mais utiles
|
||||
sysLocation Salle serveur, baie A3
|
||||
sysContact [email protected]</pre>
|
||||
|
||||
<p>Le compte lui-même se crée service arrêté, la directive étant déplacée au démarrage
|
||||
vers un fichier de persistance :</p>
|
||||
<pre class="code">systemctl stop snmpd
|
||||
echo 'createUser centreon SHA-256 "MotDePasseAuth" AES "MotDePasseChiffrement"' >> /etc/snmp/snmpd.conf
|
||||
systemctl start snmpd</pre>
|
||||
|
||||
<div class="notice">
|
||||
<b>Le piège classique</b>
|
||||
<p>Après démarrage, la ligne <code>createUser</code> disparaît de
|
||||
<code>/etc/snmp/snmpd.conf</code> : net-snmp la déplace dans
|
||||
<code>/var/lib/snmp/snmpd.conf</code> sous forme chiffrée. C'est normal. Ne la remettez
|
||||
pas, vous créeriez un doublon.</p>
|
||||
</div>
|
||||
|
||||
<p>En v2c, si la version 3 n'est pas envisageable :</p>
|
||||
<pre class="code">agentaddress udp:10.0.4.20:161
|
||||
|
||||
# Lecture seule, et uniquement depuis le collecteur
|
||||
rocommunity UneChaineLongueEtAleatoire 10.0.4.10 -V lecture</pre>
|
||||
|
||||
<h3>RHEL, Alma et Rocky</h3>
|
||||
<pre class="code">dnf install net-snmp net-snmp-utils</pre>
|
||||
|
||||
<p>Le fichier de configuration est au même endroit et la syntaxe identique. La différence
|
||||
est le pare-feu :</p>
|
||||
<pre class="code">firewall-cmd --permanent --add-rich-rule='rule family="ipv4" \
|
||||
source address="10.0.4.10/32" port port="161" protocol="udp" accept'
|
||||
firewall-cmd --reload
|
||||
systemctl enable --now snmpd</pre>
|
||||
|
||||
<p>Sur Debian et Ubuntu, l'équivalent avec ufw :</p>
|
||||
<pre class="code">ufw allow from 10.0.4.10 to any port 161 proto udp</pre>
|
||||
|
||||
<h3>Windows</h3>
|
||||
<p>Le service SNMP de Windows ne connaît que la v2c, et Microsoft le considère comme
|
||||
obsolète. Il reste utilisable, mais pour un parc Windows important, les packs
|
||||
<b>NSClient++</b> du catalogue sont un meilleur choix.</p>
|
||||
|
||||
<pre class="code"># Windows 10, 11, Server 2019 et suivants
|
||||
Add-WindowsCapability -Online -Name "SNMP.Client~~~~0.0.1.0"
|
||||
|
||||
# Serveurs plus anciens
|
||||
Install-WindowsFeature SNMP-Service -IncludeManagementTools</pre>
|
||||
|
||||
<p>La configuration se fait dans la base de registre. Communauté en lecture seule, et
|
||||
restriction au seul collecteur :</p>
|
||||
<pre class="code">$base = "HKLM:\SYSTEM\CurrentControlSet\Services\SNMP\Parameters"
|
||||
|
||||
# 4 = lecture seule. Ne jamais utiliser 8, qui autorise l'ecriture.
|
||||
New-ItemProperty -Path "$base\ValidCommunities" -Name "UneChaineLongueEtAleatoire" `
|
||||
-Value 4 -PropertyType DWord -Force
|
||||
|
||||
# Seul le collecteur a le droit d'interroger
|
||||
New-ItemProperty -Path "$base\PermittedManagers" -Name "1" `
|
||||
-Value "10.0.4.10" -PropertyType String -Force
|
||||
|
||||
Restart-Service SNMP</pre>
|
||||
|
||||
<p>Puis le pare-feu :</p>
|
||||
<pre class="code">New-NetFirewallRule -DisplayName "SNMP depuis le collecteur" -Direction Inbound `
|
||||
-Protocol UDP -LocalPort 161 -RemoteAddress 10.0.4.10 -Action Allow</pre>
|
||||
|
||||
<h3>Équipements réseau</h3>
|
||||
<p>La logique est la même partout : un compte ou une communauté en lecture seule, une
|
||||
liste d'accès limitée au collecteur, et la version 3 quand le matériel la gère.</p>
|
||||
|
||||
<pre class="code"># Cisco IOS, en v3
|
||||
snmp-server view lecture iso included
|
||||
snmp-server group SUPERVISION v3 priv read lecture access 20
|
||||
snmp-server user centreon SUPERVISION v3 auth sha MotDePasseAuth priv aes 128 MotDePasseChiffrement
|
||||
access-list 20 permit 10.0.4.10</pre>
|
||||
|
||||
<pre class="code"># Cisco IOS, en v2c
|
||||
access-list 20 permit 10.0.4.10
|
||||
snmp-server community UneChaineLongueEtAleatoire RO 20</pre>
|
||||
|
||||
<p>Sur pfSense, OPNsense, FortiGate, Stormshield et la plupart des interfaces web,
|
||||
l'écran SNMP propose les mêmes notions : communauté ou utilisateur v3, interface
|
||||
d'écoute, et adresses autorisées. Renseignez toujours la dernière.</p>
|
||||
|
||||
<h3>Vérifier depuis le collecteur</h3>
|
||||
<p>Avant de déployer quoi que ce soit, assurez-vous que le collecteur obtient une réponse.
|
||||
Une configuration Centreon parfaite ne sert à rien si le port est fermé.</p>
|
||||
|
||||
<pre class="code"># v3
|
||||
snmpwalk -v3 -l authPriv -u centreon -a SHA-256 -A "MotDePasseAuth" \
|
||||
-x AES -X "MotDePasseChiffrement" 10.0.4.20 sysDescr
|
||||
|
||||
# v2c
|
||||
snmpwalk -v2c -c UneChaineLongueEtAleatoire 10.0.4.20 sysDescr</pre>
|
||||
|
||||
<p>Une réponse contenant la description du système signifie que tout est en place. Un
|
||||
silence vient du pare-feu ou de l'adresse d'écoute ; un refus d'authentification, du
|
||||
compte ou de la communauté.</p>
|
||||
|
||||
<div class="notice">
|
||||
<b>Ce que Forge Centreon attend de vous ensuite</b>
|
||||
<p>Les packs SNMP posent la version et la communauté sur le modèle d'hôte, dans les
|
||||
réglages du projet. En version 3, laissez la communauté vide et passez les options
|
||||
d'authentification par la macro <code>SNMPEXTRAOPTIONS</code> du modèle d'hôte.</p>
|
||||
</div>
|
||||
|
||||
<p style="margin-top:24px"><a class="btn" href="/editeur">Retour à l'éditeur</a></p>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,5 +1,5 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Mentions légales — Enclume{% endblock %}
|
||||
{% block title %}Mentions légales — Forge Centreon{% endblock %}
|
||||
{% block body %}
|
||||
<div class="accueil texte">
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<p>{{ hebergeur or "Non renseigné — définir ENCLUME_HEBERGEUR." }}</p>
|
||||
|
||||
<h3>Marques</h3>
|
||||
<p>Centreon est une marque déposée de Centreon SAS. Enclume est un projet indépendant, sans
|
||||
<p>Centreon est une marque déposée de Centreon SAS. Forge Centreon est un projet indépendant, sans
|
||||
lien ni affiliation avec cette société. Les noms de constructeurs et d'éditeurs cités dans le
|
||||
catalogue appartiennent à leurs propriétaires respectifs.</p>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Moderation — Enclume{% endblock %}
|
||||
{% block title %}Moderation — Forge Centreon{% endblock %}
|
||||
{% block body %}
|
||||
<div class="wrap-moderation">
|
||||
{% if not connecte %}
|
||||
|
||||
@@ -121,6 +121,11 @@ def create_app() -> Flask:
|
||||
verifies=sum(1 for p in cat.packs.values() if p.status == "verified"),
|
||||
)
|
||||
|
||||
@app.get("/aide/snmp")
|
||||
def guide_snmp() -> str:
|
||||
"""Installer et securiser SNMP, par systeme."""
|
||||
return render_template("guide-snmp.html")
|
||||
|
||||
@app.get("/mentions")
|
||||
def mentions() -> str:
|
||||
return render_template(
|
||||
@@ -252,6 +257,58 @@ def create_app() -> Flask:
|
||||
payload = request.get_json(force=True, silent=True) or {}
|
||||
return jsonify(generate(payload.get("project", payload)))
|
||||
|
||||
@app.get("/api/fuseaux")
|
||||
def api_fuseaux() -> Response:
|
||||
"""Fuseaux horaires connus : la saisie libre menait a des erreurs muettes."""
|
||||
from .model import fuseaux_horaires
|
||||
|
||||
return jsonify(fuseaux_horaires())
|
||||
|
||||
@app.get("/api/commandes-usuelles")
|
||||
def api_commandes_usuelles() -> Response:
|
||||
"""Commandes de verification livrees par Centreon."""
|
||||
return jsonify(
|
||||
[
|
||||
{"nom": "check_centreon_ping", "description": "Ping, avec seuils de perte et de latence"},
|
||||
{"nom": "check_host_alive", "description": "Ping simplifie, disponibilite de l'hote"},
|
||||
{"nom": "check-host-alive", "description": "Variante historique de check_host_alive"},
|
||||
{"nom": "check_centreon_dummy", "description": "Etat fixe, pour un hote passif"},
|
||||
{"nom": "check_centreon_snmp_value", "description": "Valeur SNMP, pour un hote sans ping"},
|
||||
]
|
||||
)
|
||||
|
||||
@app.get("/api/valeurs-standards")
|
||||
def api_valeurs_standards() -> Response:
|
||||
"""Valeurs livrees par Centreon, proposees plutot que saisies.
|
||||
|
||||
Chaque champ libre est une occasion de faute de frappe qui ne se voit
|
||||
qu'au moment du deploiement sur le central.
|
||||
"""
|
||||
return jsonify(
|
||||
{
|
||||
"modeles_hote": [
|
||||
"generic-active-host-custom",
|
||||
"generic-passive-host-custom",
|
||||
"generic-active-host",
|
||||
"generic-passive-host",
|
||||
],
|
||||
"modeles_service": [
|
||||
"generic-active-service-custom",
|
||||
"generic-passive-service-custom",
|
||||
"generic-active-service",
|
||||
"generic-passive-service",
|
||||
],
|
||||
"versions_snmp": ["1", "2c", "3"],
|
||||
"repertoires_plugins": [
|
||||
"/usr/lib/centreon/plugins",
|
||||
"/usr/lib/nagios/plugins",
|
||||
"/usr/local/centreon/plugins",
|
||||
],
|
||||
"criticites": ["Critique", "Majeure", "Mineure", "Information"],
|
||||
"collecteurs": ["Central"],
|
||||
}
|
||||
)
|
||||
|
||||
@app.get("/api/macros-notification")
|
||||
def api_macros_notification() -> Response:
|
||||
"""Les macros proposees dans l'editeur de blocs."""
|
||||
|
||||
Reference in new issue
Block a user