#!/usr/bin/env bash set -euo pipefail ROOT="${1:-.}" need_file() { local f="$1" if [[ ! -f "$ROOT/$f" ]]; then echo "Fichier introuvable: $f" >&2 exit 1 fi } echo "==> Vérification du dépôt" need_file "checks/check_life_noc_probe.py" need_file "icinga/commands/check_life_noc_probe.conf" need_file "scripts/generate_services.py" need_file "domains.yaml" echo "==> Sauvegarde" mkdir -p "$ROOT/.patch-backup-days-until-due" cp -a "$ROOT/checks/check_life_noc_probe.py" "$ROOT/.patch-backup-days-until-due/check_life_noc_probe.py.bak" cp -a "$ROOT/icinga/commands/check_life_noc_probe.conf" "$ROOT/.patch-backup-days-until-due/check_life_noc_probe.conf.bak" cp -a "$ROOT/scripts/generate_services.py" "$ROOT/.patch-backup-days-until-due/generate_services.py.bak" cp -a "$ROOT/domains.yaml" "$ROOT/.patch-backup-days-until-due/domains.yaml.bak" echo "==> Réécriture de checks/check_life_noc_probe.py" cat > "$ROOT/checks/check_life_noc_probe.py" <<'PYEOF' #!/usr/bin/env python3 import argparse from datetime import datetime, timezone from pathlib import Path import yaml 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 load_input_value(inputs_file: str, item_key: str) -> str: path = Path(inputs_file) if not path.exists(): raise ValueError(f"inputs file introuvable: {inputs_file}") data = yaml.safe_load(path.read_text(encoding="utf-8")) if not isinstance(data, dict): raise ValueError(f"inputs file invalide: {inputs_file}") if item_key not in data: raise ValueError(f"item_key introuvable dans le store: {item_key}") item = data[item_key] if not isinstance(item, dict): raise ValueError(f"entrée invalide pour item_key: {item_key}") if "value" not in item: raise ValueError(f"champ 'value' absent pour item_key: {item_key}") return str(item["value"]).strip() 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") ap.add_argument("--inputs-file") ap.add_argument("--item-key") ap.add_argument("--unit", required=True) ap.add_argument("--unknown-lt", type=float) ap.add_argument("--unknown-gte", type=float) ap.add_argument("--ok-gte", type=float) ap.add_argument("--ok-lt", type=float) ap.add_argument("--warning-gte", type=float) ap.add_argument("--warning-lt", type=float) ap.add_argument("--critical-gte", type=float) ap.add_argument("--critical-lt", type=float) ap.add_argument("--on-error", default="critical") ap.add_argument("--label", default="life-noc") args = ap.parse_args() try: 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}") if args.inputs_file and args.item_key: source_value = load_input_value(args.inputs_file, args.item_key) elif args.source_value: source_value = args.source_value else: raise ValueError("aucune source fournie: inputs-file/item-key ou source-value requis") dt = parse_date(source_value) now = datetime.now(timezone.utc) if args.probe_type == "elapsed_time": metric = (now - dt).total_seconds() / 86400.0 if args.critical_gte is not None and metric >= args.critical_gte: exit_with(2, f"CRITICAL - {args.label}: {metric:.1f} jours") if args.warning_gte is not None and metric >= args.warning_gte: exit_with(1, f"WARNING - {args.label}: {metric:.1f} jours") if args.ok_gte is not None and metric >= args.ok_gte: exit_with(0, f"OK - {args.label}: {metric:.1f} jours") if args.unknown_lt is not None and metric < args.unknown_lt: exit_with(3, f"UNKNOWN - {args.label}: {metric:.1f} jours") exit_with(2, f"CRITICAL - {args.label}: seuils incohérents") elif args.probe_type == "days_until_due": metric = (dt - now).total_seconds() / 86400.0 if args.critical_lt is not None and metric < args.critical_lt: exit_with(2, f"CRITICAL - {args.label}: {metric:.1f} jours restants") if args.warning_lt is not None and metric < args.warning_lt: exit_with(1, f"WARNING - {args.label}: {metric:.1f} jours restants") if args.ok_lt is not None and metric < args.ok_lt: exit_with(0, f"OK - {args.label}: {metric:.1f} jours restants") if args.unknown_gte is not None and metric >= args.unknown_gte: exit_with(3, f"UNKNOWN - {args.label}: {metric:.1f} jours restants") exit_with(2, f"CRITICAL - {args.label}: seuils incohérents") else: exit_with(2, f"CRITICAL - probe_type non supporté: {args.probe_type}") 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()) PYEOF chmod +x "$ROOT/checks/check_life_noc_probe.py" echo "==> Réécriture de icinga/commands/check_life_noc_probe.conf" cat > "$ROOT/icinga/commands/check_life_noc_probe.conf" <<'CONFEOF' 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$" "--inputs-file" = "$life_noc_inputs_file$" "--item-key" = "$life_noc_item_key$" "--unit" = "$life_noc_metric_unit$" "--unknown-lt" = "$life_noc_threshold_unknown_lt$" "--unknown-gte" = "$life_noc_threshold_unknown_gte$" "--ok-gte" = "$life_noc_threshold_ok_gte$" "--ok-lt" = "$life_noc_threshold_ok_lt$" "--warning-gte" = "$life_noc_threshold_warning_gte$" "--warning-lt" = "$life_noc_threshold_warning_lt$" "--critical-gte" = "$life_noc_threshold_critical_gte$" "--critical-lt" = "$life_noc_threshold_critical_lt$" "--on-error" = "$life_noc_on_error$" "--label" = "$life_noc_label$" } } CONFEOF echo "==> Patch de scripts/generate_services.py" python3 - "$ROOT/scripts/generate_services.py" <<'PYEOF' from pathlib import Path import sys path = Path(sys.argv[1]) text = path.read_text(encoding="utf-8") old = ' if "critical_gte" in thresholds:\n lines.append(f\' vars.life_noc_threshold_critical_gte = "{icinga_escape(str(thresholds["critical_gte"]))}"\')\n' new = old + ' if "unknown_gte" in thresholds:\n lines.append(f\' vars.life_noc_threshold_unknown_gte = "{icinga_escape(str(thresholds["unknown_gte"]))}"\')\n if "ok_lt" in thresholds:\n lines.append(f\' vars.life_noc_threshold_ok_lt = "{icinga_escape(str(thresholds["ok_lt"]))}"\')\n if "warning_lt" in thresholds:\n lines.append(f\' vars.life_noc_threshold_warning_lt = "{icinga_escape(str(thresholds["warning_lt"]))}"\')\n if "critical_lt" in thresholds:\n lines.append(f\' vars.life_noc_threshold_critical_lt = "{icinga_escape(str(thresholds["critical_lt"]))}"\')\n' if old not in text: raise SystemExit("Bloc seuils introuvable dans generate_services.py") text = text.replace(old, new, 1) old2 = ' probe_type = str(probe.get("type", "")).strip()\n if probe_type not in {"elapsed_time"}:\n raise ValueError("seul probe.type=elapsed_time est supporté en v1")\n' new2 = ' probe_type = str(probe.get("type", "")).strip()\n if probe_type not in {"elapsed_time", "days_until_due"}:\n raise ValueError("seuls probe.type=elapsed_time et days_until_due sont supportés")\n' if old2 in text: text = text.replace(old2, new2, 1) else: old3 = ' probe_type = str(probe.get("type", "")).strip()\n if probe_type not in {"elapsed_time", "days_until_due"}:\n raise ValueError("seuls probe.type=elapsed_time et days_until_due sont supportés")\n' if old3 not in text: raise SystemExit("Bloc validate_probe introuvable ou inattendu dans generate_services.py") path.write_text(text, encoding="utf-8") print("generate_services.py patché") PYEOF echo "==> Création du store data/inputs/obligations-legales-personnelles.yaml" mkdir -p "$ROOT/data/inputs" cat > "$ROOT/data/inputs/obligations-legales-personnelles.yaml" <<'YAMLEOF' renouvellement-permis-conduire: value: "2026-09-20" captured_at: "2026-03-14T23:30:00Z" origin: manual YAMLEOF echo "==> Mise à jour de domains.yaml" python3 - "$ROOT/domains.yaml" <<'PYEOF' from pathlib import Path import sys import yaml path = Path(sys.argv[1]) data = yaml.safe_load(path.read_text(encoding="utf-8")) domain = "obligations-legales-personnelles" items = data.get("domains", {}).get(domain) if not isinstance(items, list): raise SystemExit(f"Domaine introuvable ou invalide: {domain}") target = None for item in items: if isinstance(item, dict) and item.get("name") == "renouvellement-permis-conduire": target = item break if target is None: raise SystemExit("Item 'renouvellement-permis-conduire' introuvable dans domains.yaml") target["date"] = "2026-09-20" target["notes"] = "Vérifier la proximité de l’échéance du permis" target["probe"] = { "type": "days_until_due", "source": { "type": "manual_date", "inputs_file": "/opt/life-noc/data/inputs/obligations-legales-personnelles.yaml", "item_key": "renouvellement-permis-conduire", }, "metric": { "unit": "days", }, "thresholds": { "critical_lt": 7, "warning_lt": 30, "ok_lt": 90, "unknown_gte": 90, }, "policy": { "on_error": "critical", }, } path.write_text(yaml.safe_dump(data, sort_keys=False, allow_unicode=True), encoding="utf-8") print("domains.yaml mis à jour") PYEOF echo "==> Régénération" ( cd "$ROOT" python3 scripts/generate_services.py ) echo echo "Patch terminé." echo "Étapes suivantes :" echo " python3 scripts/generate_services.py" echo " make check" echo " make deploy-with-bpm" echo echo "Test utile ensuite :" echo " python3 scripts/life_noc_input.py set obligations-legales-personnelles renouvellement-permis-conduire 2026-03-20"