Le dépôt fait / explique / prouve ce qu'il affirme, vérifiable en une commande. - Phase 1 : docs/audit/affirmations.md — 54 affirmations publiques tracées vers une commande de preuve et un statut (✅/🟡/❌/⚪). - Phase 2 : CLAUDE.md réduit à un pointeur mince ; contradiction SSH levée (le code applique déjà PasswordAuthentication no + AuthenticationMethods publickey, conforme à AGENTS.md) ; section AGENTS « Codex » → « agents IA ». - Phase 3 : parcours démarrage réparé (QUICKSTART renvoyait à un modèle absent, chemins de voûte faux, commandes make périmées) ; make verifier vert (ansible-lint 33 → 0 : site.yml généré nommé, pipefail, name[template]) ; voûte Proxmox unifiée lue par le clonage (all/vault.yml). - Phase 4 : make prouver → docs/audit/preuve-<date>.md, harnais rejouable qui rappelle l'outillage existant (aucune validation réimplémentée). - Phase 5 : parcours QUICKSTART prouvé hors-ligne sur le socle ; modèle socle rendu valide (autorite interne → auto-heberge) ; split-brain d'inventaire corrigé (repli sur le répertoire existant, pas principal/). make prouver : 15 OK, 0 échec, 1 sautée (voûte). ansible-lint : 0 failure. Écarts découverts en cours de traitement (AFF-097..100) : tous résolus. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
151 lines
6.3 KiB
YAML
151 lines
6.3 KiB
YAML
---
|
|
# 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 }})."
|