sauvegarde la base du portail et teste sa restauration chaque semaine

La sauvegarde ne couvrait que /etc, /home et /root : la base MariaDB du
portail et son .env (APP_KEY) en étaient absents. Ajoute aussi le réglage
du quota et du gabarit du projet des labs dans Ansible, la reprise des VM
au démarrage de l'hôte (Nova), et le script du test de capacité.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Adam Belkacemi 2026-10-09 17:28:50 +02:00
parent 424d0f6f79
commit 3f0b565e1e
11 changed files with 458 additions and 1 deletions

View file

@ -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

View file

@ -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"

View file

@ -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 }

View file

@ -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

View file

@ -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"

View file

@ -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 }}"

View file

@ -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 "

View file

@ -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" <<METRIQUE
# HELP syllabus_derniere_restauration_reussie_timestamp Horodatage Unix du dernier test de restauration du portail réussi.
# TYPE syllabus_derniere_restauration_reussie_timestamp gauge
syllabus_derniere_restauration_reussie_timestamp $(date +%s)
METRIQUE
mv "$DOSSIER_TEXTFILE/restauration.prom.tmp" "$DOSSIER_TEXTFILE/restauration.prom"
logger -t syllabus-test-restauration "Restauration de $DERNIERE réussie"
echo "== RESTAURATION RÉUSSIE ($DERNIERE)"

View file

@ -0,0 +1,7 @@
# Surcharge Kolla pour nova-compute (étape 13), fusionnée dans nova.conf par
# `kolla-ansible reconfigure --tags nova`. Valeurs par défaut de Nova lues dans
# son code (nova/conf/compute.py) avant de les changer.
[DEFAULT]
# Défaut : false. Après un redémarrage de l'hôte, les VM restaient éteintes :
# le stock chaud aurait contenu des stacks « prêtes » aux VM arrêtées.
resume_guests_state_on_host_boot = true

View file

@ -46,6 +46,15 @@ groups:
summary: "L'expiration du certificat HTTPS n'est plus mesuree"
description: "Le fichier certificat.prom du collecteur textfile n'est plus produit : verifier le cron syllabus-expiration-certificat."
- alert: RestaurationNonTestee
expr: (time() - syllabus_derniere_restauration_reussie_timestamp) > 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

186
openstack/tests/capacite.py Normal file
View file

@ -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()