From 2ffb2fac07e645175fce744a701a8585678398e6 Mon Sep 17 00:00:00 2001 From: Fab Date: Tue, 29 Sep 2026 23:47:59 +0200 Subject: [PATCH] =?UTF-8?q?un=20worker=20peut=20prendre=20en=20charge=20de?= =?UTF-8?q?s=20json=20pr=C3=A9sents=20dans=20/kaz/jobs=20(pour=20les=20ser?= =?UTF-8?q?vices=20des=20orgas)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bin/worker.py | 309 ++++++++++++++++++++++++ bin/worker_actions/modifier_services.py | 77 ++++++ config/orgaTmpl/orga-gen.sh | 98 +++++--- 3 files changed, 454 insertions(+), 30 deletions(-) create mode 100644 bin/worker.py create mode 100644 bin/worker_actions/modifier_services.py diff --git a/bin/worker.py b/bin/worker.py new file mode 100644 index 0000000..60b0c76 --- /dev/null +++ b/bin/worker.py @@ -0,0 +1,309 @@ +#!/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) diff --git a/bin/worker_actions/modifier_services.py b/bin/worker_actions/modifier_services.py new file mode 100644 index 0000000..21ebaf3 --- /dev/null +++ b/bin/worker_actions/modifier_services.py @@ -0,0 +1,77 @@ +import logging +import os +import subprocess + +#ex de json accepté: +#{ +# "action": "modifier_services", +# "creer": [ +# "cloud" +# ], +# "enlever": [ +# "paheko", +# "wp" +# ], +# "nom_court_orga": "monorga11", +# "emails": [ +# "admin@kaz.bzh", +# "contact@kaz.bzh" +# ] +# } + +log = logging.getLogger(__name__) + + +def run(message, encours): + + nom_court_orga = message["nom_court_orga"] + + services_creer = message.get("creer", []) + services_enlever = message.get("enlever", []) + + script = "/kaz/config/orgaTmpl/orga-gen.sh" + + mail_file = encours.removesuffix(".encours") + ".mail" + + command = [script] + + command.extend( + f"+{service}" + for service in services_creer + ) + + command.extend( + f"-{service}" + for service in services_enlever + ) + + command.append(nom_court_orga) + + log.info( + f"Exécution : {' '.join(command)}" + ) + + env = os.environ.copy() + env["ORGA_GEN_MAIL_FILE"] = mail_file + + result = subprocess.run( + command, + capture_output=True, + text=True, + env=env, + ) + + log.info( + f"Résultat du script :\n{result.stdout}" + ) + + if result.stderr: + log.warning( + f"Sortie stderr du script :\n{result.stderr}" + ) + + if result.returncode != 0: + raise RuntimeError( + f"Le script a échoué avec le code " + f"{result.returncode}" + ) diff --git a/config/orgaTmpl/orga-gen.sh b/config/orgaTmpl/orga-gen.sh index d509bc9..5c347f1 100755 --- a/config/orgaTmpl/orga-gen.sh +++ b/config/orgaTmpl/orga-gen.sh @@ -440,7 +440,7 @@ if [[ -n "${STAGE_DEFAULT}${STAGE_CREATE}" ]]; then ln -sf ../../config/orgaTmpl/orga-gen.sh ln -sf ../../config/orgaTmpl/orga-rm.sh - ln -sf ../../config/orgaTmpl/init-paheko.sh + #ln -sf ../../config/orgaTmpl/init-paheko.sh #ln -sf ../../config/orgaTmpl/initdb.d/ #ln -sf ../../config/orgaTmpl/app/ ln -sf ../../config/orgaTmpl/wiki-conf/ @@ -525,7 +525,8 @@ fi if [[ -n "${STAGE_DEFAULT}${STAGE_INIT}" ]]; then [ -z "$DBaInitialiser" ] || ./init-db.sh $DBaInitialiser # ########## init services - [[ "${paheko}" = "on" ]] && ./init-paheko.sh + #[[ "${paheko}" = "on" ]] && ./init-paheko.sh + [[ "${paheko}" = "on" ]] # initCmd="--install" # # XXX risque d'écraser user DB # [[ "${cloud}" = "on" ]] && initCmd="$initCmd -cloud" @@ -555,10 +556,60 @@ if [[ -n "${STAGE_DEFAULT}${STAGE_CREATE}" ]]; then MAIL="Bonjour, -et hop ! voici les accès au(x) service(s) demandé(s): +et hop ! comme demandé: " - if [[ "${wp}" = "on" ]]; then +#quels services ont été arrêtés ou démarés ? +for ARG in "$@"; do + case "${ARG}" in + '-paheko'* ) + MAIL="$MAIL +Paheko (compta) a été supprimé. +" + ( + cd /kaz/dockers/paheko/ && + ./docker-compose-gen.sh && + docker-compose down && + docker-compose up -d + ) + + ;; + + '-wp'* ) + MAIL="$MAIL +Le site web a été supprimé. +" + ;; + + '-cloud'* ) + MAIL="$MAIL +Le cloud dédié a été supprimé. +" + ;; + + '+paheko'* ) + MAIL_ADMIN_PASS=$(apg -n 1 -m 16 -M NCL) + + MAIL="$MAIL +Paheko: il te faut terminer l'installation + +adresse: https://${ORGA}-paheko.kaz.bzh/admin/ + +Dans 'Nom de l'association' et dans 'Nom et prénom', mets: ${ORGA} +Dans 'Adresse E-Mail', mets: ${GREEN}XXXXXX${RESET} +Dans les 2 champs 'Mots de passe', mets: ${MAIL_ADMIN_PASS} + +doc: https://wiki.kaz.bzh/paheko/start +" + ( + cd /kaz/dockers/paheko/ && + ./docker-compose-gen.sh && + docker-compose down && + docker-compose up -d + ) + ;; + + '+wp'* ) MAIL_ADMIN=`grep WORDPRESS_ADMIN_USER /kaz/secret/orgas/${ORGA}/env-wpServ | cut -f2 -d=` MAIL_ADMIN_PASS=`grep WORDPRESS_ADMIN_PASSWORD /kaz/secret/orgas/${ORGA}/env-wpServ | cut -f2 -d=` @@ -572,10 +623,10 @@ doc: https://wiki.kaz.bzh/wordpress/start La première chose à faire est de paramétrer ton site pour qu'il puisse envoyer des mails: https://wiki.kaz.bzh/wordpress/premiere_connexion Si tu as envie d'avoir comme adresse de site , https://${ORGA}.kaz.bzh au lieu de https://${ORGA}-wp.kaz.bzh, fais nous signe ! -" - fi - - if [[ "${cloud}" = "on" ]]; then +" + ;; + + '+cloud'* ) MAIL_ADMIN=`grep NEXTCLOUD_ADMIN_USER /kaz/secret/orgas/${ORGA}/env-nextcloudServ | cut -f2 -d=` MAIL_ADMIN_PASS=`grep NEXTCLOUD_ADMIN_PASSWORD /kaz/secret/orgas/${ORGA}/env-nextcloudServ | cut -f2 -d=` @@ -588,21 +639,10 @@ mot de passe: ${MAIL_ADMIN_PASS} doc: https://wiki.kaz.bzh/nextcloud/start La première chose à faire est de paramétrer ton cloud pour qu'il puisse envoyer des mails: https://wiki.kaz.bzh/nextcloud/administration/start#pour_parametrer_l_envoi_des_mails_de_notification_depuis_votre_cloud " - fi - - if [[ "${paheko}" = "on" ]]; then - MAIL_ADMIN_PASS=$(apg -n 1 -m 16 -M NCL) - - MAIL="$MAIL -Paheko: -adresse: https://${ORGA}-paheko.kaz.bzh/admin/ -identifiant: ${GREEN}--mettre ici le mail de l'orga--${RESET} -mot de passe: ${MAIL_ADMIN_PASS} - -doc: https://wiki.kaz.bzh/paheko/start -" - fi - + ;; + esac +done + MAIL="$MAIL Pour tes prochaines questions, nous t'invitons à les poser sur notre agora, dans le canal \"Un soucis, une question\" sur https://agora.kaz.bzh/kaz/channels/une-question--un-soucis @@ -616,6 +656,10 @@ L'équipe Sysadmin, pour la collégiale de Kaz." fi +if [[ -n "${ORGA_GEN_MAIL_FILE}" ]]; then + echo -e "$MAIL" > "$ORGA_GEN_MAIL_FILE" +fi + echo -e "${GREEN}" echo "*****************************************************" echo "Voici le mail à copier/coller et à envoyer à l'orga :" @@ -623,11 +667,5 @@ echo "*****************************************************" echo -e "${RESET}" echo -e "$MAIL" echo -e "${GREEN}" -echo "MAIS AVANT D'ENVOYER CE MAIL, IL FAUT : - * completer la fiche paheko de l'orga en cochant les services crees" - -if [[ "${paheko}" = "on" ]]; then -echo " * lancer /kaz/dockers/paheko/docker-compose-gen.sh, restart le docker puis finir d'installer paheko en allant sur https://${ORGA}-paheko.kaz.bzh/admin/" -fi -echo -e ${RESET} +echo -e "${RESET}" ##### FIN MAIL