187 lines
7.7 KiB
Python
187 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()
|