diff --git a/ansible/openstack-projet.yml b/ansible/openstack-projet.yml new file mode 100644 index 0000000..b46d932 --- /dev/null +++ b/ansible/openstack-projet.yml @@ -0,0 +1,8 @@ +--- +# Étape 13 : gabarits et quotas du projet des labs. +- name: Projet OpenStack des labs + hosts: syllabus + become: true + gather_facts: true + roles: + - openstack-projet diff --git a/ansible/roles/kolla/tasks/main.yml b/ansible/roles/kolla/tasks/main.yml index 7aa2349..aeca8e2 100644 --- a/ansible/roles/kolla/tasks/main.yml +++ b/ansible/roles/kolla/tasks/main.yml @@ -111,6 +111,31 @@ group: root mode: "0600" +- name: créer le dossier des surcharges nova + ansible.builtin.file: + path: "{{ kolla_config_dir }}/config/nova" + state: directory + owner: root + group: root + mode: "0750" + +- name: déployer la surcharge de nova-compute (versionnée, sans secret) + ansible.builtin.copy: + src: "{{ playbook_dir }}/../kolla/config/nova/nova-compute.conf" + dest: "{{ kolla_config_dir }}/config/nova/nova-compute.conf" + owner: root + group: root + mode: "0640" + register: kolla_surcharge_nova + +- name: rappeler de reconfigurer nova si la surcharge a changé + ansible.builtin.debug: + msg: > + Surcharge nova-compute modifiée : lancer + `kolla-ansible reconfigure -i {{ kolla_config_dir }}/all-in-one --tags nova` + sur le serveur (voir docs/runbooks/openstack.md). + when: kolla_surcharge_nova.changed + - name: installer les dépendances Ansible de Kolla (collections Galaxy) ansible.builtin.command: cmd: "{{ kolla_venv }}/bin/kolla-ansible install-deps" diff --git a/ansible/roles/openstack-projet/defaults/main.yml b/ansible/roles/openstack-projet/defaults/main.yml new file mode 100644 index 0000000..01d01a8 --- /dev/null +++ b/ansible/roles/openstack-projet/defaults/main.yml @@ -0,0 +1,12 @@ +--- +# Projet des labs (créé à l'étape 6) : ce rôle garantit son gabarit et ses +# quotas. Le quota est un filet de sécurité ; la vraie limite de capacité est +# la réserve mémoire de Nova (kolla/config/nova/nova-compute.conf), voir +# docs/capacite.md. +openstack_projet: paillasse +openstack_projet_quota: + instances: 40 + cores: 40 + ram: 30720 +openstack_gabarits: + - { nom: eleve.petit, ram: 768, vcpus: 1, disque: 5 } diff --git a/ansible/roles/openstack-projet/tasks/main.yml b/ansible/roles/openstack-projet/tasks/main.yml new file mode 100644 index 0000000..a4d583d --- /dev/null +++ b/ansible/roles/openstack-projet/tasks/main.yml @@ -0,0 +1,39 @@ +--- +# Client openstack de Kolla, compte admin (clouds.yaml généré par post-deploy). +- name: lire les gabarits existants + ansible.builtin.command: openstack flavor list -f json + environment: &os_admin + PATH: "/opt/kolla-venv/bin:{{ ansible_env.PATH }}" + OS_CLIENT_CONFIG_FILE: /etc/kolla/clouds.yaml + OS_CLOUD: kolla-admin + register: openstack_gabarits_existants + changed_when: false + +- name: créer les gabarits manquants + ansible.builtin.command: >- + openstack flavor create --public --ram {{ item.ram }} --vcpus {{ item.vcpus }} + --disk {{ item.disque }} {{ item.nom }} + environment: *os_admin + loop: "{{ openstack_gabarits }}" + when: item.nom not in (openstack_gabarits_existants.stdout | from_json | map(attribute='Name') | list) + +- name: lire le quota de calcul du projet + ansible.builtin.command: openstack quota show --compute {{ openstack_projet }} -f json + environment: *os_admin + register: openstack_quota_brut + changed_when: false + +- name: régler le quota de calcul du projet + ansible.builtin.command: >- + openstack quota set --instances {{ openstack_projet_quota.instances }} + --cores {{ openstack_projet_quota.cores }} --ram {{ openstack_projet_quota.ram }} + {{ openstack_projet }} + environment: *os_admin + vars: + openstack_quota_actuel: >- + {{ dict(openstack_quota_brut.stdout | from_json | map(attribute='Resource') | + zip(openstack_quota_brut.stdout | from_json | map(attribute='Limit'))) }} + when: >- + openstack_quota_actuel.instances != openstack_projet_quota.instances or + openstack_quota_actuel.cores != openstack_projet_quota.cores or + openstack_quota_actuel.ram != openstack_projet_quota.ram diff --git a/ansible/roles/sauvegardes/defaults/main.yml b/ansible/roles/sauvegardes/defaults/main.yml index 793d81c..16f03dd 100644 --- a/ansible/roles/sauvegardes/defaults/main.yml +++ b/ansible/roles/sauvegardes/defaults/main.yml @@ -11,3 +11,14 @@ sauvegardes_repertoires: - /root sauvegardes_age_dir: /root/.age sauvegardes_age_cle: /root/.age/cle-sauvegardes.txt +# Portail (ajouté à l'étape 13) : base exportée par mariadb-dump, et fichiers +# d'état hors de /etc (APP_KEY dans .env, futurs travaux d'élèves). +sauvegardes_portail_conteneur: portail_mariadb +sauvegardes_portail_base: portail +sauvegardes_portail_fichiers: + - /var/www/portail/.env + - /var/www/portail/storage/app +sauvegardes_image_mariadb: "mariadb:11" +sauvegardes_php_version: "8.3" +# Test de restauration hebdomadaire (CLAUDE.md : restauration testée chaque semaine). +sauvegardes_test_restauration_quand: "Mon *-*-* 04:30:00" diff --git a/ansible/roles/sauvegardes/tasks/main.yml b/ansible/roles/sauvegardes/tasks/main.yml index 939d062..c8ee01c 100644 --- a/ansible/roles/sauvegardes/tasks/main.yml +++ b/ansible/roles/sauvegardes/tasks/main.yml @@ -56,6 +56,42 @@ mode: "0644" notify: recharger systemd +- name: déployer le test de restauration du portail (étape 13) + ansible.builtin.template: + src: syllabus-test-restauration.sh.j2 + dest: /usr/local/sbin/syllabus-test-restauration + owner: root + group: root + mode: "0700" + +- name: déployer le service et le minuteur du test de restauration + ansible.builtin.copy: + dest: "/etc/systemd/system/syllabus-test-restauration.{{ item.type }}" + mode: "0644" + content: "{{ item.contenu }}" + loop: + - type: service + contenu: | + # Géré par Ansible (rôle sauvegardes). + [Unit] + Description=Test de restauration de la dernière sauvegarde du portail + After=docker.service network-online.target + Wants=network-online.target + [Service] + Type=oneshot + ExecStart=/usr/local/sbin/syllabus-test-restauration + - type: timer + contenu: | + # Géré par Ansible (rôle sauvegardes). + [Unit] + Description=Test de restauration hebdomadaire du portail + [Timer] + OnCalendar={{ sauvegardes_test_restauration_quand }} + Persistent=true + [Install] + WantedBy=timers.target + notify: recharger systemd + - name: appliquer les changements systemd ansible.builtin.meta: flush_handlers @@ -65,6 +101,12 @@ enabled: true state: started +- name: activer le minuteur du test de restauration + ansible.builtin.systemd: + name: syllabus-test-restauration.timer + enabled: true + state: started + - name: afficher la clé publique age (à connaître, jamais la privée) ansible.builtin.debug: msg: "Clé publique age du serveur : {{ sauvegardes_age_pub.stdout }}" diff --git a/ansible/roles/sauvegardes/templates/sauvegarde-dedibackup.sh.j2 b/ansible/roles/sauvegardes/templates/sauvegarde-dedibackup.sh.j2 index 632038b..c4da74f 100644 --- a/ansible/roles/sauvegardes/templates/sauvegarde-dedibackup.sh.j2 +++ b/ansible/roles/sauvegardes/templates/sauvegarde-dedibackup.sh.j2 @@ -25,8 +25,25 @@ trap 'rm -rf "$WORKDIR"' EXIT ARCHIVE_QUOT="quotidienne-${DATE}.tar.zst.age" +# Portail (étape 13) : export cohérent de sa base (--single-transaction, sans +# bloquer le site) et ses fichiers d'état. Le mot de passe root est lu dans +# le conteneur : il n'apparaît jamais sur la ligne de commande de l'hôte. +# Absent sur un serveur neuf (la sauvegarde arrive avant le portail) : ignoré. +EN_PLUS=() +if [ "$(docker inspect -f '{% raw %}{{.State.Running}}{% endraw %}' {{ sauvegardes_portail_conteneur }} 2>/dev/null)" = "true" ]; then + docker exec {{ sauvegardes_portail_conteneur }} sh -c \ + 'MYSQL_PWD="$MARIADB_ROOT_PASSWORD" exec mariadb-dump -uroot --single-transaction --routines --triggers --databases {{ sauvegardes_portail_base }}' \ + > "$WORKDIR/portail.sql" + EN_PLUS+=(-C "$WORKDIR" portail.sql) +fi +for chemin in {{ sauvegardes_portail_fichiers | join(' ') }}; do + if [ -e "$chemin" ]; then + EN_PLUS+=("$chemin") + fi +done + tar --exclude="{{ sauvegardes_age_dir }}" --exclude='/root/.dedibackup' \ - -I zstd -cf - {{ sauvegardes_repertoires | join(' ') }} \ + -I zstd -cf - {{ sauvegardes_repertoires | join(' ') }} "${EN_PLUS[@]}" \ | age -r "$AGE_PUB" -o "$WORKDIR/$ARCHIVE_QUOT" lftp -u "$DEDIBACKUP_USER,$DEDIBACKUP_PASS" "$DEDIBACKUP_HOST" -e " diff --git a/ansible/roles/sauvegardes/templates/syllabus-test-restauration.sh.j2 b/ansible/roles/sauvegardes/templates/syllabus-test-restauration.sh.j2 new file mode 100644 index 0000000..0ccf010 --- /dev/null +++ b/ansible/roles/sauvegardes/templates/syllabus-test-restauration.sh.j2 @@ -0,0 +1,101 @@ +#!/bin/bash +# Géré par Ansible (rôle sauvegardes). Test de restauration du portail +# (étape 13, puis chaque semaine) : +# 1. télécharge la dernière sauvegarde quotidienne de Dedibackup ; +# 2. la déchiffre (clé age du serveur) et en extrait la base et le .env ; +# 3. restaure la base dans un conteneur MariaDB jetable, isolé ; +# 4. fait tourner le code du portail contre cette copie (migrations, +# comptages), avec l'APP_KEY restaurée ; +# 5. compare les tables à la production et publie une métrique Prometheus. +# Ne modifie jamais la production. N'affiche aucun secret. +set -euo pipefail + +set +x +source /root/.dedibackup + +CLE="{{ sauvegardes_age_cle }}" +APP="/var/www/portail" +CONTENEUR="syllabus-restauration" +DOSSIER=$(mktemp -d /var/tmp/restauration-XXXXXX) +nettoyer() { + docker rm -f "$CONTENEUR" >/dev/null 2>&1 || true + rm -rf "$DOSSIER" +} +trap nettoyer EXIT + +echo "== 1. Dernière sauvegarde quotidienne" +DERNIERE=$(lftp -u "$DEDIBACKUP_USER,$DEDIBACKUP_PASS" "$DEDIBACKUP_HOST" \ + -e "set ftp:ssl-allow no; cls -1 quotidienne-*.tar.zst.age; quit" | sort | tail -1) +[ -n "$DERNIERE" ] || { echo "aucune sauvegarde trouvée"; exit 1; } +lftp -u "$DEDIBACKUP_USER,$DEDIBACKUP_PASS" "$DEDIBACKUP_HOST" \ + -e "set ftp:ssl-allow no; get $DERNIERE -o $DOSSIER/archive.age; quit" +echo "$DERNIERE ($(du -h "$DOSSIER/archive.age" | cut -f1))" + +echo "== 2. Déchiffrement et extraction" +mkdir "$DOSSIER/extrait" +age -d -i "$CLE" "$DOSSIER/archive.age" \ + | tar -I zstd -xf - -C "$DOSSIER/extrait" portail.sql var/www/portail/.env +[ -s "$DOSSIER/extrait/portail.sql" ] || { echo "portail.sql absent de l'archive"; exit 1; } +APP_KEY_RESTAUREE=$(grep -oP '^APP_KEY=\K.*' "$DOSSIER/extrait/var/www/portail/.env") +if [ "$APP_KEY_RESTAUREE" = "$(grep -oP '^APP_KEY=\K.*' "$APP/.env")" ]; then + echo "APP_KEY restaurée identique à la production (valeur non affichée)" +else + echo "APP_KEY restaurée DIFFÉRENTE de la production"; exit 1 +fi + +echo "== 3. Restauration dans un conteneur jetable" +MDP=$(openssl rand -hex 16) +# Réseau du portail (Docker tourne sans pont par défaut), aucun port publié : +# seul l'hôte joint ce conteneur, par son adresse interne. +docker run -d --name "$CONTENEUR" --network portail_default \ + -e MARIADB_ROOT_PASSWORD="$MDP" {{ sauvegardes_image_mariadb }} >/dev/null +for _ in $(seq 1 60); do + docker exec "$CONTENEUR" sh -c 'MYSQL_PWD="$MARIADB_ROOT_PASSWORD" mariadb -uroot -e "SELECT 1"' >/dev/null 2>&1 && break + sleep 1 +done +docker exec -i "$CONTENEUR" sh -c 'MYSQL_PWD="$MARIADB_ROOT_PASSWORD" exec mariadb -uroot' < "$DOSSIER/extrait/portail.sql" +IP=$(docker inspect -f '{% raw %}{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}{% endraw %}' "$CONTENEUR") +echo "base restaurée dans $CONTENEUR" + +echo "== 4. Le portail tourne contre la copie" +# APP_CONFIG_CACHE vers un fichier inexistant : Laravel ignore la config mise +# en cache (qui pointe vers la production) et lit ces variables en priorité. +artisan_copie() { + env APP_CONFIG_CACHE="$DOSSIER/config-absente.php" APP_KEY="$APP_KEY_RESTAUREE" \ + DB_HOST="$IP" DB_PORT=3306 DB_USERNAME=root DB_PASSWORD="$MDP" DB_DATABASE={{ sauvegardes_portail_base }} \ + CACHE_STORE=array SESSION_DRIVER=array QUEUE_CONNECTION=sync LOG_CHANNEL=stderr \ + php{{ sauvegardes_php_version }} "$APP/artisan" "$@" +} +# Échec de connexion = échec du test (set -e), pas un « 0 en attente » trompeur. +STATUT=$(artisan_copie migrate:status) +APPLIQUEES=$(grep -c " Ran" <<<"$STATUT" || true) +EN_ATTENTE=$(grep -c "Pending" <<<"$STATUT" || true) +echo "migrations dans la copie : $APPLIQUEES appliquées, $EN_ATTENTE en attente" +[ "$APPLIQUEES" -gt 0 ] && [ "$EN_ATTENTE" = "0" ] || exit 1 + +echo "== 5. Comparaison avec la production (lignes par table)" +compter() { + docker exec "$1" sh -c 'export MYSQL_PWD="$MARIADB_ROOT_PASSWORD" + for t in $(mariadb -uroot -N -e "SHOW TABLES" {{ sauvegardes_portail_base }}); do + echo "$t $(mariadb -uroot -N -e "SELECT COUNT(*) FROM \`$t\`" {{ sauvegardes_portail_base }})" + done' +} +compter "$CONTENEUR" | sort > "$DOSSIER/copie.txt" +compter {{ sauvegardes_portail_conteneur }} | sort > "$DOSSIER/production.txt" +printf "%-28s %10s %10s\n" table copie production +join -a1 -a2 -e "absente" -o 0,1.2,2.2 "$DOSSIER/copie.txt" "$DOSSIER/production.txt" | while read -r t c p; do + printf "%-28s %10s %10s%s\n" "$t" "$c" "$p" "$([ "$c" = "$p" ] || echo ' ≠ (écart depuis la sauvegarde ?)')" +done +TABLES_MANQUANTES=$(join -v2 "$DOSSIER/copie.txt" "$DOSSIER/production.txt" | wc -l) +[ "$TABLES_MANQUANTES" = "0" ] || { echo "$TABLES_MANQUANTES table(s) de production absente(s) de la copie"; exit 1; } +echo "lab_sessions vues par le portail dans la copie : $(artisan_copie tinker --execute='echo App\Models\LabSession::count();' 2>/dev/null | tail -1)" + +DOSSIER_TEXTFILE="{{ sauvegardes_dossier_textfile | default('/var/lib/node_exporter/textfile') }}" +cat > "$DOSSIER_TEXTFILE/restauration.prom.tmp" < 8 * 86400 + for: 1h + labels: + severite: avertissement + annotations: + summary: "Aucun test de restauration du portail reussi depuis plus de 8 jours" + description: "Voir journalctl -u syllabus-test-restauration et docs/runbooks/sauvegardes.md." + - alert: SauvegardeAncienne expr: (time() - syllabus_derniere_sauvegarde_reussie_timestamp) > 26 * 3600 for: 10m diff --git a/openstack/tests/capacite.py b/openstack/tests/capacite.py new file mode 100644 index 0000000..0642579 --- /dev/null +++ b/openstack/tests/capacite.py @@ -0,0 +1,186 @@ +#!/usr/bin/env python3 +"""Test de capacité (étape 13) : N VM eleve.petit lancées en même temps. + +À lancer en root sur le serveur (client openstack de Kolla, virsh dans le +conteneur nova_libvirt) : + + sudo python3 capacite.py --vm 20 --sortie /root/capacite-AAAA-MM-JJ + +Phases : démarrage simultané, repos, charge CPU, charge mémoire (arrêtée +d'elle-même si la mémoire disponible de l'hôte passe sous --plancher-go). +Crée un réseau « capacite-test » et des VM « capacite-N » dans le projet +paillasse ; ne supprime rien (nettoyage séparé, après accord). +""" +import argparse +import base64 +import csv +import json +import os +import subprocess +import threading +import time + +ENV = dict(os.environ, PATH="/opt/kolla-venv/bin:" + os.environ["PATH"], + OS_CLIENT_CONFIG_FILE="/etc/kolla/clouds-paillasse.yaml", OS_CLOUD="paillasse-service") + + +def openstack(*args): + return subprocess.run(["openstack", *args], env=ENV, check=True, capture_output=True, text=True).stdout + + +def agent(uuid, commande, delai=5): + r = subprocess.run(["docker", "exec", "nova_libvirt", "virsh", "qemu-agent-command", uuid, + json.dumps(commande), "--timeout", str(delai)], capture_output=True, text=True) + return json.loads(r.stdout) if r.returncode == 0 else None + + +def executer(uuid, script, attendre=True, delai=30): + r = agent(uuid, {"execute": "guest-exec", "arguments": {"path": "/bin/sh", "arg": ["-c", script], "capture-output": True}}) + if not r or not attendre: + return r is not None, "" + fin = time.time() + delai + while time.time() < fin: + s = agent(uuid, {"execute": "guest-exec-status", "arguments": {"pid": r["return"]["pid"]}}) + if s and s["return"].get("exited"): + return s["return"].get("exitcode") == 0, base64.b64decode(s["return"].get("out-data", "")).decode() + time.sleep(0.3) + return False, "délai" + + +def meminfo(): + m = {} + for ligne in open("/proc/meminfo"): + cle, val = ligne.split(":") + m[cle] = int(val.split()[0]) // 1024 + return m + + +def cpu_brut(): + v = list(map(int, open("/proc/stat").readline().split()[1:])) + return sum(v), v[3] + v[4] + + +def rss_qemu(): + r = subprocess.run(["ps", "-C", "qemu-system-x86_64,qemu-kvm", "-o", "rss=,args="], capture_output=True, text=True) + total, par_vm = 0, {} + for ligne in r.stdout.splitlines(): + rss, args = ligne.strip().split(None, 1) + mo = int(rss) // 1024 + total += mo + for mot in args.split(): + if mot.startswith("uuid="): + par_vm[mot[5:]] = mo + if "-uuid" in args: + par_vm[args.split("-uuid")[1].split()[0]] = mo + return total, par_vm + + +class Echantillonneur(threading.Thread): + """Relève l'hôte toutes les 2 s : mémoire, CPU, charge, RSS des VM.""" + + def __init__(self, chemin): + super().__init__(daemon=True) + self.chemin, self.phase, self.arret = chemin, "base", threading.Event() + + def run(self): + with open(self.chemin, "w", newline="") as f: + w = csv.writer(f) + w.writerow(["t", "phase", "mem_disponible_mo", "mem_utilisee_mo", "cpu_pct", "charge_1min", "rss_qemu_total_mo"]) + t0, (tot0, idle0) = time.time(), cpu_brut() + while not self.arret.is_set(): + time.sleep(2) + tot, idle = cpu_brut() + cpu = 100 * (1 - (idle - idle0) / max(1, tot - tot0)) + tot0, idle0 = tot, idle + m = meminfo() + w.writerow([round(time.time() - t0, 1), self.phase, m["MemAvailable"], m["MemTotal"] - m["MemAvailable"], + round(cpu, 1), open("/proc/loadavg").read().split()[0], rss_qemu()[0]]) + f.flush() + + +def main(): + p = argparse.ArgumentParser() + p.add_argument("--vm", type=int, default=20) + p.add_argument("--sortie", required=True) + p.add_argument("--plancher-go", type=float, default=3.0) + a = p.parse_args() + os.makedirs(a.sortie, exist_ok=True) + resume = {"vm": a.vm, "base": meminfo()} + + ech = Echantillonneur(os.path.join(a.sortie, "hote.csv")) + ech.start() + time.sleep(10) + + if "capacite-test" not in openstack("network", "list", "-f", "value", "-c", "Name").split(): + openstack("network", "create", "--disable-port-security", "capacite-test") + openstack("subnet", "create", "--network", "capacite-test", "--subnet-range", "10.250.0.0/24", "--no-dhcp", "capacite-test") + + # 1. Démarrage simultané + ech.phase = "demarrage" + t0 = time.time() + openstack("server", "create", "--image", "paillasse-debian", "--flavor", "eleve.petit", "--network", "capacite-test", + "--min", str(a.vm), "--max", str(a.vm), "capacite") + vms, actives, pretes = {}, {}, {} + while time.time() - t0 < 600 and len(pretes) < a.vm: + for s in json.loads(openstack("server", "list", "--name", "^capacite-", "-f", "json", "-c", "ID", "-c", "Name", "-c", "Status")): + vms[s["ID"]] = s["Name"] + if s["Status"] == "ACTIVE" and s["ID"] not in actives: + actives[s["ID"]] = round(time.time() - t0, 1) + if s["Status"] == "ERROR": + actives.setdefault(s["ID"], "ERROR") + for uuid in list(actives): + if uuid not in pretes and actives[uuid] != "ERROR" and agent(uuid, {"execute": "guest-ping"}, 2): + pretes[uuid] = round(time.time() - t0, 1) + time.sleep(1) + with open(os.path.join(a.sortie, "demarrage.csv"), "w", newline="") as f: + w = csv.writer(f) + w.writerow(["vm", "uuid", "active_s", "agent_pret_s"]) + for uuid, nom in sorted(vms.items(), key=lambda x: x[1]): + w.writerow([nom, uuid, actives.get(uuid, ""), pretes.get(uuid, "")]) + resume["demarrage"] = {"pretes": len(pretes), "erreurs": sum(1 for v in actives.values() if v == "ERROR"), + "premiere_s": min(pretes.values(), default=None), "derniere_s": max(pretes.values(), default=None)} + + # 2. Repos + ech.phase = "repos" + time.sleep(120) + total, par_vm = rss_qemu() + nos = {u: par_vm.get(u) for u in pretes} + resume["repos"] = {"mem": meminfo(), "rss_par_vm_mo": nos, "rss_moyen_mo": round(sum(v for v in nos.values() if v) / max(1, len(nos)))} + + # 3. Charge CPU : un cœur à 100 % dans chaque VM pendant 60 s + ech.phase = "cpu" + for uuid in pretes: + executer(uuid, "timeout 60 sh -c 'while :; do :; done'", attendre=False) + time.sleep(20) + latences = [] + for uuid in list(pretes)[:5]: + t = time.time() + executer(uuid, "true") + latences.append(round(time.time() - t, 2)) + resume["cpu"] = {"latence_commande_s": latences, "charge_1min": open("/proc/loadavg").read().split()[0]} + time.sleep(45) + + # 4. Charge mémoire : 350 Mo touchés dans chaque VM, une à une + ech.phase = "memoire" + paliers = [] + for i, uuid in enumerate(pretes, 1): + dispo = meminfo()["MemAvailable"] + if dispo < a.plancher_go * 1024: + resume["memoire_arret"] = f"plancher atteint avant la VM {i} ({dispo} Mo disponibles)" + break + ok, _ = executer(uuid, "head -c 350M /dev/zero > /dev/shm/charge && sync", delai=60) + time.sleep(3) + paliers.append({"vm": i, "ok": ok, "mem_disponible_mo": meminfo()["MemAvailable"], "rss_vm_mo": rss_qemu()[1].get(uuid)}) + resume["memoire"] = paliers + if len(paliers) >= 2: + resume["memoire_cout_par_vm_mo"] = round((paliers[0]["mem_disponible_mo"] - paliers[-1]["mem_disponible_mo"]) / (len(paliers) - 1)) + + ech.phase = "fin" + time.sleep(6) + ech.arret.set() + json.dump(resume, open(os.path.join(a.sortie, "resume.json"), "w"), indent=2, ensure_ascii=False) + print(json.dumps({k: v for k, v in resume.items() if k not in ("memoire",)}, indent=1, ensure_ascii=False)[:3000]) + + +if __name__ == "__main__": + main()