#!/usr/bin/env python3 #KAN: 29/09/2026 #KOI: permettre de lancer des actions pour les orga depuis paheko #KI: fab #structure: # #/kaz/ #├── bin/ #│ ├── worker.py #│ └── worker_actions/ #│ ├── __init__.py #│ ├── modifier_services.py #│ └── definir_quota.py #└── jobs/ # ├── job-123-ABCD.json # ├── job-789-HIJS.json.encours # ├── job-456-EFGH.json.OK # ├── job-089-LMNO.json.KO # └── ... import glob import importlib import json import logging import os import subprocess import sys import urllib.request BASE_DIR = "/kaz/bin" JOBS_DIR = "/kaz/jobs" if BASE_DIR not in sys.path: sys.path.insert(0, BASE_DIR) # ---------------------------------------------------------------------- # LOG # ---------------------------------------------------------------------- LOG_DIR = "/kaz/log" LOG_FILE = os.path.join(LOG_DIR, "worker.log") os.makedirs(LOG_DIR, exist_ok=True) logging.basicConfig( level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s", datefmt="%Y-%m-%d %H:%M:%S", handlers=[ logging.FileHandler(LOG_FILE, encoding="utf-8"), logging.StreamHandler(sys.stdout), ], ) log = logging.getLogger(__name__) # ---------------------------------------------------------------------- # ENVOI D'UN MESSAGE MATTERMOST # ---------------------------------------------------------------------- def send_mattermost(message): url_agora = "agora.kaz.bzh" result = subprocess.run( [ "bash", "-c", ". /kaz/secret/env-mattermostAdmin && " "printf '%s' \"$token_webhook\"" ], capture_output=True, text=True, check=True, ) token_webhook = result.stdout url = f"https://{url_agora}/hooks/{token_webhook}" payload = json.dumps({ "channel": "creation-comptes", "text": message, }).encode("utf-8") request = urllib.request.Request( url, data=payload, headers={ "Content-Type": "application/json", }, method="POST", ) with urllib.request.urlopen(request) as response: if response.status < 200 or response.status >= 300: raise RuntimeError( f"Erreur Mattermost : HTTP {response.status}" ) # ---------------------------------------------------------------------- # ENVOI DU MAIL # ---------------------------------------------------------------------- def send_mail(mail_file, emails): if not emails: log.info("Aucun destinataire de mail.") return if not os.path.isfile(mail_file): log.info(f"Aucun mail à envoyer : {mail_file}") return with open(mail_file, "r", encoding="utf-8") as f: mail = f.read() if not mail.strip(): log.info(f"Le fichier mail est vide : {mail_file}") return #PROD #recipients = [email.strip() for email in emails if email.strip()] #TEST recipients = ["fab@kaz.bzh"] if not recipients: log.info("Aucun destinataire de mail valide.") return #TEST annonce = ( "#################################################\n" "Pendant quelques temps, contact@kaz.bzh va recevoir les mails de modification des services.\n" "Vérifier le contenu du mail et si OK, le transférer à :\n" + "\n".join(emails) + "\n" "En supprimant bien sûr cette annonce.\n" "Plus tard, le mail partira directement, aussi, il faut bien le valider.\n" "#################################################\n" "\n" ) mail = annonce + mail #FIN TEST message = ( "From: contact@kaz.bzh\n" f"To: {', '.join(recipients)}\n" "Subject: KAZ - informations\n" "Content-Type: text/plain; charset=UTF-8\n" "\n" + mail ) log.info(f"Envoi du mail à : {', '.join(recipients)}") result = subprocess.run( ["/usr/sbin/sendmail", "-t", "-i"], input=message, text=True, capture_output=True, ) if result.returncode != 0: raise RuntimeError( f"sendmail a échoué avec le code {result.returncode}: " f"{result.stderr}" ) log.info("Mail envoyé avec succès.") # ---------------------------------------------------------------------- # RECHERCHE D'UN JOB # ---------------------------------------------------------------------- jobs = sorted( glob.glob( os.path.join(JOBS_DIR, "job-*.json") ) ) if not jobs: log.info("Aucun job à traiter.") sys.exit(0) # ---------------------------------------------------------------------- # UN SEUL JOB # ---------------------------------------------------------------------- job = jobs[0] encours = job + ".encours" ok = job + ".OK" ko = job + ".KO" mail_file = job + ".mail" log.info(f"Job trouvé : {job}") # ---------------------------------------------------------------------- # PRISE EN CHARGE DU JOB # ---------------------------------------------------------------------- try: os.rename(job, encours) except FileNotFoundError: # Le fichier a disparu entre le glob() et le rename(). log.info(f"Job disparu entre temps : {job}") sys.exit(0) except OSError as e: log.error(f"Impossible de prendre le job en charge : {e}") sys.exit(1) log.info(f"Job en cours : {encours}") # ---------------------------------------------------------------------- # LECTURE DU JSON # ---------------------------------------------------------------------- try: with open(encours, "r", encoding="utf-8") as f: message = json.load(f) log.info(f"Message : {message}") action = message.get("action") if not action: raise ValueError("Le champ 'action' est absent du JSON") # ------------------------------------------------------------------ # CHARGEMENT ET EXECUTION DE L'ACTION # ------------------------------------------------------------------ log.info(f"Action demandée : {action}") module = importlib.import_module( f"worker_actions.{action}" ) module.run(message, encours) # ------------------------------------------------------------------ # ENVOI DU MAIL # ------------------------------------------------------------------ send_mail( mail_file, message.get("emails", []) ) # if os.path.isfile(mail_file): # os.remove(mail_file) # log.info(f"Fichier mail supprimé : {mail_file}") # ------------------------------------------------------------------ # ENVOI DU MSG MM # ------------------------------------------------------------------ message_mattermost = json.dumps( message, indent=2, ensure_ascii=False ) send_mattermost(message_mattermost) # ------------------------------------------------------------------ # SUCCES # ------------------------------------------------------------------ os.rename(encours, ok) log.info(f"Job terminé avec succès : {ok}") # ---------------------------------------------------------------------- # ERREUR # ---------------------------------------------------------------------- except Exception as e: log.exception( f"Erreur lors du traitement du job : {e}" ) try: os.rename(encours, ko) log.info(f"Job marqué KO : {ko}") except OSError as rename_error: log.error( f"Impossible de renommer le job en .KO : {rename_error}" ) sys.exit(1)