Masquer les packs deja importes dans la fenetre d'import
build / Images Harbor (catalog-sync, Dockerfile.catalog-sync) (push) Successful in 9m52s
build / Garde-fou (push) Successful in 10m41s
build / Images Harbor (web, Dockerfile) (push) Successful in 13m15s

This commit is contained in:
hcornet committed 2026-09-11 19:32:39 +02:00
1 parent 2187a1971e
commit ec195cac8f
71 files changed
+796 -30

No files matched your search

+2
View File
@@ -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,
)
+7
View 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]))
+21
View File
@@ -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:
+12
View File
@@ -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
View File
@@ -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();
+11
View File
@@ -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; }
+3 -2
View File
@@ -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>
+5 -4
View File
@@ -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>
+143
View File
@@ -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 %}
+2 -2
View File
@@ -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 -1
View File
@@ -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 %}
+57
View File
@@ -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."""