feat(artemis/infra): Ajoute la gestion des serveurs Google Compute Engine en français

This commit is contained in:
Serreau Jovann
2025-07-21 13:25:15 +02:00
parent 8f96e1c2fb
commit 18ef3466b5
13 changed files with 1708 additions and 13 deletions

12
account.json Normal file
View File

@@ -0,0 +1,12 @@
{
"type": "service_account",
"project_id": "esy-web-279616",
"private_key_id": "1028eb629a868bdd6b0722e208a25f0b54788285",
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCl7Iq0fY+P/Uf4\nef8Px/bU1l2/BV8dM8i7tAC1nDPztTQriw6zUQrnloAKHT6RZh5YXh0bKLVB3ZF0\nWtzx53bGw7Ufn3Xy7at1Cbk2tmamw5dYdUzRpiPFQKPr2NbIMHUEUBZkU9TlV4J4\nhLOwEvAFURRN08m2wOmRPpOvyzy0FL/ibjMY7rKuPvXfMrFtdFKsXbDT1iCV4UG8\nFwou0jKLXro/yRJ45P2KWprvS8X0dAwfHVcfLZbdT+ifW5FtCZh7xITe7UyhW+Zj\nkSZXhMbUga9AWHKzfUyesW+QvBmsJRarcwrSdn1PggJj7CICTFJDo0u9S5+mk9/+\ncsvEQAqNAgMBAAECggEABsCQiHXPoNFy21bDnXXKwrCSkhJ642pXEeRqXvmjV5iZ\nLehzybJTmkcSoNa91BbuxzPVRWSrs6l7oNuNwD5mJAKU3VuU5jTr1FB3/BUCKmkt\nQIlNff6f2AggY5moa+8k4+8KajzLHiYohhUIZvLuV5kMFXq367sABkwgoHfJ2Z4p\nxqBM3B5/x/1JxheBYhc1pgLxza40eE/mKJMP6eC1VPkru0ntxLcFbcoGKwL/dP8l\nnCGiHRFtt5Weh2wW2A9F0ZkfmdhtUyh9kIKiEZmq/AXbPm5g0+v9Mpn9omWw/Ogu\nTrsKZ5F5YWK9XSFf2HaNWKI0u1b3j05KUSX510WhQQKBgQDOZGqULtmQqNi12kQX\nMvkw3D3ExMH4fziJ8oi6t4Bpp93iAnE5q/9zdWmTxICSVGoY6zhMAsfTSmzeA6Fp\nW7aRjNDeyw3skHV/mf0Kg015AwyaAlhKOLMSh2fX+p3Ptr5dUHe90vqYzI0PVAFc\noBAIhzBzaTIViccyf+CKCM72EQKBgQDNzg+SlnFKeyuHNLUPX2Ek9ZldXFkoU6wI\nRptp9ux/S+PWLgSGF8mX04e7t5eccc+H9Rhfm+ndM4nbkfkZXgHtzASGBUeaFSQl\n/tU6nmRcudwNcIbN7yUdt8Ll+KqusEYRSfVWJ4XIyNMW+WKQ/Wjw1Vo8rfkzgVA3\nrM81zk5gvQKBgF2Oz3FUu3MD1xi4VF1f0e4AKE/mETegE/UUaD6bqC481iv5h3Hp\nMecYtj8xuEac1WtuVRq/t+1js24An06vMFdSSex0h19RLLInD7mQQ7IzW6cEoRkk\nEqi3kK8rABaEdE7Ah0cZOFfDgb4NCoD+XcY/4gqvCPESf6W4qgRoccjBAoGAO/xZ\nsaJD9y+baldEhuyIBhvHzdyC6CwrMmZSGjqsiBX4nI7hJqx8R9KR93b1q9XIZZpc\ntlFdgunovqT3dBtgeI1ErEORsSmEVcbHI8TS/+v0Zb5srE2OBfFvz1QBe9VJNvTV\nm3z9k55lWIbr4dLa6YdmO9WBky+X0AKAivBNDAkCgYBJvrnMytfe1FLQcV1EJxkY\nIfeDI7POJBnpeN/DAE5/8231rRmhqsKmTGzieKysRi+s0Yx9V4q4ixR6O9+smpXI\nAWyV3QME6AhLicuzoWobYj5wgV9vHYNMawkSgljBWLmU20aWhRpYqESr355QrPCU\nx0HwZXICbVUERaSfD1ALyQ==\n-----END PRIVATE KEY-----\n",
"client_email": "root-user@esy-web-279616.iam.gserviceaccount.com",
"client_id": "103002399867175151805",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/root-user%40esy-web-279616.iam.gserviceaccount.com"
}

View File

@@ -5,6 +5,37 @@ import {AutoSubmit} from './class/AutoSubmit'
function script() {
customElements.define('auto-submit',AutoSubmit,{extends:'form'})
}
document.addEventListener('DOMContentLoaded', script);
function full() {
const sidebar = document.getElementById('sidebar');
const sidebarToggle = document.getElementById('sidebar-toggle');
sidebarToggle.addEventListener('click', function () {
sidebar.classList.toggle('-translate-x-full');
});
const submenuToggles = document.querySelectorAll('[data-submenu-toggle]');
submenuToggles.forEach(button => {
button.addEventListener('click', function () {
const targetId = this.dataset.submenuToggle;
const submenu = document.getElementById(`submenu-${targetId}`);
const arrowIcon = this.querySelector('.arrow-icon');
if (submenu && arrowIcon) {
// Toggle the 'active' class on the submenu
submenu.classList.toggle('active');
// Toggle the 'rotate' class on the arrow icon
arrowIcon.classList.toggle('rotate');
}
});
});
}
document.addEventListener('DOMContentLoaded', ()=>{
script();
});
document.addEventListener("turbo:load", ()=> {
full()
});

View File

@@ -5,3 +5,16 @@ h1,h2,h3,h4,h5,h6,
label,span,input,{
font-family: 'Intel One Mono', monospace;
}
.card-server {
padding: 0 !important;
.header{
border-bottom: 1px solid var(--color-gray-700);
padding: 0.5rem;
background: var(--color-gray-700);
}
}
.bg-RUNNING{
background: green;
}

View File

@@ -21,6 +21,7 @@ document.addEventListener('DOMContentLoaded', () => {
});
return svg;
}
document.querySelectorAll('input[type="password"]').forEach(input=>{
// Crée un conteneur div pour l'input et l'icône
const wrapperDiv = document.createElement('div');

View File

@@ -12,6 +12,7 @@
"doctrine/doctrine-migrations-bundle": "^3.4.2",
"doctrine/orm": "^3.5",
"docusealco/docuseal-php": "^1.0",
"google/cloud": "^0.296.0",
"imagine/imagine": "^1.5",
"knplabs/knp-paginator-bundle": "^6.8",
"lasserafn/php-initial-avatar-generator": "^4.4",

1290
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20250721112006 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TABLE compute (id SERIAL NOT NULL, instance_id VARCHAR(255) NOT NULL, zone VARCHAR(255) NOT NULL, internal_ip VARCHAR(255) NOT NULL, external_ip VARCHAR(255) NOT NULL, status VARCHAR(255) DEFAULT NULL, PRIMARY KEY(id))');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SCHEMA public');
$this->addSql('DROP TABLE compute');
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Controller\Artemis\Infra;
use App\Service\Google\ComputeEngineClient;
use LasseRafn\InitialAvatarGenerator\InitialAvatar;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class ServerController extends AbstractController
{
#[Route(path: '/artemis/infra/server',name: 'artemis_infrastructure_server')]
public function artemis(Request $request,ComputeEngineClient $computeEngineClient): Response
{
if($request->query->has('sync')) {
$computeEngineClient->list();
}
$lists = [];
foreach ($computeEngineClient->list() as $instance) {
$lists[] = $computeEngineClient->detail($instance);
}
return $this->render('artemis/infra/server.twig',[
'lists' => $lists
]);
}
}

96
src/Entity/Compute.php Normal file
View File

@@ -0,0 +1,96 @@
<?php
namespace App\Entity;
use AllowDynamicProperties;
use App\Repository\ComputeRepository;
use Doctrine\ORM\Mapping as ORM;
#[AllowDynamicProperties] #[ORM\Entity(repositoryClass: ComputeRepository::class)]
class Compute
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255)]
private ?string $instanceId = null;
#[ORM\Column(length: 255)]
private ?string $zone = null;
#[ORM\Column(length: 255)]
private ?string $internalIp = null;
#[ORM\Column(length: 255)]
private ?string $externalIp = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $status = null;
public function getId(): ?int
{
return $this->id;
}
public function getInstanceId(): ?string
{
return $this->instanceId;
}
public function setInstanceId(string $instanceId): static
{
$this->instanceId = $instanceId;
return $this;
}
public function getZone(): ?string
{
return $this->zone;
}
public function setZone(string $zone): static
{
$this->zone = $zone;
return $this;
}
public function getInternalIp(): ?string
{
return $this->internalIp;
}
public function setInternalIp(string $internalIp): static
{
$this->internalIp = $internalIp;
return $this;
}
public function getExternalIp(): ?string
{
return $this->externalIp;
}
public function setExternalIp(string $externalIp): static
{
$this->externalIp = $externalIp;
return $this;
}
public function getStatus(): ?string
{
return $this->status;
}
public function setStatus(?string $status): static
{
$this->status = $status;
return $this;
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace App\Repository;
use App\Entity\Compute;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<Compute>
*/
class ComputeRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Compute::class);
}
// /**
// * @return Compute[] Returns an array of Compute objects
// */
// public function findByExampleField($value): array
// {
// return $this->createQueryBuilder('c')
// ->andWhere('c.exampleField = :val')
// ->setParameter('val', $value)
// ->orderBy('c.id', 'ASC')
// ->setMaxResults(10)
// ->getQuery()
// ->getResult()
// ;
// }
// public function findOneBySomeField($value): ?Compute
// {
// return $this->createQueryBuilder('c')
// ->andWhere('c.exampleField = :val')
// ->setParameter('val', $value)
// ->getQuery()
// ->getOneOrNullResult()
// ;
// }
}

View File

@@ -0,0 +1,74 @@
<?php
namespace App\Service\Google;
use App\Entity\Compute;
use Doctrine\ORM\EntityManagerInterface;
use Google\Cloud\Compute\V1\AccessConfig;
use Google\Cloud\Compute\V1\Client\InstancesClient;
use Google\Cloud\Compute\V1\GetInstanceRequest;
use Google\Cloud\Compute\V1\Instance;
use Google\Cloud\Compute\V1\ListInstancesRequest;
use Google\Cloud\Compute\V1\NetworkInterface;
use Google\Protobuf\RepeatedField;
use Symfony\Component\HttpKernel\KernelInterface;
class ComputeEngineClient
{
private InstancesClient $client;
private string $projectId;
private string $zone;
public function __construct(private readonly EntityManagerInterface $entityManager,KernelInterface $kernel)
{
$this->client = new InstancesClient([
'credentials' => $kernel->getProjectDir()."/account.json"
]);
$content = file_get_contents($kernel->getProjectDir()."/account.json");
$content = json_decode($content);
$this->projectId = $content->project_id;
$this->zone = "europe-west4-a";
}
public function list()
{
$request = (new ListInstancesRequest())
->setProject($this->projectId)
->setZone($this->zone);
$instancesList = $this->client->list($request);
$instances = [];
/** @var Instance $instance */
foreach ($instancesList as $instance) {
if(str_contains($instance->getName(),'srv-')) {
/** @var NetworkInterface $network */
$network = $instance->getNetworkInterfaces()[0];
/** @var AccessConfig $accessConfig */
$accessConfig = $network->getAccessConfigs()[0];
$compute = new Compute();
$compute->setInstanceId($instance->getId());
$compute->setZone(str_replace("https://www.googleapis.com/compute/v1/projects/".$this->projectId."/zones/","",$instance->getZone()));
$compute->setInternalIp($network->getNetworkIP());
$compute->setExternalIp($accessConfig->getNatIP());
$compute->setStatus("down");
$this->entityManager->persist($compute);
$instances[] = $compute;
}
}
$this->entityManager->flush();
return $instances;
}
public function detail(Compute $compute)
{
$request = (new GetInstanceRequest())
->setInstance($compute->getInstanceId())
->setProject($this->projectId)
->setZone($this->zone);
$instance = $this->client->get($request);
$compute->setStatus($instance->getStatus());
$compute->name = $instance->getName();
return$compute;
}
}

View File

@@ -26,6 +26,24 @@
background-color: #4b5563;
border-radius: 20px;
}
/* Hide submenu by default */
.submenu {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease-out;
}
/* Show submenu when active */
.submenu.active {
max-height: 500px; /* A value large enough to contain all submenu items */
transition: max-height 0.3s ease-in;
}
/* Rotate arrow icon */
.arrow-icon {
transition: transform 0.3s ease-in-out;
}
.arrow-icon.rotate {
transform: rotate(90deg);
}
</style>
</head>
<body class="bg-gray-100 dark:bg-gray-900 text-gray-900 dark:text-gray-100">
@@ -45,6 +63,38 @@
<span class="ml-3">Tableau de bord</span>
</a>
</li>
<li class="px-4 py-2">
<button class="flex items-center justify-between w-full p-2 text-base font-normal text-gray-900 dark:text-white rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none" data-submenu-toggle="infrastructure">
<div class="flex items-center">
<svg class="w-6 h-6 text-gray-500 dark:text-gray-400" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M2 10a8 8 0 018-8v8h8a8 8 0 11-16 0z"></path><path d="M12 2.252A8.014 8.014 0 0117.748 12H12V2.252z"></path></svg>
<span class="ml-3">Infrastructure</span>
</div>
<svg class="w-4 h-4 text-gray-500 dark:text-gray-400 arrow-icon" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"></path></svg>
</button>
<ul id="submenu-infrastructure" class="submenu ml-6 mt-2 space-y-2">
<li>
<a href="{{ path('artemis_infrastructure_server') }}" class="flex items-center p-2 text-sm font-normal text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700">
<span class="ml-3">Serveurs</span>
</a>
</li>
</ul>
</li>
<li class="px-4 py-2">
<button class="flex items-center justify-between w-full p-2 text-base font-normal text-gray-900 dark:text-white rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none" data-submenu-toggle="intranet">
<div class="flex items-center">
<svg class="w-6 h-6 text-gray-500 dark:text-gray-400" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M2 10a8 8 0 018-8v8h8a8 8 0 11-16 0z"></path><path d="M12 2.252A8.014 8.014 0 0117.748 12H12V2.252z"></path></svg>
<span class="ml-3">Intranet</span>
</div>
<svg class="w-4 h-4 text-gray-500 dark:text-gray-400 arrow-icon" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"></path></svg>
</button>
<ul id="submenu-intranet" class="submenu ml-6 mt-2 space-y-2">
<li>
<a href="#" class="flex items-center p-2 text-sm font-normal text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700">
<span class="ml-3">Serveurs</span>
</a>
</li>
</ul>
</li>
</ul>
</nav>
</aside>
@@ -101,16 +151,5 @@
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
const sidebar = document.getElementById('sidebar');
const sidebarToggle = document.getElementById('sidebar-toggle');
sidebarToggle.addEventListener('click', function () {
sidebar.classList.toggle('-translate-x-full');
});
});
</script>
</body>
</html>

View File

@@ -0,0 +1,37 @@
{% extends 'artemis/base.twig' %}
{% block content %}
<div class="flex justify-between items-center mb-6">
<h2 class="text-3xl font-semibold text-gray-800 dark:text-gray-200">Liste des Serveurs</h2>
<div>
<button class="px-4 py-2 bg-blue-600 text-white font-medium rounded-md shadow-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900">
+ Ajouter un nouveau serveur
</button>
{% if is_granted('ROLE_ROOT') %}
<a href="{{ path('artemis_infrastructure_server',{sync:1}) }}" class="px-4 py-2 bg-purple-600 text-white font-medium rounded-md shadow-md hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900">
Sync serveur
</a>
{% endif %}
</div>
</div>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4">
{% for instance in lists %}
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-4 card-server">
<div class="header flex justify-between items-center mb-2">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">{{ instance.name }}</h3>
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-green-100 bg-{{ instance.status }}">{{ instance.status }}</span>
</div>
<p class="text-sm text-gray-500 dark:text-gray-300 mb-1">IP Interne: {{ instance.internalIp }}</p>
<p class="text-sm text-gray-500 dark:text-gray-300 mb-1">IP Externe: {{ instance.externalIp }}</p>
<p class="text-sm text-gray-500 dark:text-gray-300 mb-1">Sites: 5</p>
<p class="text-sm text-gray-500 dark:text-gray-300 mb-3">Uptime: 25 jours</p>
<div class="flex justify-end space-x-2">
<a href="#" class="text-blue-600 hover:text-blue-900 dark:text-blue-400 dark:hover:text-blue-200 text-sm">Voir</a>
<a href="#" class="text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-200 text-sm">Éditer</a>
</div>
</div>
{% endfor %}
</div>
{% endblock %}