ajoute le proxy console Go et le relais WebSocket vers nova-serialproxy
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
30e14b1c54
commit
8742bde807
30 changed files with 1103 additions and 6 deletions
44
portail/app/Http/Controllers/ConsoleController.php
Normal file
44
portail/app/Http/Controllers/ConsoleController.php
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\LabSession;
|
||||
use App\Models\Seance;
|
||||
use App\Services\OpenStack\JetonConsole;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
/**
|
||||
* Émet des jetons de console série à usage court (étape 9) : le navigateur
|
||||
* ne reçoit jamais l'adresse de nova-serialproxy, seulement un jeton Redis
|
||||
* valable 5 minutes. L'élève peut écrire dans sa propre VM ; le prof ne
|
||||
* peut que regarder celle d'un élève de sa classe.
|
||||
*/
|
||||
class ConsoleController extends Controller
|
||||
{
|
||||
public function __construct(private readonly JetonConsole $jetons) {}
|
||||
|
||||
public function pourEleve(LabSession $labSession): JsonResponse
|
||||
{
|
||||
$eleve = Auth::guard('eleve')->user();
|
||||
abort_if($labSession->eleve_id !== $eleve->id, 403);
|
||||
abort_if($labSession->etat !== LabSession::ADMIS, 403, 'En attente d\'admission.');
|
||||
abort_if($labSession->stack_id === null, 404, 'Aucune VM associée à cette session.');
|
||||
|
||||
return response()->json([
|
||||
'jeton' => $this->jetons->pourEcriture($labSession->stack_id),
|
||||
]);
|
||||
}
|
||||
|
||||
public function pourProf(Seance $seance, LabSession $labSession): JsonResponse
|
||||
{
|
||||
abort_if($seance->classe->user_id !== Auth::guard('web')->id(), 403);
|
||||
abort_if($labSession->seance_id !== $seance->id, 404);
|
||||
abort_if($labSession->etat !== LabSession::ADMIS, 403, 'Élève pas encore admis.');
|
||||
abort_if($labSession->stack_id === null, 404, 'Aucune VM associée à cette session.');
|
||||
|
||||
return response()->json([
|
||||
'jeton' => $this->jetons->pourLecture($labSession->stack_id),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -38,6 +38,17 @@ class SeanceController extends Controller
|
|||
]);
|
||||
}
|
||||
|
||||
public function consoleProf(Seance $seance, LabSession $labSession): View
|
||||
{
|
||||
abort_if($seance->classe->user_id !== Auth::guard('web')->id(), 403);
|
||||
abort_if($labSession->seance_id !== $seance->id, 404);
|
||||
|
||||
return view('seances.console-prof', [
|
||||
'seance' => $seance->load('lab'),
|
||||
'labSession' => $labSession->load('eleve'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function admettre(Seance $seance, LabSession $labSession): RedirectResponse
|
||||
{
|
||||
abort_if($seance->classe->user_id !== Auth::guard('web')->id(), 403);
|
||||
|
|
|
|||
47
portail/app/Services/OpenStack/JetonConsole.php
Normal file
47
portail/app/Services/OpenStack/JetonConsole.php
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\OpenStack;
|
||||
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* Range l'adresse de la console série dans Redis sous un jeton aléatoire,
|
||||
* pour 5 minutes (étape 9). Le navigateur ne reçoit que ce jeton, jamais
|
||||
* l'URL ni le jeton Nova sous-jacent. Format lu par proxy-console/main.go :
|
||||
* ne pas changer les noms de champs sans mettre à jour les deux côtés.
|
||||
*/
|
||||
class JetonConsole
|
||||
{
|
||||
private const DUREE_MINUTES = 5;
|
||||
|
||||
public function __construct(private readonly NovaClient $nova) {}
|
||||
|
||||
public function pourEcriture(string $instanceId): string
|
||||
{
|
||||
return $this->creer($instanceId, 'ecriture');
|
||||
}
|
||||
|
||||
public function pourLecture(string $instanceId): string
|
||||
{
|
||||
return $this->creer($instanceId, 'lecture');
|
||||
}
|
||||
|
||||
private function creer(string $instanceId, string $role): string
|
||||
{
|
||||
$jeton = Str::random(40);
|
||||
$url = $this->nova->consoleSerielle($instanceId);
|
||||
|
||||
Redis::setex(
|
||||
"console:{$jeton}",
|
||||
self::DUREE_MINUTES * 60,
|
||||
json_encode([
|
||||
'url' => $url,
|
||||
'instance_id' => $instanceId,
|
||||
'role' => $role,
|
||||
]),
|
||||
);
|
||||
|
||||
return $jeton;
|
||||
}
|
||||
}
|
||||
59
portail/app/Services/OpenStack/NovaClient.php
Normal file
59
portail/app/Services/OpenStack/NovaClient.php
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\OpenStack;
|
||||
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Juste ce qu'il faut pour demander à Nova l'adresse de la console série
|
||||
* d'une VM (étape 9). La liaison Laravel ↔ OpenStack complète (Heat, labs)
|
||||
* est l'étape 10 : on ne construit pas de client générique ici.
|
||||
*/
|
||||
class NovaClient
|
||||
{
|
||||
public function consoleSerielle(string $instanceId): string
|
||||
{
|
||||
$reponse = Http::withHeaders([
|
||||
'X-Auth-Token' => $this->jetonKeystone(),
|
||||
'OpenStack-API-Version' => 'compute 2.99',
|
||||
])
|
||||
->post(config('openstack.nova_url')."/servers/{$instanceId}/remote-consoles", [
|
||||
'remote_console' => ['protocol' => 'serial', 'type' => 'serial'],
|
||||
]);
|
||||
|
||||
if ($reponse->failed()) {
|
||||
throw new RuntimeException("Nova a refusé la demande de console série pour {$instanceId} : ".$reponse->body());
|
||||
}
|
||||
|
||||
return $reponse->json('remote_console.url');
|
||||
}
|
||||
|
||||
/**
|
||||
* Jeton Keystone mis en cache (un jeton v3 dure 1h côté Keystone, on le
|
||||
* garde un peu moins longtemps par prudence).
|
||||
*/
|
||||
private function jetonKeystone(): string
|
||||
{
|
||||
return Cache::remember('openstack.jeton_keystone', now()->addMinutes(50), function () {
|
||||
$reponse = Http::post(config('openstack.auth_url').'/v3/auth/tokens', [
|
||||
'auth' => [
|
||||
'identity' => [
|
||||
'methods' => ['application_credential'],
|
||||
'application_credential' => [
|
||||
'id' => config('openstack.application_credential_id'),
|
||||
'secret' => config('openstack.application_credential_secret'),
|
||||
],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
if ($reponse->failed()) {
|
||||
throw new RuntimeException('Authentification OpenStack refusée : '.$reponse->body());
|
||||
}
|
||||
|
||||
return $reponse->header('X-Subject-Token');
|
||||
});
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue