From 3ea21ef27ea39a61ed00bc5e63be3432c04b15f3 Mon Sep 17 00:00:00 2001 From: Daniel Allaire Date: Sat, 14 Mar 2026 17:02:35 -0400 Subject: [PATCH] =?UTF-8?q?Corrige=20l=E2=80=99int=C3=A9gration=20Icinga?= =?UTF-8?q?=20Web=202=20et=20le=20transport=20de=20commandes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ansible/roles/icinga2/tasks/main.yml | 8 ++ .../templates/commandtransports.ini.j2 | 2 +- .../roles/icingaweb2/templates/config.ini.j2 | 3 +- ansible/roles/life_noc/tasks/main.yml | 12 +++ checks/check_life_noc_probe.py | 61 +++++++++++++ domains.yaml | 38 ++++++-- icinga/commands/check_life_noc_probe.conf | 16 ++++ icinga/services/revue.conf | 34 ++++--- icinga/templates/service_life_noc_probe.conf | 4 + scripts/generate_services.py | 88 ++++++++++++++++++- 10 files changed, 247 insertions(+), 19 deletions(-) create mode 100644 checks/check_life_noc_probe.py create mode 100644 icinga/commands/check_life_noc_probe.conf create mode 100644 icinga/templates/service_life_noc_probe.conf diff --git a/ansible/roles/icinga2/tasks/main.yml b/ansible/roles/icinga2/tasks/main.yml index bb590b1..c31834b 100644 --- a/ansible/roles/icinga2/tasks/main.yml +++ b/ansible/roles/icinga2/tasks/main.yml @@ -48,6 +48,14 @@ - validate icinga2 - restart icinga2 +- name: enable command feature + ansible.builtin.command: icinga2 feature enable command + args: + creates: /etc/icinga2/features-enabled/command.conf + notify: + - validate icinga2 + - restart icinga2 + - name: enable and start icinga2 ansible.builtin.service: name: icinga2 diff --git a/ansible/roles/icingaweb2/templates/commandtransports.ini.j2 b/ansible/roles/icingaweb2/templates/commandtransports.ini.j2 index 38a85d3..5a796be 100644 --- a/ansible/roles/icingaweb2/templates/commandtransports.ini.j2 +++ b/ansible/roles/icingaweb2/templates/commandtransports.ini.j2 @@ -1,3 +1,3 @@ [icinga2] transport = "local" -path = "{{ life_noc_icinga2_command_pipe }}" +path = "/run/icinga2/cmd/icinga2.cmd" diff --git a/ansible/roles/icingaweb2/templates/config.ini.j2 b/ansible/roles/icingaweb2/templates/config.ini.j2 index b826249..4d9c941 100644 --- a/ansible/roles/icingaweb2/templates/config.ini.j2 +++ b/ansible/roles/icingaweb2/templates/config.ini.j2 @@ -1,6 +1,7 @@ [global] show_stacktraces = "0" -config_backend = "ini" +config_backend = "db" +config_resource = "icingaweb2" [logging] log = "syslog" diff --git a/ansible/roles/life_noc/tasks/main.yml b/ansible/roles/life_noc/tasks/main.yml index 730b6b9..8492778 100644 --- a/ansible/roles/life_noc/tasks/main.yml +++ b/ansible/roles/life_noc/tasks/main.yml @@ -187,6 +187,18 @@ - validate icinga - reload icinga +- name: install generic life-noc probe plugin + ansible.builtin.copy: + src: "{{ life_noc_project_root }}/checks/check_life_noc_probe.py" + dest: /usr/lib/nagios/plugins/check_life_noc_probe.py + remote_src: true + owner: root + group: root + mode: "0755" + notify: + - validate icinga + - reload icinga + - name: ensure bpm destination directory exists ansible.builtin.file: path: "{{ life_noc_bpm_destination_dir }}" diff --git a/checks/check_life_noc_probe.py b/checks/check_life_noc_probe.py new file mode 100644 index 0000000..3302646 --- /dev/null +++ b/checks/check_life_noc_probe.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 + +import argparse +from datetime import datetime, timezone + + +def exit_with(code: int, text: str) -> None: + print(text) + raise SystemExit(code) + + +def parse_date(value: str) -> datetime: + return datetime.strptime(value, "%Y-%m-%d").replace(tzinfo=timezone.utc) + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("--probe-type", required=True) + ap.add_argument("--source-type", required=True) + ap.add_argument("--source-value", required=True) + ap.add_argument("--unit", required=True) + ap.add_argument("--unknown-lt", type=float) + ap.add_argument("--ok-gte", type=float) + ap.add_argument("--warning-gte", type=float) + ap.add_argument("--critical-gte", type=float) + ap.add_argument("--on-error", default="critical") + ap.add_argument("--label", default="life-noc") + args = ap.parse_args() + + try: + if args.probe_type != "elapsed_time": + exit_with(2, f"CRITICAL - probe_type non supporté: {args.probe_type}") + + if args.source_type != "manual_date": + exit_with(2, f"CRITICAL - source_type non supporté: {args.source_type}") + + if args.unit != "days": + exit_with(2, f"CRITICAL - unité non supportée: {args.unit}") + + dt = parse_date(args.source_value) + now = datetime.now(timezone.utc) + delta_days = (now - dt).total_seconds() / 86400.0 + + if args.critical_gte is not None and delta_days >= args.critical_gte: + exit_with(2, f"CRITICAL - {args.label}: {delta_days:.1f} jours") + if args.warning_gte is not None and delta_days >= args.warning_gte: + exit_with(1, f"WARNING - {args.label}: {delta_days:.1f} jours") + if args.ok_gte is not None and delta_days >= args.ok_gte: + exit_with(0, f"OK - {args.label}: {delta_days:.1f} jours") + if args.unknown_lt is not None and delta_days < args.unknown_lt: + exit_with(3, f"UNKNOWN - {args.label}: {delta_days:.1f} jours") + + exit_with(2, f"CRITICAL - {args.label}: seuils incohérents") + except Exception as exc: + if args.on_error == "critical": + exit_with(2, f"CRITICAL - erreur sonde: {exc}") + exit_with(3, f"UNKNOWN - erreur sonde: {exc}") + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/domains.yaml b/domains.yaml index 254129d..2f422b9 100644 --- a/domains.yaml +++ b/domains.yaml @@ -2,12 +2,40 @@ domains: revue: - name: revue-quotidienne-life-noc - date: 2026-03-07 - notes: Revue quotidienne du tableau Life-NOC - + date: "2026-03-13" + notes: Vérifier le tableau de bord Life-NOC + probe: + type: elapsed_time + source: + type: manual_date + value: "2026-03-13" + metric: + unit: days + thresholds: + unknown_lt: 1 + ok_gte: 1 + warning_gte: 2 + critical_gte: 3 + policy: + on_error: critical + - name: revue-hebdomadaire-priorites - date: 2026-03-08 - notes: Revue hebdomadaire des priorités personnelles et professionnelles + date: "2026-03-10" + notes: Revoir les priorités de la semaine + probe: + type: elapsed_time + source: + type: manual_date + value: "2026-03-10" + metric: + unit: days + thresholds: + unknown_lt: 5 + ok_gte: 5 + warning_gte: 7 + critical_gte: 10 + policy: + on_error: critical - name: revue-mensuelle-systeme-vie date: 2026-04-01 diff --git a/icinga/commands/check_life_noc_probe.conf b/icinga/commands/check_life_noc_probe.conf new file mode 100644 index 0000000..a319637 --- /dev/null +++ b/icinga/commands/check_life_noc_probe.conf @@ -0,0 +1,16 @@ +object CheckCommand "check_life_noc_probe" { + command = [ PluginDir + "/check_life_noc_probe.py" ] + + arguments = { + "--probe-type" = "$life_noc_probe_type$" + "--source-type" = "$life_noc_source_type$" + "--source-value" = "$life_noc_source_value$" + "--unit" = "$life_noc_metric_unit$" + "--unknown-lt" = "$life_noc_threshold_unknown_lt$" + "--ok-gte" = "$life_noc_threshold_ok_gte$" + "--warning-gte" = "$life_noc_threshold_warning_gte$" + "--critical-gte" = "$life_noc_threshold_critical_gte$" + "--on-error" = "$life_noc_on_error$" + "--label" = "$life_noc_label$" + } +} diff --git a/icinga/services/revue.conf b/icinga/services/revue.conf index 3e7d5c3..84a0a09 100644 --- a/icinga/services/revue.conf +++ b/icinga/services/revue.conf @@ -5,22 +5,36 @@ */ apply Service "revue-revue-quotidienne-life-noc" { - import "service_echeance" - vars.date_echeance = "2026-03-07" - vars.mock_state = "OK" - vars.mock_message = "Sous contrôle" + import "service_life_noc_probe" + vars.life_noc_probe_type = "elapsed_time" + vars.life_noc_source_type = "manual_date" + vars.life_noc_source_value = "2026-03-13" + vars.life_noc_metric_unit = "days" + vars.life_noc_label = "revue-quotidienne-life-noc" groups = [ "REVUE" ] - notes = "Revue quotidienne du tableau Life-NOC" + vars.life_noc_threshold_unknown_lt = "1" + vars.life_noc_threshold_ok_gte = "1" + vars.life_noc_threshold_warning_gte = "2" + vars.life_noc_threshold_critical_gte = "3" + vars.life_noc_on_error = "critical" + notes = "Vérifier le tableau de bord Life-NOC" assign where host.name == "life-noc" } apply Service "revue-revue-hebdomadaire-priorites" { - import "service_echeance" - vars.date_echeance = "2026-03-08" - vars.mock_state = "OK" - vars.mock_message = "Sous contrôle" + import "service_life_noc_probe" + vars.life_noc_probe_type = "elapsed_time" + vars.life_noc_source_type = "manual_date" + vars.life_noc_source_value = "2026-03-10" + vars.life_noc_metric_unit = "days" + vars.life_noc_label = "revue-hebdomadaire-priorites" groups = [ "REVUE" ] - notes = "Revue hebdomadaire des priorités personnelles et professionnelles" + vars.life_noc_threshold_unknown_lt = "5" + vars.life_noc_threshold_ok_gte = "5" + vars.life_noc_threshold_warning_gte = "7" + vars.life_noc_threshold_critical_gte = "10" + vars.life_noc_on_error = "critical" + notes = "Revoir les priorités de la semaine" assign where host.name == "life-noc" } diff --git a/icinga/templates/service_life_noc_probe.conf b/icinga/templates/service_life_noc_probe.conf new file mode 100644 index 0000000..96b34ba --- /dev/null +++ b/icinga/templates/service_life_noc_probe.conf @@ -0,0 +1,4 @@ +template Service "service_life_noc_probe" { + check_command = "check_life_noc_probe" + vars.life_noc_on_error = "critical" +} diff --git a/scripts/generate_services.py b/scripts/generate_services.py index 117dae6..2aa09b9 100644 --- a/scripts/generate_services.py +++ b/scripts/generate_services.py @@ -11,6 +11,7 @@ SERVICEGROUPS_DIR = Path("icinga/servicegroups") SERVICEGROUPS_FILE = SERVICEGROUPS_DIR / "life-noc.conf" HOST_NAME = "life-noc" SERVICE_TEMPLATE = "service_echeance" +PROBE_TEMPLATE = "service_life_noc_probe" DEFAULT_MOCK_STATE = "OK" DEFAULT_MOCK_MESSAGE = "Sous contrôle" @@ -73,7 +74,7 @@ def normalize_mock_state(value: str) -> str: return state -def render_service(domain: str, group_name: str, item: dict) -> str: +def render_mock_service(domain: str, group_name: str, item: dict) -> str: name = item["name"].strip() date = str(item["date"]).strip() notes = item.get("notes", "").strip() @@ -107,6 +108,85 @@ def render_service(domain: str, group_name: str, item: dict) -> str: return "\n".join(lines) +def render_probe_service(domain: str, group_name: str, item: dict) -> str: + name = item["name"].strip() + notes = item.get("notes", "").strip() + notes_url = item.get("notes_url", "").strip() + action_url = item.get("action_url", "").strip() + probe = item["probe"] + + probe_type = str(probe["type"]).strip() + source = probe["source"] + metric = probe["metric"] + thresholds = probe["thresholds"] + policy = probe.get("policy", {}) + + service_name = f"{domain}-{name}" + + lines = [ + f'apply Service "{icinga_escape(service_name)}" {{', + f' import "{PROBE_TEMPLATE}"', + f' vars.life_noc_probe_type = "{icinga_escape(probe_type)}"', + f' vars.life_noc_source_type = "{icinga_escape(str(source["type"]).strip())}"', + f' vars.life_noc_source_value = "{icinga_escape(str(source["value"]).strip())}"', + f' vars.life_noc_metric_unit = "{icinga_escape(str(metric["unit"]).strip())}"', + f' vars.life_noc_label = "{icinga_escape(name)}"', + f' groups = [ "{icinga_escape(group_name)}" ]', + ] + + if "unknown_lt" in thresholds: + lines.append(f' vars.life_noc_threshold_unknown_lt = "{icinga_escape(str(thresholds["unknown_lt"]))}"') + if "ok_gte" in thresholds: + lines.append(f' vars.life_noc_threshold_ok_gte = "{icinga_escape(str(thresholds["ok_gte"]))}"') + if "warning_gte" in thresholds: + lines.append(f' vars.life_noc_threshold_warning_gte = "{icinga_escape(str(thresholds["warning_gte"]))}"') + if "critical_gte" in thresholds: + lines.append(f' vars.life_noc_threshold_critical_gte = "{icinga_escape(str(thresholds["critical_gte"]))}"') + + on_error = str(policy.get("on_error", "critical")).strip() + lines.append(f' vars.life_noc_on_error = "{icinga_escape(on_error)}"') + + if notes: + lines.append(f' notes = "{icinga_escape(notes)}"') + if notes_url: + lines.append(f' notes_url = "{icinga_escape(notes_url)}"') + if action_url: + lines.append(f' action_url = "{icinga_escape(action_url)}"') + + lines.append(f' assign where host.name == "{HOST_NAME}"') + lines.append("}") + lines.append("") + + return "\n".join(lines) + + +def validate_probe(item: dict) -> None: + probe = item["probe"] + if not isinstance(probe, dict): + raise ValueError("probe doit être un objet") + + if str(probe.get("type", "")).strip() != "elapsed_time": + raise ValueError("seul probe.type=elapsed_time est supporté en v1") + + source = probe.get("source") + if not isinstance(source, dict): + raise ValueError("probe.source doit être un objet") + if str(source.get("type", "")).strip() != "manual_date": + raise ValueError("seul probe.source.type=manual_date est supporté en v1") + if "value" not in source: + raise ValueError("probe.source.value est requis") + + metric = probe.get("metric") + if not isinstance(metric, dict): + raise ValueError("probe.metric doit être un objet") + if str(metric.get("unit", "")).strip() != "days": + raise ValueError("seul probe.metric.unit=days est supporté en v1") + + thresholds = probe.get("thresholds") + if not isinstance(thresholds, dict): + raise ValueError("probe.thresholds doit être un objet") + + def render_servicegroup(group_name: str) -> str: return "\n".join([ f'object ServiceGroup "{icinga_escape(group_name)}" {{', @@ -186,7 +266,11 @@ def main() -> int: return 1 try: - content.append(render_service(domain, group_name, item)) + if "probe" in item: + validate_probe(item) + content.append(render_probe_service(domain, group_name, item)) + else: + content.append(render_mock_service(domain, group_name, item)) except ValueError as exc: print(f"Erreur dans '{raw_domain}', entrée {idx}: {exc}", file=sys.stderr) return 1