develop #18

Merged
maranda merged 17 commits from develop into main 2025-01-13 15:24:09 +01:00
55 changed files with 1596 additions and 991 deletions

1364
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -7,8 +7,9 @@ when@dev:
handlers:
main:
type: stream
path: "%kernel.logs_dir%/%kernel.environment%.log"
level: debug
level: info
path: php://stderr
formatter: App\Formatter\CustomLineFormatter
channels: ["!event"]
# uncomment to get logging in your browser
# you may have to allow bigger header sizes in your Web server configuration

View File

@ -33,6 +33,8 @@ security:
- { path: ^/og-repository/webhook, roles: PUBLIC_ACCESS }
- { path: ^/og-lives/install/webhook, roles: PUBLIC_ACCESS }
- { path: ^/auth/refresh, roles: PUBLIC_ACCESS }
- { path: ^/menu-browser, roles: PUBLIC_ACCESS }
- { path: ^/menu/, roles: PUBLIC_ACCESS }
- { path: ^/, roles: IS_AUTHENTICATED_FULLY }
when@test:

View File

@ -0,0 +1,33 @@
<?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 Version20241211074943 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('DROP INDEX UNIQ_IDENTIFIER_NAME ON og_live');
$this->addSql('ALTER TABLE og_live DROP name');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE og_live ADD name VARCHAR(255) NOT NULL');
$this->addSql('CREATE UNIQUE INDEX UNIQ_IDENTIFIER_NAME ON og_live (name)');
}
}

View File

@ -0,0 +1,33 @@
<?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 Version20241211075520 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('ALTER TABLE network_settings DROP FOREIGN KEY FK_48869B54F7E54CF3');
$this->addSql('ALTER TABLE network_settings ADD CONSTRAINT FK_48869B54F7E54CF3 FOREIGN KEY (og_live_id) REFERENCES og_live (id) ON DELETE SET NULL');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE network_settings DROP FOREIGN KEY FK_48869B54F7E54CF3');
$this->addSql('ALTER TABLE network_settings ADD CONSTRAINT FK_48869B54F7E54CF3 FOREIGN KEY (og_live_id) REFERENCES og_live (id)');
}
}

View File

@ -0,0 +1,33 @@
<?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 Version20241211080733 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('ALTER TABLE og_live CHANGE filename filename VARCHAR(255) NOT NULL');
$this->addSql('CREATE UNIQUE INDEX UNIQ_IDENTIFIER_FILENAME ON og_live (filename)');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('DROP INDEX UNIQ_IDENTIFIER_FILENAME ON og_live');
$this->addSql('ALTER TABLE og_live CHANGE filename filename VARCHAR(255) DEFAULT NULL');
}
}

View File

@ -0,0 +1,31 @@
<?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 Version20241216080914 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('ALTER TABLE menu DROP title');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE menu ADD title VARCHAR(255) NOT NULL');
}
}

View File

@ -0,0 +1,31 @@
<?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 Version20250107121226 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('ALTER TABLE menu ADD is_default TINYINT(1) NOT NULL');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE menu DROP is_default');
}
}

View File

@ -0,0 +1,33 @@
<?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 Version20250107124654 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('ALTER TABLE client DROP FOREIGN KEY FK_C7440455CCD7E912');
$this->addSql('ALTER TABLE client ADD CONSTRAINT FK_C7440455CCD7E912 FOREIGN KEY (menu_id) REFERENCES menu (id) ON DELETE SET NULL');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE client DROP FOREIGN KEY FK_C7440455CCD7E912');
$this->addSql('ALTER TABLE client ADD CONSTRAINT FK_C7440455CCD7E912 FOREIGN KEY (menu_id) REFERENCES menu (id)');
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace App\Command;
use App\Entity\Menu;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
#[AsCommand(name: 'opengnsys:load-default-menu', description: 'Load the default menu')]
class LoadDefaultMenuCommand extends Command
{
public function __construct(
private readonly EntityManagerInterface $entityManager
)
{
parent::__construct();
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$menu = new Menu();
$menu->setName('Default menu');
$menu->setResolution('1920x1080');
$menu->setComments('Default menu comments');
$menu->setPublicUrl('main');
$menu->setIsDefault(true);
$this->entityManager->persist($menu);
$this->entityManager->flush();
return Command::SUCCESS;
}
}

View File

@ -6,7 +6,9 @@ namespace App\Command\Migration;
use App\Entity\Client;
use App\Entity\Image;
use App\Entity\ImageRepository;
use App\Entity\OrganizationalUnit;
use App\Model\ImageStatus;
use App\Model\OrganizationalUnitTypes;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Query\ResultSetMapping;
@ -70,18 +72,26 @@ class MigrateImagesCommand extends Command
}
$imageEntity = $imagesRepository->findOneBy(['migrationId' => $image['idimagen']]);
$repository = $this->entityManager->getRepository(ImageRepository::class)->findAll()[0];
if(!$imageEntity) {
$imageEntity = new Image();
$imageEntity->setMigrationId((string) $image['idimagen']);
$imageEntity->setName($image['nombreca']);
$imageEntity->setClient($clientEntity);
$imageEntity->setOrganizationalUnit($ouEntity);
//$imageEntity->setOrganizationalUnit($ouEntity);
$imageEntity->setRemotePc(false);
$imageEntity->setStatus(ImageStatus::SUCCESS);
$imageEntity->setRepository($repository);
$imageEntity->setRevision((string) $image['revision']);
$imageEntity->setDescription($image['descripcion']);
$imageEntity->setComments($image['comentarios']);
}
$this->entityManager->persist($imageEntity);
$this->entityManager->flush();
}
$this->entityManager->flush();

View File

@ -18,6 +18,9 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
class DeployImageAction extends AbstractController
@ -32,17 +35,42 @@ class DeployImageAction extends AbstractController
{
}
/**
* @throws RedirectionExceptionInterface
* @throws ClientExceptionInterface
* @throws ServerExceptionInterface
*/
public function __invoke(DeployImageInput $input, Image $image): JsonResponse
{
/** @var Partition $partition */
$partition = $input->partition->getEntity();
switch ($input->method){
case DeployMethodTypes::UNICAST:
case DeployMethodTypes::UNICAST_DIRECT:
$inputData = [
'method' => $input->method,
'client' => $input->client->getEntity()->getUuid(),
'image' => $image->getUuid(),
'numDisk' => (string) $partition->getDiskNumber(),
'numPartition' => (string) $partition->getPartitionNumber(),
];
$agentJobId = $this->deployImageOgAgentAction->__invoke($image, $input, DeployMethodTypes::UNICAST);
$this->createService->__invoke($input->client->getEntity(), CommandTypes::DEPLOY_IMAGE, TraceStatus::IN_PROGRESS, $agentJobId, $inputData);
break;
case DeployMethodTypes::MULTICAST_UFTP:
case DeployMethodTypes::MULTICAST_UDPCAST:
case DeployMethodTypes::MULTICAST:
case DeployMethodTypes::MULTICAST_UFTP_DIRECT:
case DeployMethodTypes::MULTICAST_UDPCAST_DIRECT:
$inputData = [
'method' => $input->method,
'client' => $input->client->getEntity()->getUuid(),
'image' => $image->getUuid(),
'p2pMode' => $input->p2pMode,
'p2pTime' => $input->p2pTime,
'mcastIp' => $input->mcastIp,
'mcastPort' => $input->mcastPort,
'mcastSpeed' => $input->mcastSpeed,
@ -51,16 +79,30 @@ class DeployImageAction extends AbstractController
'numPartition' => (string) $partition->getPartitionNumber(),
];
switch ($input->method){
case DeployMethodTypes::UNICAST:
$agentJobId = $this->deployImageOgAgentAction->__invoke($image, $input, DeployMethodTypes::UNICAST);
try {
$this->deployImageOgRepositoryAction->__invoke($input, $image, $this->httpClient);
} catch (\Exception $e) {
return new JsonResponse(data: ['error' => $e->getMessage()], status: Response::HTTP_INTERNAL_SERVER_ERROR);
}
$agentJobId = $this->deployImageOgAgentAction->__invoke($image, $input, DeployMethodTypes::MULTICAST);
$this->createService->__invoke($input->client->getEntity(), CommandTypes::DEPLOY_IMAGE, TraceStatus::IN_PROGRESS, $agentJobId, $inputData);
break;
case DeployMethodTypes::MULTICAST:
$agentJobId = $this->deployImageOgAgentAction->__invoke($image, $input, DeployMethodTypes::MULTICAST);
$this->createService->__invoke($image->getClient(), CommandTypes::DEPLOY_IMAGE, TraceStatus::IN_PROGRESS, $agentJobId, $inputData);
case DeployMethodTypes::TORRENT:
$inputData = [
'method' => $input->method,
'client' => $input->client->getEntity()->getUuid(),
'image' => $image->getUuid(),
'p2pMode' => $input->p2pMode,
'p2pTime' => $input->p2pTime,
'numDisk' => (string) $partition->getDiskNumber(),
'numPartition' => (string) $partition->getPartitionNumber(),
];
$agentJobId = $this->deployImageOgAgentAction->__invoke($image, $input, DeployMethodTypes::TORRENT);
$this->createService->__invoke($input->client->getEntity(), CommandTypes::DEPLOY_IMAGE, TraceStatus::IN_PROGRESS, $agentJobId, $inputData);
break;
}

View File

@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
namespace App\Controller;
use App\Entity\Client;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Twig\Environment;
class MenuBrowserController extends AbstractController
{
public function __construct(
protected readonly EntityManagerInterface $entityManager,
private Environment $twig,
)
{
}
#[Route('/menu-browser')]
public function index(Request $request): Response
{
$host = $request->getClientIp();
$partitions = [];
$client = $this->entityManager->getRepository(Client::class)->findOneBy(['ip' => $host]);
if ($client) {
$partitions = $client->getPartitions();
}
$menuName = $client->getMenu()->getPublicUrl();
return $this->render('browser/' . $menuName . '.html.twig', [
'ip' => $host,
'partitions' => $partitions,
]);
}
#[Route('/menu/{templateName}', name: 'render_menu_template')]
public function renderMenu(string $templateName, Request $request): Response
{
$templatePath = 'browser/' . $templateName . '.html.twig';
if (!$this->twig->getLoader()->exists($templatePath)) {
throw $this->createNotFoundException(sprintf('La plantilla "%s" no existe.', $templateName));
}
return $this->render($templatePath, [
'ip' => 'invitado',
'partitions' => [],
]);
}
}

View File

@ -16,6 +16,7 @@ use App\Model\TraceStatus;
use App\Service\Trace\CreateService;
use Doctrine\ORM\EntityManagerInterface;
use Exception;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
@ -33,6 +34,7 @@ class CreateImageAction extends AbstractController
protected readonly EntityManagerInterface $entityManager,
protected readonly HttpClientInterface $httpClient,
protected readonly CreateService $createService,
protected readonly LoggerInterface $logger,
)
{
}
@ -77,6 +79,7 @@ class CreateImageAction extends AbstractController
}
try {
$this->logger->info('Creating image', ['image' => $image->getId()]);
$response = $this->httpClient->request('POST', 'https://'.$image->getClient()->getIp().':8000/CloningEngine/CrearImagen', [
'verify_peer' => false,
'verify_host' => false,
@ -86,7 +89,9 @@ class CreateImageAction extends AbstractController
'json' => $data,
]);
} catch (TransportExceptionInterface $e) {
$this->logger->error('Error creating image', ['image' => $image->getId(), 'error' => $e->getMessage()]);
return new JsonResponse(
data: ['error' => $e->getMessage()],
status: Response::HTTP_INTERNAL_SERVER_ERROR

View File

@ -11,9 +11,11 @@ use App\Entity\Image;
use App\Entity\Partition;
use App\Entity\Trace;
use App\Model\ClientStatus;
use App\Model\DeployMethodTypes;
use App\Model\TraceStatus;
use App\Service\Trace\CreateService;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
@ -31,11 +33,11 @@ class DeployImageAction extends AbstractController
protected readonly EntityManagerInterface $entityManager,
protected readonly HttpClientInterface $httpClient,
protected readonly CreateService $createService,
protected readonly LoggerInterface $logger,
)
{
}
public function __invoke(Image $image, DeployImageInput $input, string $method)
{
if (!$image->getClient()->getIp()) {
@ -50,6 +52,26 @@ class DeployImageAction extends AbstractController
/** @var Partition $partition */
$partition = $input->partition->getEntity();
$method = match ($input->method) {
DeployMethodTypes::MULTICAST_UFTP_DIRECT, DeployMethodTypes::MULTICAST_UDPCAST_DIRECT, => 'multicast-direct',
DeployMethodTypes::MULTICAST, DeployMethodTypes::MULTICAST_UFTP, DeployMethodTypes::MULTICAST_UDPCAST => 'multicast',
DeployMethodTypes::UNICAST_DIRECT => 'unicast-direct',
DeployMethodTypes::UNICAST => 'unicast',
DeployMethodTypes::TORRENT => 'torrent',
default => throw new ValidatorException('Invalid method'),
};
$ptcMulticastValue = "$method $input->mcastPort:$input->mcastMode:$input->mcastIp:$input->mcastSpeed:$input->maxClients:$input->maxTime";
$ptcTorrentValue = "$method $input->p2pMode:$input->p2pTime";
$ptcUnicastValue = $method;
$ptcValue = match ($input->method) {
DeployMethodTypes::MULTICAST, DeployMethodTypes::MULTICAST_UFTP, DeployMethodTypes::MULTICAST_UFTP_DIRECT, DeployMethodTypes::MULTICAST_UDPCAST, DeployMethodTypes::MULTICAST_UDPCAST_DIRECT => $ptcMulticastValue,
DeployMethodTypes::UNICAST, DeployMethodTypes::UNICAST_DIRECT => $ptcUnicastValue,
DeployMethodTypes::TORRENT => $ptcTorrentValue,
default => throw new ValidatorException('Invalid method'),
};
$data = [
'dsk' => (string) $partition->getDiskNumber(),
'par' => (string) $partition->getPartitionNumber(),
@ -58,7 +80,7 @@ class DeployImageAction extends AbstractController
'nci' => $image->getName(),
'ipr' => $image->getRepository()->getIp(),
'nfn' => 'RestaurarImagen',
'ptc' => $method,
'ptc' => $ptcValue,
'ids' => '0'
];
@ -71,8 +93,10 @@ class DeployImageAction extends AbstractController
],
'json' => $data,
]);
$this->logger->info('Deploying image', ['image' => $image->getId()]);
} catch (TransportExceptionInterface $e) {
$this->logger->error('Error deploying image', ['image' => $image->getId(), 'error' => $e->getMessage()]);
return new JsonResponse(
data: ['error' => $e->getMessage()],
status: Response::HTTP_INTERNAL_SERVER_ERROR

View File

@ -15,6 +15,7 @@ use App\Model\ImageStatus;
use App\Model\TraceStatus;
use App\Service\Trace\CreateService;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
@ -32,6 +33,7 @@ class PartitionAssistantAction extends AbstractController
protected readonly EntityManagerInterface $entityManager,
protected readonly HttpClientInterface $httpClient,
protected readonly CreateService $createService,
protected readonly LoggerInterface $logger,
)
{
}
@ -70,12 +72,11 @@ class PartitionAssistantAction extends AbstractController
'par' => (string) $partition->partitionNumber,
'cpt' => $partition->partitionCode,
'sfi' => $partition->filesystem,
'tam' => (string) ($partition->size * 1024),
'tam' => (string) (integer) ($partition->size * 1024),
'ope' => $partition->format ? "1" : "0",
];
}
// Hacer una llamada por cada disco
foreach ($disks as $diskNumber => $diskInfo) {
$data = [];
if (!empty($diskInfo['diskData'])) {
@ -99,7 +100,9 @@ class PartitionAssistantAction extends AbstractController
],
'json' => $result,
]);
$this->logger->info('Partitioning disk', ['client' => $client->getId(), 'disk' => $diskNumber]);
} catch (TransportExceptionInterface $e) {
$this->logger->error('Error partitioning disk', ['client' => $client->getId(), 'disk' => $diskNumber, 'error' => $e->getMessage()]);
return new JsonResponse(
data: ['error' => "Error en disco $diskNumber: " . $e->getMessage()],
status: Response::HTTP_INTERNAL_SERVER_ERROR

View File

@ -14,6 +14,7 @@ use App\Model\ImageStatus;
use App\Model\TraceStatus;
use App\Service\Trace\CreateService;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
@ -31,6 +32,7 @@ class PowerOffAction extends AbstractController
protected readonly EntityManagerInterface $entityManager,
protected readonly HttpClientInterface $httpClient,
protected readonly CreateService $createService,
protected readonly LoggerInterface $logger,
)
{
}
@ -55,8 +57,10 @@ class PowerOffAction extends AbstractController
],
'json' => $data,
]);
$this->logger->info('Powering off client', ['client' => $client->getId()]);
} catch (TransportExceptionInterface $e) {
$this->logger->error('Error powering off client', ['client' => $client->getId(), 'error' => $e->getMessage()]);
return new JsonResponse(
data: ['error' => $e->getMessage()],
status: Response::HTTP_INTERNAL_SERVER_ERROR

View File

@ -14,6 +14,7 @@ use App\Model\ImageStatus;
use App\Model\TraceStatus;
use App\Service\Trace\CreateService;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
@ -31,6 +32,7 @@ class RebootAction extends AbstractController
protected readonly EntityManagerInterface $entityManager,
protected readonly HttpClientInterface $httpClient,
protected readonly CreateService $createService,
protected readonly LoggerInterface $logger,
)
{
}
@ -55,8 +57,10 @@ class RebootAction extends AbstractController
],
'json' => $data,
]);
$this->logger->info('Rebooting client', ['client' => $client->getId()]);
} catch (TransportExceptionInterface $e) {
$this->logger->error('Error rebooting client', ['client' => $client->getId(), 'error' => $e->getMessage()]);
return new JsonResponse(
data: ['error' => $e->getMessage()],
status: Response::HTTP_INTERNAL_SERVER_ERROR

View File

@ -47,7 +47,7 @@ class StatusAction extends AbstractController
throw new ValidatorException('IP is required');
}
if ($client->getStatus() === ClientStatus::OG_LIVE || $client->getStatus() === ClientStatus::OFF || $client->getStatus() === ClientStatus::BUSY) {
if ($client->getStatus() === ClientStatus::OG_LIVE || $client->getStatus() === ClientStatus::OFF || $client->getStatus() === ClientStatus::BUSY || $client->getStatus() === ClientStatus::INITIALIZING) {
$response = $this->getOgLiveStatus($client);
}
@ -79,6 +79,7 @@ class StatusAction extends AbstractController
$client->setStatus($statusCode === Response::HTTP_OK ? ClientStatus::OG_LIVE : ClientStatus::OFF);
} catch (TransportExceptionInterface $e) {
$this->logger->error('Error checking client status', ['client' => $client->getId(), 'error' => $e->getMessage()]);
$client->setStatus(ClientStatus::OFF);
$this->entityManager->persist($client);
$this->entityManager->flush();
@ -116,7 +117,7 @@ class StatusAction extends AbstractController
} catch (TransportExceptionInterface $e) {
$client->setStatus(ClientStatus::OFF);
$this->logger->error('Error checking client status', ['client' => $client->getId(), 'error' => $e->getMessage()]);
return Response::HTTP_INTERNAL_SERVER_ERROR;
}

View File

@ -7,6 +7,7 @@ namespace App\Controller\OgAgent\Webhook;
use App\Entity\Client;
use App\Entity\OrganizationalUnit;
use App\Entity\Partition;
use App\Model\ClientStatus;
use App\Service\CreatePartitionService;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
@ -47,6 +48,9 @@ class AgentController extends AbstractController
return new JsonResponse(['message' => 'Client not found'], Response::HTTP_NOT_FOUND);
}
$clientEntity->setStatus(ClientStatus::OG_LIVE);
$this->entityManager->persist($clientEntity);
if (isset($data['cfg'])) {
$this->createPartitionService->__invoke($data, $clientEntity);
}

View File

@ -113,34 +113,7 @@ class ClientsController extends AbstractController
$this->logger->info('Image updated. Success.', ['image' => (string) $image->getUuid()]);
}
if ($data['nfn'] === 'RESPUESTA_RestaurarImagen') {
$trace = $this->entityManager->getRepository(Trace::class)->findOneBy(['jobId' => $data['job_id']]);
$image = $this->entityManager->getRepository(Image::class)->findOneBy(['uuid' => $data['idi']]);
$client = $trace->getClient();
if ($data['res'] === 1) {
$trace->setStatus(TraceStatus::SUCCESS);
$trace->setFinishedAt(new \DateTime());
$image->setStatus(ImageStatus::PENDING);
$client->setStatus(ClientStatus::OG_LIVE);
if (isset($data['cfg'])) {
$this->createPartitionService->__invoke($data,$client);
}
} else {
$trace->setStatus(TraceStatus::FAILED);
$trace->setFinishedAt(new \DateTime());
$trace->setOutput($data['der']);
}
$client->setStatus(ClientStatus::OG_LIVE);
$this->entityManager->persist($client);
$this->entityManager->persist($image);
$this->entityManager->persist($trace);
$this->entityManager->flush();
}
if ($data['nfn'] === 'RESPUESTA_Configurar') {
if ($data['nfn'] === 'RESPUESTA_RestaurarImagen'|| $data['nfn'] === 'RESPUESTA_Configurar') {
$trace = $this->entityManager->getRepository(Trace::class)->findOneBy(['jobId' => $data['job_id']]);
$client = $trace->getClient();
@ -158,7 +131,6 @@ class ClientsController extends AbstractController
}
$client->setStatus(ClientStatus::OG_LIVE);
$this->entityManager->persist($client);
$this->entityManager->persist($trace);
$this->entityManager->flush();
@ -185,7 +157,7 @@ class ClientsController extends AbstractController
}
$softwareProfile = new SoftwareProfile();
$softwareProfile->setDescription('Perfil: ' . $image->getName());
$softwareProfile->setDescription('Perfil software: ' . $image->getClient()->getName());
$softwareProfile->setOrganizationalUnit($image->getClient()->getOrganizationalUnit());
foreach ($existingSoftware as $softwareEntity) {

View File

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace App\Controller\OgBoot;
use App\Controller\OgBoot\PxeBootFile\PostAction;
use App\Service\Trace\CreateService;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
@ -26,8 +27,11 @@ abstract class AbstractOgBootController extends AbstractController
protected string $ogBootApiUrl,
#[Autowire(env: 'OG_CORE_IP')]
protected string $ogCoreIP,
#[Autowire(env: 'OG_LOG_IP')]
protected string $ogLogIp,
protected readonly EntityManagerInterface $entityManager,
protected readonly HttpClientInterface $httpClient,
protected readonly CreateService $createService,
)
{
}

View File

@ -32,14 +32,22 @@ class SyncAction extends AbstractOgBootController
foreach ($content['message']['installed_ogLives'] as $ogLive) {
$ogLiveEntity = $this->entityManager->getRepository(OgLive::class)->findOneBy(['checksum' => $ogLive['id']]);
if (!$ogLiveEntity) {
$ogLiveEntity = $this->entityManager->getRepository(OgLive::class)->findOneBy(['filename' => str_replace(self::OG_BOOT_DIRECTORY, '', $ogLive['directory'])]);
if (!$ogLiveEntity) {
$ogLiveEntity = new OgLive();
}
}
$this->extracted($ogLiveEntity, $ogLive);
$this->entityManager->persist($ogLiveEntity);
}
$this->entityManager->flush();
//$this->serDefaultOgLive($content['default_oglive']);
if (isset($content['message']['default_oglive'])) {
$this->serDefaultOgLive($content['message']['default_oglive']);
}
return new JsonResponse(data: $content, status: Response::HTTP_OK);
}
@ -51,13 +59,10 @@ class SyncAction extends AbstractOgBootController
*/
private function extracted(OgLive $ogLiveEntity, mixed $ogLive): void
{
if (!$ogLiveEntity->getId()){
$ogLiveEntity->setName(str_replace(self::OG_BOOT_DIRECTORY, '', $ogLive['directory']));
}
$ogLiveEntity->setInstalled(true);
$ogLiveEntity->setArchitecture($ogLive['architecture']);
$ogLiveEntity->setDistribution($ogLive['distribution']);
$ogLiveEntity->setFilename($ogLive['directory']);
$ogLiveEntity->setFilename(str_replace(self::OG_BOOT_DIRECTORY, '', $ogLive['directory']));
$ogLiveEntity->setKernel($ogLive['kernel']);
$ogLiveEntity->setRevision($ogLive['revision']);
$ogLiveEntity->setDirectory($ogLive['directory']);
@ -67,7 +72,7 @@ class SyncAction extends AbstractOgBootController
private function serDefaultOgLive(string $defaultOgLive): void
{
$ogLiveEntity = $this->entityManager->getRepository(OgLive::class)->findOneBy(['name' => $defaultOgLive]);
$ogLiveEntity = $this->entityManager->getRepository(OgLive::class)->findOneBy(['filename' => $defaultOgLive]);
if (!$ogLiveEntity) {
return;

View File

@ -6,6 +6,7 @@ use App\Controller\OgBoot\AbstractOgBootController;
use App\Entity\OgLive;
use App\Model\OgLiveStatus;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
@ -24,11 +25,11 @@ class InstallOgLiveResponseAction extends AbstractController
{
public CONST string OG_LIVE_INSTALL_SUCCESS = 'success';
public CONST string OG_LIVE_INSTALL_FAILED = 'failure';
const string OG_BOOT_DIRECTORY = '/opt/opengnsys/ogboot/tftpboot//';
public function __construct(
protected readonly EntityManagerInterface $entityManager
protected readonly EntityManagerInterface $entityManager,
protected readonly LoggerInterface $logger,
)
{
}
@ -63,6 +64,7 @@ class InstallOgLiveResponseAction extends AbstractController
}
$this->updateOgLive($ogLive, $message, $status);
$this->logger->info(sprintf('OgLive %s updated successfully', $ogLive->getChecksum()));
return new JsonResponse(data: sprintf('OgLive %s updated successfully', $ogLive->getChecksum()), status: Response::HTTP_OK);
}
@ -70,6 +72,7 @@ class InstallOgLiveResponseAction extends AbstractController
private function updateOgLive (OgLive $ogLive, array $details, string $status): void
{
if ($status === self::OG_LIVE_INSTALL_SUCCESS) {
$ogLive->setFilename(str_replace(self::OG_BOOT_DIRECTORY, '', $ogLive['directory']));
$ogLive->setInstalled(true);
$ogLive->setSynchronized(true);
$ogLive->setChecksum($details['id']);

View File

@ -28,6 +28,14 @@ class PostAction extends AbstractOgBootController
*/
public function __invoke(Client $client, PxeTemplate $pxeTemplate): JsonResponse
{
$ogRepoIp = $this->ogBootApiUrl;
if ($client->getRepository()) {
$ogRepoIp = $client->getRepository()->getIp();
} else if ($client->getOrganizationalUnit()->getNetworkSettings()->getRepository()) {
$ogRepoIp = $client->getOrganizationalUnit()->getNetworkSettings()->getRepository()->getIp();
}
$params = [
'json' => [
'template_name' => $pxeTemplate->getName(),
@ -40,10 +48,10 @@ class PostAction extends AbstractOgBootController
'computer_name' => $client->getName(),
'netiface' => $client->getNetiface(),
'group' => $client->getOrganizationalUnit()->getName(),
'ogrepo' => $client->getRepository() ? $client->getRepository()->getIp() : $client->getOrganizationalUnit()->getNetworkSettings()->getRepository()->getIp(),
'ogrepo' => $ogRepoIp,
'ogcore' => $this->ogCoreIP,
'oglive' => $this->ogBootApiUrl,
'oglog' => $client->getOrganizationalUnit()->getNetworkSettings()?->getOgLog(),
'oglog' => $this->ogLogIp,
'ogshare' => $client->getOrganizationalUnit()->getNetworkSettings()?->getOgShare()
? $client->getOrganizationalUnit()->getNetworkSettings()?->getOgShare(): $this->ogBootApiUrl,
'oglivedir' => $client->getOgLive()->getFilename(),

View File

@ -7,6 +7,8 @@ use App\Dto\Input\DeployImageInput;
use App\Entity\Command;
use App\Entity\Image;
use App\Model\CommandTypes;
use App\Model\DeployImageTypes;
use App\Model\DeployMethodTypes;
use App\Model\TraceStatus;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
@ -24,12 +26,31 @@ class DeployImageAction extends AbstractOgRepositoryController
* @throws ServerExceptionInterface
* @throws RedirectionExceptionInterface
* @throws ClientExceptionInterface
* @throws TransportExceptionInterface
*/
public function __invoke(DeployImageInput $input, Image $data, HttpClientInterface $httpClient): JsonResponse
{
$client = $input->client;
$this->createService->__invoke($data->getClient(), CommandTypes::DEPLOY_IMAGE, TraceStatus::IN_PROGRESS, null);
$params = [
'json' => [
'ID_img' => $data->getImageFullsum(),
'bitrate' => (string) $input->mcastSpeed.'M',
'ip' => $input->mcastIp,
'port' => $input->mcastPort,
'method' => $input->mcastMode,
'nclients' => $input->maxClients,
'maxtime' => $input->maxTime
]
];
$type = match ($input->method) {
'udpcast', 'udpcast_direct' => DeployMethodTypes::MULTICAST_UDPCAST,
'p2p' => DeployMethodTypes::TORRENT,
default => DeployMethodTypes::MULTICAST_UFTP,
};
$content = $this->createRequest('POST', 'http://'.$data->getRepository()->getIp().':8006/ogrepository/v1/'.$type, $params);
return new JsonResponse(data: [], status: Response::HTTP_OK);
}

View File

@ -55,7 +55,7 @@ class WoLAction extends AbstractOgRepositoryController
$content = $this->createRequest('POST', 'http://'.$repository->getIp(). ':8006/ogrepository/v1/wol', $params);
$client->setStatus(ClientStatus::OFF);
$client->setStatus(ClientStatus::INITIALIZING);
$this->entityManager->persist($client);
$this->entityManager->flush();

View File

@ -40,14 +40,20 @@ class DeployImageInput
public ?string $mcastIp = null;
#[Groups(['image:write'])]
public ?string $mcastSpeed = null;
public ?int $mcastSpeed = null;
#[OrganizationalUnitMulticastPort]
#[Groups(['image:write'])]
public ?string $mcastPort = null;
public ?int $mcastPort = null;
#[OrganizationalUnitMulticastMode]
#[Groups(['image:write'])]
public ?string $mcastMode = null;
#[Groups(['image:write'])]
public ?int $maxClients = null;
#[Groups(['image:write'])]
public ?int $maxTime = null;
}

View File

@ -16,11 +16,6 @@ final class MenuInput
#[ApiProperty(description: 'The name of the menu', example: "Menu 1")]
public ?string $name = null;
#[Assert\NotNull()]
#[Groups(['menu:write'])]
#[ApiProperty(description: 'The title of the menu', example: "Menu 1 title")]
public ?string $title = null;
#[Groups(['menu:write'])]
#[ApiProperty(description: 'Comments of the menu', example: "Menu 1 comments")]
public ?string $comments = null;
@ -38,6 +33,10 @@ final class MenuInput
#[ApiProperty(description: 'The private url of the menu', example: "http://example.com")]
public ?string $privateUrl = null;
#[Groups(['menu:write'])]
#[ApiProperty(description: 'The default menu', example: "false")]
public ?bool $isDefault = false;
public function __construct(?Menu $menu = null)
{
if (!$menu) {
@ -45,11 +44,11 @@ final class MenuInput
}
$this->name = $menu->getName();
$this->title = $menu->getTitle();
$this->comments = $menu->getComments();
$this->resolution = $menu->getResolution();
$this->publicUrl = $menu->getPublicUrl();
$this->privateUrl = $menu->getPrivateUrl();
$this->isDefault = $menu->isDefault();
}
public function createOrUpdateEntity(?Menu $menu = null): Menu
@ -59,11 +58,11 @@ final class MenuInput
}
$menu->setName($this->name);
$menu->setTitle($this->title);
$menu->setComments($this->comments);
$menu->setResolution($this->resolution);
$menu->setPublicUrl($this->publicUrl);
$menu->setPrivateUrl($this->privateUrl);
$menu->setIsDefault($this->isDefault);
return $menu;
}

View File

@ -10,10 +10,7 @@ use Symfony\Component\Validator\Constraints as Assert;
final class OgLiveInput
{
#[Assert\NotBlank(message: 'validators.hardware.name.not_blank')]
#[Groups(['og-live:write'])]
#[ApiProperty(description: 'The name of the ogLive', example: "OgLive 1")]
public ?string $name = null;
const string DOWNLOAD_URL = 'https://ognproject.evlt.uma.es/oglive//';
#[Groups(['og-live:write'])]
#[ApiProperty(description: 'The download url of the ogLive', example: "http://example.com/oglive1.iso")]
@ -25,7 +22,6 @@ final class OgLiveInput
return;
}
$this->name = $ogLive->getName();
$this->downloadUrl = $ogLive->getDownloadUrl();
}
@ -35,7 +31,13 @@ final class OgLiveInput
$ogLive = new OgLive();
}
$ogLive->setName($this->name);
$filename = str_replace(self::DOWNLOAD_URL, '', $this->downloadUrl);
if (str_ends_with($filename, '.iso')) {
$filename = substr($filename, 0, -4);
}
$ogLive->setFilename($filename);
$ogLive->setDownloadUrl($this->downloadUrl);
$ogLive->setStatus(OgLiveStatus::INACTIVE);

View File

@ -13,9 +13,6 @@ final class MenuOutput extends AbstractOutput
#[Groups(['menu:read', 'organizational-unit:read'])]
public string $name;
#[Groups(['menu:read'])]
public ?string $title = null;
#[Groups(['menu:read'])]
public string $resolution;
@ -28,6 +25,9 @@ final class MenuOutput extends AbstractOutput
#[Groups(['menu:read'])]
public ?string $privateUrl = null;
#[Groups(['menu:read'])]
public ?bool $isDefault = false;
#[Groups(['menu:read'])]
public \DateTime $createdAt;
@ -39,11 +39,11 @@ public function __construct(Menu $menu)
parent::__construct($menu);
$this->name = $menu->getName();
$this->title = $menu->getTitle();
$this->resolution = $menu->getResolution();
$this->comments = $menu->getComments();
$this->publicUrl = $menu->getPublicUrl();
$this->privateUrl = $menu->getPrivateUrl();
$this->isDefault = $menu->isDefault();
$this->createdAt = $menu->getCreatedAt();
$this->createdBy = $menu->getCreatedBy();
}

View File

@ -10,9 +10,6 @@ use Symfony\Component\Serializer\Annotation\Groups;
#[Get(shortName: 'OgLive')]
final class OgLiveOutput extends AbstractOutput
{
#[Groups(['og-live:read', 'client:read', "organizational-unit:read"])]
public string $name;
#[Groups(['og-live:read'])]
public ?bool $synchronized = false;
@ -37,8 +34,8 @@ final class OgLiveOutput extends AbstractOutput
#[Groups(['og-live:read'])]
public ?string $revision = '';
#[Groups(['og-live:read'])]
public ?string $filename = '';
#[Groups(['og-live:read', 'client:read', "organizational-unit:read"])]
public ?string $filename = null;
#[Groups(['og-live:read'])]
public ?string $kernel = '';
@ -56,7 +53,6 @@ final class OgLiveOutput extends AbstractOutput
{
parent::__construct($ogLive);
$this->name = $ogLive->getName();
$this->synchronized = $ogLive->isSynchronized();
$this->installed = $ogLive->isInstalled();
$this->isDefault = $ogLive->getIsDefault();

View File

@ -49,6 +49,7 @@ class Client extends AbstractEntity
private Collection $partitions;
#[ORM\ManyToOne]
#[ORM\JoinColumn( onDelete: 'SET NULL')]
private ?Menu $menu = null;
#[ORM\ManyToOne]
@ -63,7 +64,7 @@ class Client extends AbstractEntity
#[ORM\ManyToOne(inversedBy: 'clients')]
private ?PxeTemplate $template = null;
#[ORM\ManyToOne(inversedBy: 'clients')]
#[ORM\ManyToOne()]
private ?ImageRepository $repository = null;
#[ORM\ManyToOne(inversedBy: 'clients')]

View File

@ -12,9 +12,6 @@ class Menu extends AbstractEntity
{
use NameableTrait;
#[ORM\Column(length: 255)]
private ?string $title = null;
#[ORM\Column(length: 255)]
private ?string $resolution = null;
@ -27,28 +24,12 @@ class Menu extends AbstractEntity
#[ORM\Column(length: 255, nullable: true)]
private ?string $privateUrl = null;
/**
* @var Collection<int, Client>
*/
#[ORM\OneToMany(mappedBy: 'menu', targetEntity: Client::class)]
private Collection $clients;
#[ORM\Column]
private ?bool $isDefault = null;
public function __construct()
{
parent::__construct();
$this->clients = new ArrayCollection();
}
public function getTitle(): ?string
{
return $this->title;
}
public function setTitle(string $title): static
{
$this->title = $title;
return $this;
}
public function getResolution(): ?string
@ -99,31 +80,14 @@ class Menu extends AbstractEntity
return $this;
}
/**
* @return Collection<int, Client>
*/
public function getClients(): Collection
public function isDefault(): ?bool
{
return $this->clients;
return $this->isDefault;
}
public function addClient(Client $client): static
public function setIsDefault(bool $isDefault): static
{
if (!$this->clients->contains($client)) {
$this->clients->add($client);
}
return $this;
}
public function removeClient(Client $client): static
{
if ($this->clients->removeElement($client)) {
// set the owning side to null (unless already changed)
if ($client->getMenu() === $this) {
$client->setMenu(null);
}
}
$this->isDefault = $isDefault;
return $this;
}

View File

@ -70,6 +70,7 @@ class NetworkSettings extends AbstractEntity
private ?ImageRepository $repository = null;
#[ORM\ManyToOne]
#[ORM\JoinColumn( onDelete: 'SET NULL')]
private ?OgLive $ogLive = null;
#[ORM\Column(length: 255, nullable: true)]

View File

@ -9,11 +9,10 @@ use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
#[ORM\Entity(repositoryClass: OgLiveRepository::class)]
#[ORM\UniqueConstraint(name: 'UNIQ_IDENTIFIER_NAME', fields: ['name'])]
#[UniqueEntity(fields: ['name'], message: 'validators.og_live.name.unique')]
#[ORM\UniqueConstraint(name: 'UNIQ_IDENTIFIER_FILENAME', fields: ['filename'])]
#[UniqueEntity(fields: ['filename'], message: 'validators.og_live.filename.unique')]
class OgLive extends AbstractEntity
{
use NameableTrait;
use SynchronizedTrait;
#[ORM\Column(length: 255, nullable: true)]
@ -37,7 +36,7 @@ class OgLive extends AbstractEntity
#[ORM\Column(length: 255, nullable: true)]
private ?string $directory = null;
#[ORM\Column(length: 255, nullable: true)]
#[ORM\Column(length: 255, nullable: false)]
private ?string $filename = null;
#[ORM\Column(nullable: true)]

View File

@ -97,12 +97,6 @@ class OrganizationalUnit extends AbstractEntity
#[ORM\Column]
private ?bool $reserved = false;
/**
* @var Collection<int, Image>
*/
#[ORM\OneToMany(mappedBy: 'organizationalUnit', targetEntity: Image::class)]
private Collection $images;
public function __construct()
{
parent::__construct();
@ -110,7 +104,6 @@ class OrganizationalUnit extends AbstractEntity
$this->users = new ArrayCollection();
$this->clients = new ArrayCollection();
$this->softwareProfiles = new ArrayCollection();
$this->images = new ArrayCollection();
}
public function getDescription(): ?string
@ -436,34 +429,4 @@ class OrganizationalUnit extends AbstractEntity
return $this;
}
/**
* @return Collection<int, Image>
*/
public function getImages(): Collection
{
return $this->images;
}
public function addImage(Image $image): static
{
if (!$this->images->contains($image)) {
$this->images->add($image);
$image->setOrganizationalUnit($this);
}
return $this;
}
public function removeImage(Image $image): static
{
if ($this->images->removeElement($image)) {
// set the owning side to null (unless already changed)
if ($image->getOrganizationalUnit() === $this) {
$image->setOrganizationalUnit(null);
}
}
return $this;
}
}

View File

@ -27,7 +27,7 @@ final readonly class ClientSubscriber implements EventSubscriberInterface
public static function getSubscribedEvents(): array
{
return [
KernelEvents::VIEW => ['sendMail', EventPriorities::POST_WRITE],
KernelEvents::VIEW => ['updatePxe', EventPriorities::POST_WRITE],
];
}
@ -37,7 +37,7 @@ final readonly class ClientSubscriber implements EventSubscriberInterface
* @throws RedirectionExceptionInterface
* @throws ClientExceptionInterface
*/
public function sendMail(ViewEvent $event): void
public function updatePxe(ViewEvent $event): void
{
$clientOutput = $event->getControllerResult();
$method = $event->getRequest()->getMethod();

View File

@ -33,7 +33,7 @@ final class MenuFactory extends ModelFactory
'createdAt' => self::faker()->dateTime(),
'name' => self::faker()->text(255),
'resolution' => self::faker()->text(255),
'title' => self::faker()->text(255),
'isDefault' => self::faker()->boolean(),
'updatedAt' => self::faker()->dateTime(),
];
}

View File

@ -34,8 +34,7 @@ final class OgLiveFactory extends ModelFactory
{
return [
'createdAt' => self::faker()->dateTime(),
'name' => self::faker()->text(255),
'downloadUrl' => self::faker()->text(255),
'filename' => self::faker()->text(255),
'status' => OgLiveStatus::ACTIVE,
'updatedAt' => self::faker()->dateTime(),
];

View File

@ -0,0 +1,26 @@
<?php
namespace App\Formatter;
use Monolog\Formatter\LineFormatter;
class CustomLineFormatter extends LineFormatter
{
public function __construct()
{
parent::__construct(null, 'Y-m-d H:i:s', true, true);
}
public function format($record): string
{
$output = [
'severity' => $record['level_name'],
'operation' => $record['channel'],
'component' => 'ogcore',
'params' => $record['context'],
'desc' => $record['message'],
];
return json_encode($output, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . PHP_EOL;
}
}

View File

@ -5,8 +5,13 @@ namespace App\Model;
final class DeployMethodTypes
{
public const string MULTICAST = 'multicast';
public const string MULTICAST_UFTP = 'uftp';
public const string MULTICAST_UFTP_DIRECT = 'uftp-direct';
public const string MULTICAST_UDPCAST = 'udpcast';
public const string MULTICAST_UDPCAST_DIRECT = 'udp-direct';
public const string UNICAST = 'unicast';
public const string TORRENT = 'torrent';
public const string UNICAST_DIRECT = 'unicast-direct';
public const string TORRENT = 'p2p';
private const array DEPLOYMENT_METHOD_TYPES = [
self::MULTICAST => 'Multicast',

View File

@ -4,8 +4,8 @@ namespace App\Model;
final class OrganizationalUnitMulticastModes
{
public const string HALF_DUPLEX = 'half-duplex';
public const string FULL_DUPLEX = 'full-duplex';
public const string HALF_DUPLEX = 'half';
public const string FULL_DUPLEX = 'full';
private const array MCAST_MODES = [
self::HALF_DUPLEX => 'Half Duplex',

View File

@ -15,4 +15,40 @@ class ClientRepository extends AbstractRepository
{
parent::__construct($registry, Client::class);
}
public function findClientsByOrganizationalUnitAndDescendants(int $organizationalUnitId): array
{
$query = $this->getEntityManager()->createQuery(
'SELECT o.path
FROM App\Entity\OrganizationalUnit o
WHERE o.id = :id'
)->setParameter('id', $organizationalUnitId);
$result = $query->getOneOrNullResult();
if (!$result) {
return [];
}
$path = $result['path'] . '/%';
$query = $this->getEntityManager()->createQuery(
'SELECT o.id
FROM App\Entity\OrganizationalUnit o
WHERE o.id = :id OR o.path LIKE :path'
)
->setParameter('id', $organizationalUnitId)
->setParameter('path', $path);
$unitIds = array_column($query->getArrayResult(), 'id');
$query = $this->getEntityManager()->createQuery(
'SELECT c
FROM App\Entity\Client c
WHERE c.organizationalUnit IN (:unitIds)'
)
->setParameter('unitIds', $unitIds);
return $query->getResult();
}
}

View File

@ -6,13 +6,15 @@ use App\Entity\Client;
use App\Entity\OperativeSystem;
use App\Entity\Partition;
use App\Model\PartitionTypes;
use App\Service\Utils\GetPartitionCodeService;
use Doctrine\ORM\EntityManagerInterface;
use Exception;
class CreatePartitionService
{
public function __construct(
protected EntityManagerInterface $entityManager
protected EntityManagerInterface $entityManager,
protected GetPartitionCodeService $partitionCodeService
)
{
}
@ -54,11 +56,15 @@ class CreatePartitionService
$partitionEntity->setSize($cfg['tam']);
if (isset($cfg['cpt']) && $cfg['cpt'] !== '') {
if ($cfg['par'] === "0") {
$partitionType = $this->partitionCodeService->__invoke($cfg['cpt']);
} else {
$decimalValue = hexdec($cfg['cpt']);
$partitionType = PartitionTypes::getPartitionType($decimalValue);
}
if ($partitionType) {
$partitionEntity->setPartitionCode($partitionType['name']);
$partitionEntity->setPartitionCode($partitionType['name'] ?? $partitionType);
}
} else {
$partitionEntity->setPartitionCode(PartitionTypes::getPartitionType(0)['name']);

View File

@ -0,0 +1,17 @@
<?php
namespace App\Service\Utils;
class GetPartitionCodeService
{
public function __invoke(string $partitionCode): string
{
return match ($partitionCode) {
"1" => "MSDOS",
"2" => "GPT",
"3" => "LVM",
"4" => "ZPOOL",
default => "UNKNOWN",
};
}
}

View File

@ -13,12 +13,14 @@ use App\Dto\Input\ClientInput;
use App\Dto\Output\ClientOutput;
use App\Dto\Output\UserGroupOutput;
use App\Repository\ClientRepository;
use App\Repository\MenuRepository;
class ClientProcessor implements ProcessorInterface
readonly class ClientProcessor implements ProcessorInterface
{
public function __construct(
private readonly ClientRepository $clientRepository,
private readonly ValidatorInterface $validator
private ClientRepository $clientRepository,
private MenuRepository $menuRepository,
private ValidatorInterface $validator
)
{
}
@ -52,11 +54,18 @@ class ClientProcessor implements ProcessorInterface
$entity = $this->clientRepository->findOneByUuid($uriVariables['uuid']);
}
$userGroup = $data->createOrUpdateEntity($entity);
$this->validator->validate($userGroup);
$this->clientRepository->save($userGroup);
$defaultMenu = $this->menuRepository->findOneBy(['isDefault' => true]);
return new ClientOutput($userGroup);
$client = $data->createOrUpdateEntity($entity);
if ($defaultMenu) {
$client->setMenu($defaultMenu);
}
$this->validator->validate($client);
$this->clientRepository->save($client);
return new ClientOutput($client);
}
private function processDelete($data, Operation $operation, array $uriVariables = [], array $context = []): null

View File

@ -11,11 +11,13 @@ use ApiPlatform\State\Pagination\TraversablePaginator;
use ApiPlatform\State\ProviderInterface;
use App\Dto\Input\ClientInput;
use App\Dto\Output\ClientOutput;
use App\Repository\ClientRepository;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
readonly class ClientProvider implements ProviderInterface
{
public function __construct(
private ClientRepository $clientRepository,
private ProviderInterface $collectionProvider,
private ProviderInterface $itemProvider
)
@ -35,18 +37,25 @@ readonly class ClientProvider implements ProviderInterface
}
}
private function provideCollection(Operation $operation, array $uriVariables = [], array $context = []): object
public function provideCollection(Operation $operation, array $uriVariables = [], array $context = []): object
{
$paginator = $this->collectionProvider->provide($operation, $uriVariables, $context);
$organizationalUnitId = $context['filters']['organizationalUnit.id'] ?? null;
if ($organizationalUnitId) {
$clients = $this->clientRepository->findClientsByOrganizationalUnitAndDescendants((int) $organizationalUnitId);
$items = new \ArrayObject();
foreach ($paginator->getIterator() as $item){
$items[] = new ClientOutput($item);
foreach ($clients as $client) {
$items[] = new ClientOutput($client);
}
return new TraversablePaginator($items, $paginator->getCurrentPage(), $paginator->getItemsPerPage(), $paginator->getTotalItems());
return new TraversablePaginator($items, 1, count($clients), count($clients));
}
return $this->collectionProvider->provide($operation, $uriVariables, $context);
}
public function provideItem(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
{
$item = $this->itemProvider->provide($operation, $uriVariables, $context);

View File

@ -1,6 +1,6 @@
{
"api-platform/core": {
"version": "3.3",
"version": "3.4",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
@ -13,17 +13,8 @@
"src/ApiResource/.gitignore"
]
},
"dama/doctrine-test-bundle": {
"version": "8.2",
"recipe": {
"repo": "github.com/symfony/recipes-contrib",
"branch": "main",
"version": "7.2",
"ref": "896306d79d4ee143af9eadf9b09fd34a8c391b70"
}
},
"doctrine/doctrine-bundle": {
"version": "2.12",
"version": "2.13",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
@ -36,18 +27,6 @@
"src/Repository/.gitignore"
]
},
"doctrine/doctrine-fixtures-bundle": {
"version": "3.6",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "3.0",
"ref": "1f5514cfa15b947298df4d771e694e578d4c204d"
},
"files": [
"src/DataFixtures/AppFixtures.php"
]
},
"doctrine/doctrine-migrations-bundle": {
"version": "3.3",
"recipe": {
@ -62,16 +41,21 @@
]
},
"gesdinet/jwt-refresh-token-bundle": {
"version": "1.3",
"version": "1.4",
"recipe": {
"repo": "github.com/symfony/recipes-contrib",
"branch": "main",
"version": "1.0",
"ref": "2390b4ed5c195e0b3f6dea45221f3b7c0af523a0"
}
},
"files": [
"config/packages/gesdinet_jwt_refresh_token.yaml",
"config/routes/gesdinet_jwt_refresh_token.yaml",
"src/Entity/RefreshToken.php"
]
},
"lexik/jwt-authentication-bundle": {
"version": "3.0",
"version": "3.1",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
@ -83,7 +67,7 @@
]
},
"nelmio/cors-bundle": {
"version": "2.4",
"version": "2.5",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
@ -94,20 +78,6 @@
"config/packages/nelmio_cors.yaml"
]
},
"phpunit/phpunit": {
"version": "9.6",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "9.6",
"ref": "7364a21d87e658eb363c5020c072ecfdc12e2326"
},
"files": [
".env.test",
"phpunit.xml.dist",
"tests/bootstrap.php"
]
},
"ramsey/uuid-doctrine": {
"version": "2.1",
"recipe": {
@ -118,13 +88,16 @@
}
},
"stof/doctrine-extensions-bundle": {
"version": "1.11",
"version": "1.12",
"recipe": {
"repo": "github.com/symfony/recipes-contrib",
"branch": "main",
"version": "1.2",
"ref": "e805aba9eff5372e2d149a9ff56566769e22819d"
}
},
"files": [
"config/packages/stof_doctrine_extensions.yaml"
]
},
"symfony/console": {
"version": "6.4",
@ -143,11 +116,12 @@
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "1.0",
"ref": "146251ae39e06a95be0fe3d13c807bcf3938b172"
"version": "2.4",
"ref": "52e9754527a15e2b79d9a610f98185a1fe46622a"
},
"files": [
".env"
".env",
".env.dev"
]
},
"symfony/framework-bundle": {
@ -156,7 +130,7 @@
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "6.4",
"ref": "a91c965766ad3ff2ae15981801643330eb42b6a5"
"ref": "32126346f25e1cee607cc4aa6783d46034920554"
},
"files": [
"config/packages/cache.yaml",
@ -169,15 +143,6 @@
"src/Kernel.php"
]
},
"symfony/maker-bundle": {
"version": "1.60",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "1.0",
"ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f"
}
},
"symfony/monolog-bundle": {
"version": "3.10",
"recipe": {
@ -190,21 +155,6 @@
"config/packages/monolog.yaml"
]
},
"symfony/phpunit-bridge": {
"version": "7.1",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "6.3",
"ref": "a411a0480041243d97382cac7984f7dce7813c08"
},
"files": [
".env.test",
"bin/phpunit",
"phpunit.xml.dist",
"tests/bootstrap.php"
]
},
"symfony/routing": {
"version": "6.4",
"recipe": {
@ -268,30 +218,5 @@
"files": [
"config/packages/validator.yaml"
]
},
"symfony/web-profiler-bundle": {
"version": "6.4",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "6.1",
"ref": "e42b3f0177df239add25373083a564e5ead4e13a"
},
"files": [
"config/packages/web_profiler.yaml",
"config/routes/web_profiler.yaml"
]
},
"zenstruck/foundry": {
"version": "1.38",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "1.10",
"ref": "37c2f894cc098ab4c08874b80cccc8e2f8de7976"
},
"files": [
"config/packages/zenstruck_foundry.yaml"
]
}
}

View File

@ -0,0 +1,200 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Menú de Opciones</title>
<style>
.navbar {
color: white;
padding: 15px 30px;
text-align: center;
top: 0;
width: 100%;
z-index: 1000;
}
.navbar img {
width: auto;
position: absolute; /* Logo en la izquierda */
left: 30px; /* Ajuste de la distancia desde la izquierda */
}
.navbar h1 {
margin: 0;
font-size: 1.8em;
}
.navbar a {
color: white;
text-decoration: none;
padding: 5px 10px;
margin-left: 10px;
}
.container {
margin: 80px auto 0; /* Deja espacio para la barra de navegación */
width: 90%;
max-width: 1000px;
text-align: center; /* Centra los elementos en navegadores básicos */
}
.menu-container {
display: block;
background-color: #f9f9f9;
border: 1px solid #ddd;
border-radius: 8px;
width: 100%;
margin-top: 20px;
}
.menu-item {
background-color: #f0f8ff; /* Fondo suave */
padding: 15px;
margin: 15px 0; /* Espaciado entre cajas */
border: 1px solid #ddd;
border-radius: 5px;
display: inline-block;
width: 45%; /* Dos elementos por fila */
box-sizing: border-box;
}
.menu-item a {
font-size: 1.2em;
color: #007bff;
text-decoration: none;
font-weight: bold;
}
.menu-item p {
font-size: 0.9em;
color: #555;
}
@media (max-width: 768px) {
.menu-item {
width: 100%; /* En pantallas pequeñas, las cajas ocupan todo el ancho */
}
}
.windows {
background-color: #e0f7fa; /* Azul suave para Windows */
}
.linux {
background-color: #c8e6c9; /* Verde suave para Linux */
}
.apagar {
background-color: #ffccbc; /* Naranja suave para Apagar */
}
.partition-container {
margin-top: 20px;
padding: 20px;
background-color: #f9f9f9;
border: 1px solid #ddd;
border-radius: 8px;
}
.partition-header {
font-size: 1.5em;
margin-bottom: 15px;
color: #333;
}
.partition-item {
display: flex;
flex-wrap: wrap; /* Permite adaptarse a pantallas pequeñas */
align-items: center;
text-align: left; /* Asegura que el contenido está alineado a la izquierda */
padding: 10px;
margin-bottom: 10px;
background-color: #ffffff;
border: 1px solid #ddd;
border-radius: 5px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.partition-item span {
margin-right: 15px;
font-size: 1em;
color: #555;
}
.partition-link {
font-size: 0.9em;
color: #007bff;
text-decoration: none;
font-weight: bold;
}
.partition-link:hover {
text-decoration: underline;
color: #0056b3;
}
@media (max-width: 768px) {
.partition-item {
flex-direction: column;
}
.partition-item span {
margin-bottom: 5px;
}
.partition-link {
margin-top: 10px;
}
}
</style>
</head>
<body>
<!-- Barra de navegación superior -->
<div class="navbar">
<img src="{{ asset('images/img.png') }}" alt="Logo">
</div>
<div class="container">
<div class="menu-container">
<h1>Bienvenido {{ ip }}</h1>
<div class="menu-item windows">
<a href="command+confirm:restoreImage REPO windows 1 1">Instalar Windows</a>
<p>El proceso de instalación tardará unos minutos.</p>
</div>
<div class="menu-item linux">
<a href="command+output+confirm:restoreImage REPO linux 1 2">Instalar GNU/Linux</a>
<p>El proceso de instalación tardará unos minutos.</p>
</div>
<div class="menu-item apagar">
<a href="command:poweroff">Apagar</a>
<p>Apagar el ordenador.</p>
</div>
<div class="menu-item apagar">
<a href="command:reboot">Reiniciar</a>
<p>Reiniciar el ordenador.</p>
</div>
</div>
<div class="partition-container">
<h2 class="partition-header">Particiones del sistema</h2>
{% if partitions|length > 0 %}
{% for partition in partitions %}
<div class="partition-item">
<span><strong>Disco:</strong> {{ partition.diskNumber }} </span>
<span><strong>Partición:</strong> {{ partition.partitionNumber }}</span>
<span>Tamaño: {{ (partition.size / 1024)|number_format(2) }} MB</span>
<span>Tipo: {{ partition.filesystem }}</span>
<span><strong>SO:</strong> {{ partition.operativeSystem ? partition.operativeSystem.name : '-' }}</span>
{% if partition.operativeSystem %}
<a href="command+output:bootOs {{ partition.diskNumber }} {{ partition.partitionNumber }}" class="partition-link">Arrancar {{ partition.operativeSystem.name }}</a>
{% endif %}
</div>
{% endfor %}
{% else %}
<p>No hay particiones disponibles.</p>
{% endif %}
</div>
</div>
</body>
</html>

View File

@ -65,7 +65,6 @@ class MenuTest extends AbstractTest
$this->createClientWithCredentials()->request('POST', '/menus',['json' => [
'name' => self::MENU_CREATE,
'title' => self::MENU_CREATE,
'resolution' => "1920x1080",
]]);
@ -75,7 +74,6 @@ class MenuTest extends AbstractTest
'@context' => '/contexts/MenuOutput',
'@type' => 'Menu',
'name' => self::MENU_CREATE,
'title' => self::MENU_CREATE,
]);
}
@ -90,7 +88,7 @@ class MenuTest extends AbstractTest
{
UserFactory::createOne(['username' => self::USER_ADMIN, 'roles'=> [UserGroupPermissions::ROLE_SUPER_ADMIN]]);
MenuFactory::createOne(['name' => self::MENU_UPDATE, 'title' => self::MENU_UPDATE, 'resolution' => "1920x1080"]);
MenuFactory::createOne(['name' => self::MENU_UPDATE, 'resolution' => "1920x1080"]);
$iri = $this->findIriBy(Menu::class, ['name' => self::MENU_UPDATE]);
$this->createClientWithCredentials()->request('PUT', $iri, ['json' => [
@ -117,7 +115,7 @@ class MenuTest extends AbstractTest
{
UserFactory::createOne(['username' => self::USER_ADMIN, 'roles'=> [UserGroupPermissions::ROLE_SUPER_ADMIN]]);
MenuFactory::createOne(['name' => self::MENU_DELETE, 'title' => self::MENU_DELETE, 'resolution' => "1920x1080"]);
MenuFactory::createOne(['name' => self::MENU_DELETE, 'resolution' => "1920x1080"]);
$iri = $this->findIriBy(Menu::class, ['name' => self::MENU_DELETE]);
$this->createClientWithCredentials()->request('DELETE', $iri);

View File

@ -61,8 +61,8 @@ class OgLiveTest extends AbstractTest
UserFactory::createOne(['username' => self::USER_ADMIN, 'roles'=> [UserGroupPermissions::ROLE_SUPER_ADMIN]]);
$this->createClientWithCredentials()->request('POST', '/og-lives',['json' => [
'name' => self::OGLIVE_CREATE,
'downloadUrl' => 'http://example.com',
'filename' => self::OGLIVE_CREATE,
'downloadUrl' => self::OGLIVE_CREATE
]]);
$this->assertResponseStatusCodeSame(201);
@ -70,8 +70,7 @@ class OgLiveTest extends AbstractTest
$this->assertJsonContains([
'@context' => '/contexts/OgLiveOutput',
'@type' => 'OgLive',
'name' => self::OGLIVE_CREATE,
'downloadUrl' => 'http://example.com',
'filename' => self::OGLIVE_CREATE,
'status' => OgLiveStatus::INACTIVE
]);
}
@ -87,19 +86,17 @@ class OgLiveTest extends AbstractTest
{
UserFactory::createOne(['username' => self::USER_ADMIN, 'roles'=> [UserGroupPermissions::ROLE_SUPER_ADMIN]]);
OgLiveFactory::createOne(['name' => self::OGLIVE_CREATE, 'downloadUrl' => 'http://example.com']);
$iri = $this->findIriBy(OgLive::class, ['name' => self::OGLIVE_CREATE]);
OgLiveFactory::createOne(['filename' => self::OGLIVE_CREATE, 'downloadUrl' => self::OGLIVE_UPDATE]);
$iri = $this->findIriBy(OgLive::class, ['filename' => self::OGLIVE_CREATE]);
$this->createClientWithCredentials()->request('PUT', $iri, ['json' => [
'name' => self::OGLIVE_UPDATE,
'downloadUrl' => 'http://example-2.com',
'filename' => self::OGLIVE_UPDATE,
]]);
$this->assertResponseIsSuccessful();
$this->assertJsonContains([
'@id' => $iri,
'name' => self::OGLIVE_UPDATE,
'downloadUrl' => 'http://example-2.com',
'filename' => self::OGLIVE_UPDATE,
]);
}
@ -114,13 +111,13 @@ class OgLiveTest extends AbstractTest
{
UserFactory::createOne(['username' => self::USER_ADMIN, 'roles'=> [UserGroupPermissions::ROLE_SUPER_ADMIN]]);
OgLiveFactory::createOne(['name' => self::OGLIVE_CREATE, 'downloadUrl' => 'http://example.com']);
$iri = $this->findIriBy(OgLive::class, ['name' => self::OGLIVE_CREATE]);
OgLiveFactory::createOne(['filename' => self::OGLIVE_CREATE, 'downloadUrl' => 'http://example.com']);
$iri = $this->findIriBy(OgLive::class, ['filename' => self::OGLIVE_CREATE]);
$this->createClientWithCredentials()->request('DELETE', $iri);
$this->assertResponseStatusCodeSame(204);
$this->assertNull(
static::getContainer()->get('doctrine')->getRepository(OgLive::class)->findOneBy(['name' => self::OGLIVE_CREATE])
static::getContainer()->get('doctrine')->getRepository(OgLive::class)->findOneBy(['filename' => self::OGLIVE_CREATE])
);
}
}

View File

@ -52,7 +52,7 @@ validators:
not_blank: 'The name should not be blank.'
og_live:
name:
filename:
not_blank: 'The name should not be blank.'
unique: 'The name should be unique.'

View File

@ -39,6 +39,11 @@ validators:
not_blank: 'El nombre no debería estar vacío.'
unique: 'El nombre debería ser único. Ya existe una imagen con ese nombre.'
og_live:
filename:
not_blank: 'El nombre no debería estar vacío.'
unique: 'El nombre debería ser único. Ya existe un archivo con ese nombre.'
network_settings:
ip_address:
invalid: 'La dirección IP "{{ value }}" no es válida.'