ajoute le self-healing des VM (sonde de santé, relance automatique, stock écarté) — bloc A, semaine 6

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Adam Belkacemi 2026-10-12 21:12:43 +02:00
parent b054888f5a
commit f2649d0f8b
21 changed files with 938 additions and 28 deletions

View file

@ -0,0 +1,171 @@
<?php
use App\Models\LabSession;
use App\Models\LabSessionMachine;
use App\Services\Labs\ClientCorrecteur;
use App\Services\Labs\StockChaud;
use App\Services\Labs\SurveillanceVm;
use App\Services\OpenStack\NovaClient;
use Illuminate\Support\Facades\Redis;
// Redis en mémoire : juste les commandes dont SurveillanceVm a besoin.
beforeEach(function () {
Redis::swap(new class
{
/** @var array<string, mixed> */
public array $d = [];
public function incr($k)
{
return $this->d[$k] = (int) ($this->d[$k] ?? 0) + 1;
}
public function get($k)
{
return $this->d[$k] ?? null;
}
public function del($k)
{
unset($this->d[$k]);
}
public function expire($k, $s) {}
public function hincrby($k, $c, $n)
{
$this->d[$k][$c] = ($this->d[$k][$c] ?? 0) + $n;
}
public function hgetall($k)
{
return $this->d[$k] ?? [];
}
});
});
/**
* Session d'élève active avec ses VM, créées il y a $ilYa secondes.
*
* @param array<string, string> $instances nom => uuid
* @return array<string, LabSessionMachine>
*/
function sessionActive(array $instances, int $ilYa = 600): array
{
$session = sessionEnAttente(LabSession::ACTIF);
$machines = [];
foreach ($instances as $nom => $uuid) {
$m = $session->machines()->create(['nom' => $nom, 'instance_id' => $uuid]);
$m->forceFill(['created_at' => now()->subSeconds($ilYa)])->save();
$machines[$nom] = $m;
}
return $machines;
}
/**
* @param array<string, array{repond: bool, etat: ?string}> $etats uuid => état
*/
function simuler(string $classe, Closure $attentes): void
{
app()->instance($classe, Mockery::mock($classe, $attentes));
}
function surveillance(array $etats, array $pretes = [], ?Closure $nova = null, ?Closure $stock = null): SurveillanceVm
{
simuler(ClientCorrecteur::class, fn ($c) => $c->shouldReceive('sante')->andReturnUsing(
fn (string $id) => ($etats[$id] ?? ['repond' => true, 'etat' => 'running']) + ['erreur' => null]
));
simuler(NovaClient::class, $nova ?? fn ($n) => $n->shouldNotReceive('redemarrerForce', 'demarrer'));
simuler(StockChaud::class, function ($s) use ($pretes, $stock) {
$s->shouldReceive('pretes')->andReturn($pretes);
$stock ? $stock($s) : $s->shouldNotReceive('retirer');
});
return app(SurveillanceVm::class);
}
$muette = ['repond' => false, 'etat' => 'running'];
test('une VM d\'élève muette 3 minutes de suite est redémarrée de force, une seule fois', function () use ($muette) {
$m = sessionActive(['pc-a' => 'uuid-a', 'pc-b' => 'uuid-b', 'routeur' => 'uuid-r']);
$s = surveillance(['uuid-a' => $muette], nova: fn ($n) => $n->shouldReceive('redemarrerForce')->once()->with('uuid-a'));
expect($s->surveiller()['relancees'])->toBe(0)
->and($s->surveiller()['relancees'])->toBe(0)
->and($s->surveiller()['relancees'])->toBe(1);
expect($m['pc-a']->refresh()->sante)->toBe(LabSessionMachine::RELANCEE)
->and($m['pc-b']->refresh()->sante)->toBeNull();
});
test('une VM éteinte par l\'élève est redémarrée, pas réinitialisée', function () {
sessionActive(['pc-a' => 'uuid-a', 'pc-b' => 'uuid-b', 'routeur' => 'uuid-r']);
$s = surveillance(['uuid-a' => ['repond' => false, 'etat' => 'shut off']], nova: fn ($n) => $n->shouldReceive('demarrer')->once()->with('uuid-a'));
foreach (range(1, 3) as $_) {
$s->surveiller();
}
});
test('après deux relances dans l\'heure, la VM est déclarée irréparable et le prof le voit', function () use ($muette) {
$m = sessionActive(['pc-a' => 'uuid-a', 'pc-b' => 'uuid-b', 'routeur' => 'uuid-r']);
$s = surveillance(['uuid-a' => $muette], nova: fn ($n) => $n->shouldReceive('redemarrerForce')->twice());
foreach (range(1, 9) as $_) {
$r = $s->surveiller();
}
expect($r['irreparables'])->toBe(1)
->and($m['pc-a']->refresh()->sante)->toBe(LabSessionMachine::IRREPARABLE)
->and(Redis::hgetall('sante:compteurs'))->toBe(['relances_session' => 2, 'abandons' => 1]);
$session = $m['pc-a']->labSession;
$prof = $session->seance->classe->prof;
$this->withSession(['jeton_session_web' => $prof->demarreNouvelleSession()])
->actingAs($prof, 'web')
->get(route('seances.attente', $session->seance))
->assertSee('pc-a ne répond plus');
});
test('toutes les VM muettes à la fois : panne de l\'hôte, aucune relance', function () use ($muette) {
sessionActive(['pc-a' => 'uuid-a', 'pc-b' => 'uuid-b', 'routeur' => 'uuid-r']);
$s = surveillance(['uuid-a' => $muette, 'uuid-b' => $muette, 'uuid-r' => $muette]);
foreach (range(1, 4) as $_) {
$r = $s->surveiller();
}
expect($r['panne_generale'])->toBeTrue()->and($r['relancees'])->toBe(0);
});
test('une VM qui vient d\'être créée n\'est pas sondée (elle démarre)', function () use ($muette) {
sessionActive(['pc-a' => 'uuid-a'], ilYa: 60);
$s = surveillance(['uuid-a' => $muette]);
foreach (range(1, 4) as $_) {
$r = $s->surveiller();
}
expect($r['sondees'])->toBe(0);
});
test('une stack du stock dont une VM est muette 2 minutes est retirée du stock', function () use ($muette) {
$pretes = [
['lab' => '08-deux-lan-routeur', 'nom' => 'stock-a-e1', 'id' => 'h1', 'instances' => ['pc-a' => 's1', 'routeur' => 's2', 'pc-b' => 's3'], 'cree_a' => time() - 600, 'brut' => 'A'],
['lab' => '08-deux-lan-routeur', 'nom' => 'stock-b-e1', 'id' => 'h2', 'instances' => ['pc-a' => 's4', 'routeur' => 's5', 'pc-b' => 's6'], 'cree_a' => time() - 600, 'brut' => 'B'],
];
$s = surveillance(['s2' => $muette], $pretes, stock: fn ($st) => $st->shouldReceive('retirer')->once()->with('08-deux-lan-routeur', 'A')->andReturn(true));
expect($s->surveiller()['retirees_stock'])->toBe(0)
->and($s->surveiller()['retirees_stock'])->toBe(1);
});
test('correcteur injoignable : aucune action', function () {
sessionActive(['pc-a' => 'uuid-a']);
$this->mock(ClientCorrecteur::class, fn ($c) => $c->shouldReceive('sante')->andThrow(new RuntimeException('Correcteur injoignable')));
$this->mock(NovaClient::class, fn ($n) => $n->shouldNotReceive('redemarrerForce', 'demarrer'));
$this->mock(StockChaud::class, fn ($s) => $s->shouldReceive('pretes')->andReturn([]));
expect(app(SurveillanceVm::class)->surveiller()['correcteur_injoignable'])->toBeTrue();
});