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:
parent
424d0f6f79
commit
3f0b565e1e
11 changed files with 458 additions and 1 deletions
8
ansible/openstack-projet.yml
Normal file
8
ansible/openstack-projet.yml
Normal 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
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
12
ansible/roles/openstack-projet/defaults/main.yml
Normal file
12
ansible/roles/openstack-projet/defaults/main.yml
Normal 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 }
|
||||
39
ansible/roles/openstack-projet/tasks/main.yml
Normal file
39
ansible/roles/openstack-projet/tasks/main.yml
Normal 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
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 }}"
|
||||
|
|
|
|||
|
|
@ -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 "
|
||||
|
|
|
|||
|
|
@ -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)"
|
||||
7
kolla/config/nova/nova-compute.conf
Normal file
7
kolla/config/nova/nova-compute.conf
Normal 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
|
||||
|
|
@ -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
186
openstack/tests/capacite.py
Normal 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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue