Paillasse/ansible/roles/portail/tasks/03-code.yml
Adam Belkacemi 6ea7a1cc16 ajoute le stockage S3 des devoirs maison (bloc B, dernier chantier)
Dépôt de fichier en plus du texte libre pour les devoirs maison, stocké
sur Hetzner Object Storage (jamais sur le disque du serveur), masqué
côté élève tant que le bucket n'est pas configuré. Purge RGPD étendue
pour supprimer aussi les fichiers d'Object Storage. Ansible lit les
identifiants depuis un fichier que seul Adam peut créer sur le serveur
(runbook docs/runbooks/stockage-s3.md) ; le bloc B du CLAUDE.md est
maintenant complet.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-14 09:52:15 +02:00

220 lines
9 KiB
YAML

---
- name: créer le dossier de l'application
ansible.builtin.file:
path: "{{ portail_dossier_app }}"
state: directory
owner: "{{ ansible_user }}"
group: "{{ ansible_user }}"
mode: "0755"
# storage/ et bootstrap/cache/ sont de l'état et du cache runtime, jamais du
# code : on ne les synchronise jamais depuis le Mac (un rsync --delete les
# effacerait sinon, y compris storage/app/ où atterriront plus tard les
# travaux d'élèves). On crée leur structure une fois, puis on laisse
# www-data (PHP-FPM) les posséder en continu.
- name: créer la structure storage/ et bootstrap/cache/ (une seule fois)
ansible.builtin.file:
path: "{{ portail_dossier_app }}/{{ item }}"
state: directory
owner: www-data
group: www-data
mode: "0775"
loop:
- storage/app/public
- storage/framework/cache/data
- storage/framework/sessions
- storage/framework/views
- storage/logs
- bootstrap/cache
- name: synchroniser le code du portail (sans vendor/node_modules/.env/storage)
become: false
ansible.posix.synchronize:
src: "{{ playbook_dir }}/../portail/"
dest: "{{ portail_dossier_app }}/"
delete: true
rsync_opts:
- "--exclude=/vendor"
- "--exclude=/node_modules"
- "--exclude=/.env"
- "--exclude=/database/database.sqlite"
- "--exclude=/storage"
- "--exclude=/bootstrap/cache"
- "--exclude=/public/build"
# Produit sur le serveur par `npm run build` (scripts/copier-vendor.mjs),
# donc appartenant à root : ne jamais le synchroniser depuis le Mac.
- "--exclude=/public/vendor"
- "--exclude=/public/hot"
- "--exclude=/.git"
register: portail_code_synchronise
notify: redémarrer horizon
- name: installer les dépendances PHP (composer)
ansible.builtin.command:
cmd: composer install --no-dev --optimize-autoloader --no-interaction
args:
chdir: "{{ portail_dossier_app }}"
environment:
COMPOSER_ALLOW_SUPERUSER: "1"
register: portail_composer_install_app
changed_when: "'Nothing to install' not in portail_composer_install_app.stdout"
- name: vérifier si .env existe déjà
ansible.builtin.stat:
path: "{{ portail_dossier_app }}/.env"
register: portail_env_stat
- name: générer une clé d'application (une seule fois)
ansible.builtin.command:
cmd: php artisan key:generate --show
args:
chdir: "{{ portail_dossier_app }}"
register: portail_app_key_generee
when: not portail_env_stat.stat.exists
changed_when: true
- name: déployer .env de production (une seule fois, jamais réécrit ensuite)
ansible.builtin.template:
src: env.production.j2
dest: "{{ portail_dossier_app }}/.env"
owner: "{{ ansible_user }}"
group: www-data
mode: "0640"
vars:
portail_app_key: "{{ portail_app_key_generee.stdout }}"
portail_app_url: "http://{{ ansible_host }}"
when: not portail_env_stat.stat.exists
# Variables ajoutées après le premier déploiement (étapes 9 et 10) : mises à
# jour ligne par ligne, pour ne jamais retoucher le reste d'un .env déjà en
# place (APP_KEY en particulier, qui ne doit jamais changer sans le vouloir).
#
# Authentification par mot de passe, pas par application credential : Heat
# refuse de créer un trust Keystone à partir d'un jeton d'application
# credential (« Delegated tokens cannot manage trusts », constaté le
# 2026-10-08) — voir portail/config/openstack.php et docs/journal.md.
- name: lire les identifiants OpenStack du portail (étape 6)
ansible.builtin.slurp:
src: /etc/kolla/clouds-paillasse.yaml
register: portail_clouds_brut
- name: extraire le nom d'utilisateur et le mot de passe
ansible.builtin.set_fact:
portail_os_username: "{{ portail_clouds_contenu | regex_search('username:\\s*\"?([^\"\\n]+)', '\\1') | first }}"
portail_os_password: "{{ portail_clouds_contenu | regex_search('password:\\s*\"?([^\"\\n]+)', '\\1') | first }}"
portail_os_project_name: "{{ portail_clouds_contenu | regex_search('project_name:\\s*\"?([^\"\\n]+)', '\\1') | first }}"
vars:
portail_clouds_contenu: "{{ portail_clouds_brut.content | b64decode }}"
no_log: true
- name: ajouter/mettre à jour les variables OpenStack dans .env
ansible.builtin.lineinfile:
path: "{{ portail_dossier_app }}/.env"
regexp: "^{{ item.cle }}="
line: "{{ item.cle }}={{ item.valeur }}"
loop:
- { cle: "OS_AUTH_URL", valeur: "http://10.10.0.1:5000" }
- { cle: "OS_NOVA_URL", valeur: "http://10.10.0.1:8774/v2.1" }
- { cle: "OS_HEAT_URL", valeur: "http://10.10.0.1:8004/v1" }
- { cle: "OS_USERNAME", valeur: "{{ portail_os_username }}" }
- { cle: "OS_PASSWORD", valeur: "{{ portail_os_password }}" }
- { cle: "OS_PROJECT_NAME", valeur: "{{ portail_os_project_name }}" }
- { cle: "OS_APPLICATION_CREDENTIAL_ID", valeur: "" }
- { cle: "OS_APPLICATION_CREDENTIAL_SECRET", valeur: "" }
- { cle: "REDIS_PREFIX", valeur: "" }
- { cle: "LABS_DOSSIER", valeur: "/opt/labs" }
# Banque de quiz officielle (quiz:synchroniser-banque) : contenu
# éditorial source (cours, TD, quiz), pas seulement les lab.yaml
# déployés — voir 08-banque-quiz.yml.
- { cle: "CONTENUS_DOSSIER", valeur: "/opt/paillasse-contenus" }
# > timeout des superviseurs Horizon (300 s, config/horizon.php)
- { cle: "REDIS_QUEUE_RETRY_AFTER", valeur: "330" }
# Étape 12 : HTTPS. Cookies de session envoyés en HTTPS seulement.
- { cle: "APP_URL", valeur: "https://{{ portail_domaine }}" }
- { cle: "SESSION_SECURE_COOKIE", valeur: "true" }
# Étape 14 (RGPD) : journaux applicatifs par jour, gardés 14 jours
# (comme ceux de Nginx), au lieu d'un fichier unique jamais purgé.
- { cle: "LOG_STACK", valeur: "daily" }
- { cle: "LOG_DAILY_DAYS", valeur: "14" }
# Bloc A, self-healing : métrique pour Prometheus (collecteur textfile).
- { cle: "LABS_SURVEILLANCE_METRIQUES", valeur: "/var/lib/node_exporter/textfile/sante_vm.prom" }
no_log: true
register: portail_env_mis_a_jour
# Horizon charge la configuration une seule fois au démarrage : sans
# redémarrage, il garderait l'ancien mot de passe après une rotation.
notify: redémarrer horizon
# Bloc B, stockage S3 (2026-10-14) : fichier créé à la main par Adam sur le
# serveur, jamais par Ansible ni par Claude (CLAUDE.md, commandes et
# dépenses). Tant qu'il n'existe pas, ce bloc ne touche à rien : le dépôt de
# fichier des devoirs reste simplement masqué côté portail
# (StockageTravauxEleves::disponible()) — voir docs/runbooks/stockage-s3.md.
- name: vérifier si les identifiants S3 existent (étape 10, facultatif)
ansible.builtin.stat:
path: /etc/syllabus/s3-paillasse.yaml
register: portail_s3_stat
- name: lire les identifiants S3 du portail (étape 10)
ansible.builtin.slurp:
src: /etc/syllabus/s3-paillasse.yaml
register: portail_s3_brut
when: portail_s3_stat.stat.exists
- name: extraire les champs S3
ansible.builtin.set_fact:
portail_s3_access_key: "{{ portail_s3_contenu | regex_search('access_key:\\s*\"?([^\"\\n]+)', '\\1') | first }}"
portail_s3_secret_key: "{{ portail_s3_contenu | regex_search('secret_key:\\s*\"?([^\"\\n]+)', '\\1') | first }}"
portail_s3_bucket: "{{ portail_s3_contenu | regex_search('bucket:\\s*\"?([^\"\\n]+)', '\\1') | first }}"
portail_s3_region: "{{ portail_s3_contenu | regex_search('region:\\s*\"?([^\"\\n]+)', '\\1') | first }}"
portail_s3_endpoint: "{{ portail_s3_contenu | regex_search('endpoint:\\s*\"?([^\"\\n]+)', '\\1') | first }}"
vars:
portail_s3_contenu: "{{ portail_s3_brut.content | b64decode }}"
when: portail_s3_stat.stat.exists
no_log: true
- name: ajouter/mettre à jour les variables S3 dans .env
ansible.builtin.lineinfile:
path: "{{ portail_dossier_app }}/.env"
regexp: "^{{ item.cle }}="
line: "{{ item.cle }}={{ item.valeur }}"
loop:
- { cle: "AWS_ACCESS_KEY_ID", valeur: "{{ portail_s3_access_key }}" }
- { cle: "AWS_SECRET_ACCESS_KEY", valeur: "{{ portail_s3_secret_key }}" }
- { cle: "AWS_BUCKET", valeur: "{{ portail_s3_bucket }}" }
- { cle: "AWS_DEFAULT_REGION", valeur: "{{ portail_s3_region }}" }
- { cle: "AWS_ENDPOINT", valeur: "{{ portail_s3_endpoint }}" }
- { cle: "AWS_USE_PATH_STYLE_ENDPOINT", valeur: "false" }
when: portail_s3_stat.stat.exists
no_log: true
notify: redémarrer horizon
- name: installer les dépendances JS (npm)
ansible.builtin.command:
cmd: npm ci
args:
chdir: "{{ portail_dossier_app }}"
register: portail_npm_ci
changed_when: true
- name: compiler les assets (vite build)
ansible.builtin.command:
cmd: npm run build
args:
chdir: "{{ portail_dossier_app }}"
register: portail_npm_build
changed_when: true
- name: appliquer les migrations
ansible.builtin.command:
cmd: php artisan migrate --force
args:
chdir: "{{ portail_dossier_app }}"
register: portail_migrate
changed_when: "'Nothing to migrate' not in portail_migrate.stdout"
- name: mettre en cache la configuration et les routes
ansible.builtin.shell:
cmd: php artisan config:cache && php artisan route:cache && php artisan view:cache
args:
chdir: "{{ portail_dossier_app }}"
changed_when: true