--- # make valider — RECETTE D'ACCEPTATION : les services FONCTIONNENT (pas juste déployés). # Lecture seule / faible impact. Complète les *-verifier (statiques, sur le plan) # et verifier-hote (niveau hôte) par une vérif FONCTIONNELLE de bout en bout. # # v1 : cibles Prometheus UP + endpoints HTTPS exposés répondent. # À venir : courriel bout-en-bout (envoi→IMAP), restauration de sauvegarde. - name: "Recette — Prometheus : toutes les cibles de supervision UP" hosts: serveur_prometheus gather_facts: false become: false tasks: - name: Interroger l'API des cibles actives ansible.builtin.uri: url: "http://localhost:9090/api/v1/targets?state=active" return_content: true register: prom_targets - name: Analyser up / down vars: cibles: "{{ (prom_targets.content | from_json).data.activeTargets }}" en_panne: "{{ cibles | rejectattr('health', 'equalto', 'up') | list }}" ansible.builtin.set_fact: recette_prom_total: "{{ cibles | length }}" recette_prom_down: "{{ en_panne | map(attribute='labels.instance') | list }}" - name: "✔ Prometheus" ansible.builtin.assert: that: "recette_prom_down | length == 0" success_msg: "OK — {{ recette_prom_total }} cibles, toutes UP." fail_msg: "ÉCHEC — cibles DOWN : {{ recette_prom_down }}" - name: "Recette — Edge : les vhosts HTTPS exposés répondent" hosts: serveur_nginx gather_facts: false become: true tasks: - name: Lister les valeurs de la directive server_name (première colonne exacte) ansible.builtin.shell: cmd: >- set -o pipefail; nginx -T 2>/dev/null | awk '$1=="server_name"{for(i=2;i<=NF;i++){gsub(/;/,"",$i); print $i}}' | sort -u executable: /bin/bash register: recette_server_names changed_when: false - name: Retenir les vhosts du domaine interne (exclut défauts/placeholders) ansible.builtin.set_fact: recette_fqdns: >- {{ recette_server_names.stdout_lines | select('search', '\.' ~ (domaine_interne | regex_escape) ~ '$') | list }} - name: Interroger chaque vhost exposé en HTTPS (via l'edge) ansible.builtin.uri: url: "https://{{ item }}/" validate_certs: false status_code: [200, 301, 302, 401, 403] follow_redirects: none timeout: 10 loop: "{{ recette_fqdns }}" register: recette_http failed_when: false - name: "✔ Endpoints HTTPS" vars: echecs: "{{ recette_http.results | rejectattr('status', 'in', [200, 301, 302, 401, 403]) | list }}" ansible.builtin.assert: that: "echecs | length == 0" success_msg: "OK — {{ recette_http.results | length }} vhosts répondent : {{ recette_fqdns }}" fail_msg: "ÉCHEC — vhosts KO : {{ echecs | map(attribute='item') | list }} (statuts {{ echecs | map(attribute='status') | list }})" - name: "Recette — Courriel bout-en-bout (MTA -> LMTP -> Maildir)" hosts: serveur_dovecot gather_facts: true become: true vars: recette_mail_dest: "testmail@{{ domaine_interne }}" recette_mail_from: "recette@{{ domaine_interne }}" tasks: - name: Jeton unique du message de test ansible.builtin.set_fact: recette_mail_token: "SETOPS-VALIDER-{{ ansible_date_time.iso8601_basic_short }}-{{ 999999 | random }}" - name: Envoyer via le MTA (sendmail — flux réel postfix -> LDAP -> LMTP -> Dovecot) ansible.builtin.shell: cmd: >- set -o pipefail; printf 'Subject: %s\nFrom: %s\nTo: %s\n\nRecette d acceptation Set-OPS.\n' '{{ recette_mail_token }}' '{{ recette_mail_from }}' '{{ recette_mail_dest }}' | /usr/sbin/sendmail -f '{{ recette_mail_from }}' '{{ recette_mail_dest }}' executable: /bin/bash delegate_to: "{{ groups['serveur_postfix'][0] }}" changed_when: true - name: Attendre la remise dans la boîte (doveadm, LMTP asynchrone) ansible.builtin.command: cmd: "doveadm search -u {{ recette_mail_dest }} mailbox INBOX subject {{ recette_mail_token }}" register: recette_mail_search until: recette_mail_search.stdout | trim | length > 0 retries: 10 delay: 3 changed_when: false failed_when: false - name: "✔ Courriel" ansible.builtin.assert: that: "recette_mail_search.stdout | trim | length > 0" success_msg: "OK — courriel remis à {{ recette_mail_dest }} (envoi -> LMTP -> Maildir prouvé)." fail_msg: "ÉCHEC — courriel NON remis à {{ recette_mail_dest }} après 30 s." - name: Nettoyer le message de test (best-effort) ansible.builtin.command: cmd: "doveadm expunge -u {{ recette_mail_dest }} mailbox INBOX subject {{ recette_mail_token }}" changed_when: true failed_when: false - name: "Recette — Sauvegardes : le dernier snapshot restic se restaure" hosts: client_backup gather_facts: false become: true tasks: - name: Restaurer le dernier snapshot dans un dossier temporaire (lecture seule sur le dépôt) ansible.builtin.shell: cmd: | set -o pipefail repo=$(grep -oP 'RESTIC_REPOSITORY="\K[^"]+' /usr/local/sbin/setops-sauvegarder.sh) export RESTIC_REPOSITORY="$repo" RESTIC_PASSWORD_FILE=/etc/setops/restic.pass n=$(restic snapshots 2>/dev/null | grep -cE '^[0-9a-f]{8} ' || echo 0) if [ "$n" -lt 1 ]; then echo "snapshots=0 restored_files=0"; exit 0; fi tmp=$(mktemp -d) if restic restore latest --target "$tmp" >/dev/null 2>&1; then f=$(find "$tmp" -type f | wc -l) else f=-1 fi rm -rf "$tmp" echo "snapshots=$n restored_files=$f" executable: /bin/bash register: recette_restore changed_when: false - name: "✔ Sauvegardes" vars: restaures: "{{ recette_restore.stdout | regex_search('restored_files=(-?\\d+)', '\\1') | first | int }}" snaps: "{{ recette_restore.stdout | regex_search('snapshots=(\\d+)', '\\1') | first | int }}" ansible.builtin.assert: that: "restaures | int > 0" success_msg: "OK — {{ snaps }} snapshot(s), restauration du dernier vérifiée ({{ restaures }} fichiers)." fail_msg: "ÉCHEC — restauration KO (snapshots={{ snaps }}, fichiers restaurés={{ restaures }})."