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>
186 lines
7.7 KiB
Python
186 lines
7.7 KiB
Python
#!/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()
|