New version 1.1.0
parent
55e02b6400
commit
44d7bb5df9
|
@ -1,4 +1,13 @@
|
|||
# Changelog
|
||||
## [1.1.0] - 2025-10-16
|
||||
### Added
|
||||
- Se ha añadido un nuevo campo en el cliente, para guardar la resolucion del menu browser.
|
||||
- Se ha añadido un validador en el asistente "deploy", el cual comprueba que el tamaño de la partition destino de todos los clientes sea igual.
|
||||
|
||||
### Fixed
|
||||
- Se ha corregido un bug a la hora de mover clientes cuando el aula destino no tiene plantilla PXE asignada.
|
||||
|
||||
---
|
||||
## [1.0.0] - 2025-10-09
|
||||
### Added
|
||||
- Se ha añadido nuevo readme
|
||||
|
|
|
@ -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 Version20251015080216 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 ADD resolution VARCHAR(255) DEFAULT 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 resolution');
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
@ -25,7 +25,7 @@ class LoadDefaultMenuCommand extends Command
|
|||
{
|
||||
$menu = new Menu();
|
||||
$menu->setName('Default menu');
|
||||
$menu->setResolution('1920x1080');
|
||||
$menu->setResolution('791');
|
||||
$menu->setComments('Default menu comments');
|
||||
$menu->setPublicUrl('main');
|
||||
$menu->setIsDefault(true);
|
||||
|
|
|
@ -4,18 +4,16 @@ namespace App\Controller;
|
|||
|
||||
use App\Controller\OgBoot\PxeBootFile\PostAction;
|
||||
use App\Dto\Input\ChangeOrganizationalUnitInput;
|
||||
use App\Dto\Output\ClientOutput;
|
||||
use App\Entity\Client;
|
||||
use App\Repository\ClientRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
|
||||
class ChangeOrganizationalUnitAction extends AbstractController
|
||||
{
|
||||
|
@ -43,10 +41,10 @@ class ChangeOrganizationalUnitAction extends AbstractController
|
|||
|
||||
$this->entityManager->persist($clientEntity);
|
||||
|
||||
$template = $clientEntity->getTemplate() ?? $clientEntity->getOrganizationalUnit()->getNetworkSettings()?->getTemplate();
|
||||
$template = $clientEntity->getTemplate() ?? $clientEntity->getOrganizationalUnit()->getNetworkSettings()?->getPxeTemplate();
|
||||
|
||||
if (!$template) {
|
||||
throw new BadRequestHttpException('No template found for client');
|
||||
throw new BadRequestHttpException('No se han encontrado plantillas PXE asociadas al cliente o a la unidad organizativa.');
|
||||
}
|
||||
|
||||
$this->postAction->__invoke($clientEntity, $template);
|
||||
|
@ -54,6 +52,6 @@ class ChangeOrganizationalUnitAction extends AbstractController
|
|||
|
||||
$this->entityManager->flush();
|
||||
|
||||
return new JsonResponse( data: 'Clients updated successfully', status: Response::HTTP_OK);
|
||||
return new JsonResponse( data: 'Clientes actualizados correctamente', status: Response::HTTP_OK);
|
||||
}
|
||||
}
|
|
@ -58,7 +58,7 @@ class PostAction extends AbstractOgBootController
|
|||
'ogntp' => $client->getOrganizationalUnit()->getNetworkSettings()?->getNtp(),
|
||||
'ogdns' => $client->getOrganizationalUnit()->getNetworkSettings()?->getDns(),
|
||||
'ogProxy' => $client->getOrganizationalUnit()->getNetworkSettings()?->getProxy(),
|
||||
'resolution' => '791'
|
||||
'resolution' => $client->getResolution() ?? '791'
|
||||
]
|
||||
];
|
||||
|
||||
|
|
|
@ -101,16 +101,22 @@ final class ClientInput
|
|||
|
||||
#[Groups(['client:write'])]
|
||||
#[ApiProperty(
|
||||
description: 'descriptions.client.validation'
|
||||
description: 'El repositorio del cliente'
|
||||
)]
|
||||
public ?ImageRepositoryOutput $repository = null;
|
||||
|
||||
#[Groups(['client:write'])]
|
||||
#[ApiProperty(
|
||||
description: 'descriptions.client.validation'
|
||||
description: 'El mantenimiento del cliente'
|
||||
)]
|
||||
public ?bool $maintenance = false;
|
||||
|
||||
#[Groups(['client:write'])]
|
||||
#[ApiProperty(
|
||||
description: 'La resolución del cliente'
|
||||
)]
|
||||
public ?string $resolution = null;
|
||||
|
||||
|
||||
public function __construct(?Client $client = null)
|
||||
{
|
||||
|
@ -127,6 +133,7 @@ final class ClientInput
|
|||
$this->ip = $client->getIp();
|
||||
$this->position = $client->getPosition();
|
||||
$this->status = $client->getStatus();
|
||||
$this->resolution = $client->getResolution();
|
||||
|
||||
if ($client->getMenu()) {
|
||||
$this->menu = new MenuOutput($client->getMenu());
|
||||
|
@ -170,7 +177,8 @@ final class ClientInput
|
|||
$client->setPosition($this->position);
|
||||
$client->setStatus($this->status);
|
||||
$client->setMaintenance($this->maintenance);
|
||||
|
||||
$client->setResolution($this->resolution);
|
||||
|
||||
return $client;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,10 +6,10 @@ namespace App\Dto\Input;
|
|||
|
||||
use ApiPlatform\Metadata\ApiProperty;
|
||||
use App\Dto\Output\ClientOutput;
|
||||
use App\Validator\Constraints\ClientsHaveSamePartitionCount;
|
||||
use App\Validator\Constraints\ClientsHaveSamePartitionSize;
|
||||
use Symfony\Component\Serializer\Annotation\Groups;
|
||||
|
||||
#[ClientsHaveSamePartitionCount]
|
||||
#[ClientsHaveSamePartitionSize]
|
||||
class DeployGitImageInput
|
||||
{
|
||||
#[Groups(['git-repository:write'])]
|
||||
|
|
|
@ -4,13 +4,13 @@ namespace App\Dto\Input;
|
|||
|
||||
use ApiPlatform\Metadata\ApiProperty;
|
||||
use App\Dto\Output\ClientOutput;
|
||||
use App\Validator\Constraints\ClientsHaveSamePartitionCount;
|
||||
use App\Validator\Constraints\ClientsHaveSamePartitionSize;
|
||||
use App\Validator\Constraints\OrganizationalUnitMulticastMode;
|
||||
use App\Validator\Constraints\OrganizationalUnitMulticastPort;
|
||||
use App\Validator\Constraints\OrganizationalUnitP2PMode;
|
||||
use Symfony\Component\Serializer\Annotation\Groups;
|
||||
|
||||
#[ClientsHaveSamePartitionCount]
|
||||
#[ClientsHaveSamePartitionSize]
|
||||
class DeployImageInput
|
||||
{
|
||||
#[Groups(['image-image-repository:write'])]
|
||||
|
|
|
@ -14,6 +14,8 @@ use Symfony\Component\Validator\Constraints as Assert;
|
|||
#[OrganizationalUnitParent]
|
||||
class OrganizationalUnitInput
|
||||
{
|
||||
private ?OrganizationalUnit $originalEntity = null;
|
||||
|
||||
#[Assert\NotBlank(message: 'validators.organizational_unit.name.not_blank')]
|
||||
#[Groups(['organizational-unit:write'])]
|
||||
#[ApiProperty(
|
||||
|
@ -113,6 +115,7 @@ class OrganizationalUnitInput
|
|||
return;
|
||||
}
|
||||
|
||||
$this->originalEntity = $organizationalUnit;
|
||||
$this->name = $organizationalUnit->getName();
|
||||
if ($organizationalUnit->getParent()) {
|
||||
$this->parent = new OrganizationalUnitOutput($organizationalUnit->getParent());
|
||||
|
@ -164,4 +167,9 @@ class OrganizationalUnitInput
|
|||
|
||||
return $organizationalUnit;
|
||||
}
|
||||
|
||||
public function getOriginalEntity(): ?OrganizationalUnit
|
||||
{
|
||||
return $this->originalEntity;
|
||||
}
|
||||
}
|
|
@ -160,6 +160,13 @@ final class ClientOutput extends AbstractOutput
|
|||
)]
|
||||
public ?bool $pxeSync = false;
|
||||
|
||||
#[Groups(['client:read'])]
|
||||
#[ApiProperty(
|
||||
description: 'La resolución del cliente',
|
||||
example: '1920x1080'
|
||||
)]
|
||||
public ?string $resolution = null;
|
||||
|
||||
public function __construct(Client $client)
|
||||
{
|
||||
parent::__construct($client);
|
||||
|
@ -208,6 +215,7 @@ final class ClientOutput extends AbstractOutput
|
|||
$this->createdBy = $client->getCreatedBy();
|
||||
$this->maintenance = $client->isMaintenance();
|
||||
$this->pxeSync = $client->isPxeSync();
|
||||
$this->resolution = $client->getResolution();
|
||||
}
|
||||
|
||||
public function convertMaskToCIDR($mask): int
|
||||
|
|
|
@ -11,14 +11,14 @@ use Symfony\Component\Serializer\Annotation\Groups;
|
|||
#[Get(shortName: 'Menu')]
|
||||
final class MenuOutput extends AbstractOutput
|
||||
{
|
||||
#[Groups(['menu:read', 'organizational-unit:read'])]
|
||||
#[Groups(['menu:read', 'client:read', 'organizational-unit:read'])]
|
||||
#[ApiProperty(
|
||||
description: 'El nombre del menú de arranque',
|
||||
example: 'Menú Principal'
|
||||
)]
|
||||
public string $name;
|
||||
|
||||
#[Groups(['menu:read'])]
|
||||
#[Groups(['menu:read', 'client:read', 'organizational-unit:read'])]
|
||||
#[ApiProperty(
|
||||
description: 'La resolución del menú',
|
||||
example: '1024x768'
|
||||
|
|
|
@ -94,6 +94,9 @@ class Client extends AbstractEntity
|
|||
#[ORM\Column(length: 255, nullable: true)]
|
||||
private ?string $token = null;
|
||||
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
private ?string $resolution = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
|
@ -371,4 +374,15 @@ class Client extends AbstractEntity
|
|||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getResolution(): ?string
|
||||
{
|
||||
return $this->resolution;
|
||||
}
|
||||
|
||||
public function setResolution(?string $resolution): static
|
||||
{
|
||||
$this->resolution = $resolution;
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -75,6 +75,7 @@ readonly class ClientProcessor implements ProcessorInterface
|
|||
|
||||
if ($defaultMenu && !$client->getMenu()) {
|
||||
$client->setMenu($defaultMenu);
|
||||
$client->setResolution($defaultMenu->getResolution());
|
||||
}
|
||||
|
||||
if ($defaultPxe && !$client->getTemplate()) {
|
||||
|
|
|
@ -1,39 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Validator\Constraints;
|
||||
|
||||
use App\Dto\Input\DeployImageInput;
|
||||
use App\Dto\Output\ClientOutput;
|
||||
use App\Entity\Client;
|
||||
use Symfony\Component\Validator\ConstraintValidator;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
|
||||
class ClientsHaveSamePartitionCountValidator extends ConstraintValidator
|
||||
{
|
||||
public function validate($value, Constraint $constraint): void
|
||||
{
|
||||
if (!$value instanceof DeployImageInput) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isset($value->clients) && is_array($value->clients)) {
|
||||
$partitionCounts = [];
|
||||
foreach ($value->clients as $client) {
|
||||
$partitionCount = $client->getEntity()->getPartitions()->count();
|
||||
$partitionCounts[(string) $client->getEntity()->getIp()] = $partitionCount;
|
||||
}
|
||||
|
||||
if (count(array_unique($partitionCounts)) > 1) {
|
||||
$errorDetails = [];
|
||||
foreach ($partitionCounts as $clientIp => $partitionCount) {
|
||||
$errorDetails[] = "Cliente $clientIp tiene $partitionCount particiones.";
|
||||
}
|
||||
|
||||
$detailedMessage = implode(" ", $errorDetails);
|
||||
|
||||
$this->context->buildViolation($constraint->message . ' Detalles: ' . $detailedMessage)
|
||||
->addViolation();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,7 +5,7 @@ namespace App\Validator\Constraints;
|
|||
use Symfony\Component\Validator\Constraint;
|
||||
|
||||
#[\Attribute]
|
||||
class ClientsHaveSamePartitionCount extends Constraint
|
||||
class ClientsHaveSamePartitionSize extends Constraint
|
||||
{
|
||||
public string $message;
|
||||
|
||||
|
@ -13,7 +13,7 @@ class ClientsHaveSamePartitionCount extends Constraint
|
|||
{
|
||||
parent::__construct($options, $groups, $payload);
|
||||
|
||||
$this->message = 'All clients must have the same number of partitions.';
|
||||
$this->message = 'Todos los clientes deben tener el mismo tamaño de partición para la partición seleccionada.';
|
||||
}
|
||||
|
||||
public function getTargets(): string
|
|
@ -0,0 +1,76 @@
|
|||
<?php
|
||||
|
||||
namespace App\Validator\Constraints;
|
||||
|
||||
use App\Dto\Input\DeployImageInput;
|
||||
use App\Dto\Input\DeployGitImageInput;
|
||||
use App\Dto\Output\ClientOutput;
|
||||
use App\Entity\Client;
|
||||
use Symfony\Component\Validator\ConstraintValidator;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
|
||||
class ClientsHaveSamePartitionSizeValidator extends ConstraintValidator
|
||||
{
|
||||
public function validate($value, Constraint $constraint): void
|
||||
{
|
||||
if (!$value instanceof DeployImageInput && !$value instanceof DeployGitImageInput) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isset($value->clients) || !is_array($value->clients) || empty($value->clients)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($value->diskNumber === null || $value->partitionNumber === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$partitionSizes = [];
|
||||
|
||||
foreach ($value->clients as $client) {
|
||||
$clientEntity = $client->getEntity();
|
||||
|
||||
if (!$clientEntity instanceof Client) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$clientIp = (string) $clientEntity->getIp();
|
||||
|
||||
$targetPartition = null;
|
||||
foreach ($clientEntity->getPartitions() as $partition) {
|
||||
if ($partition->getDiskNumber() === $value->diskNumber &&
|
||||
$partition->getPartitionNumber() === $value->partitionNumber) {
|
||||
$targetPartition = $partition;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($targetPartition === null) {
|
||||
$partitionSizes[$clientIp] = null;
|
||||
} else {
|
||||
$partitionSizes[$clientIp] = $targetPartition->getSize();
|
||||
}
|
||||
}
|
||||
|
||||
$validSizes = array_filter($partitionSizes, fn($size) => $size !== null);
|
||||
|
||||
if (count($validSizes) !== count($partitionSizes)) {
|
||||
$clientsWithoutPartition = array_keys(array_filter($partitionSizes, fn($size) => $size === null));
|
||||
$this->context->buildViolation('Algunos clientes no tienen la partición seleccionada (Disco: ' . $value->diskNumber . ', Partición: ' . $value->partitionNumber . '). Clientes sin partición: ' . implode(', ', $clientsWithoutPartition))
|
||||
->addViolation();
|
||||
return;
|
||||
}
|
||||
|
||||
if (count(array_unique($validSizes)) > 1) {
|
||||
$errorDetails = [];
|
||||
foreach ($partitionSizes as $clientIp => $partitionSize) {
|
||||
$errorDetails[] = "Cliente $clientIp tiene tamaño $partitionSize KB.";
|
||||
}
|
||||
|
||||
$detailedMessage = implode(" ", $errorDetails);
|
||||
|
||||
$this->context->buildViolation($constraint->message . ' Detalles: ' . $detailedMessage)
|
||||
->addViolation();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -9,12 +9,14 @@ use Symfony\Component\Validator\Constraint;
|
|||
class OrganizationalUnitParent extends Constraint
|
||||
{
|
||||
public string $message;
|
||||
public string $selfParentMessage;
|
||||
|
||||
public function __construct(mixed $options = null, ?array $groups = null, mixed $payload = null)
|
||||
{
|
||||
parent::__construct($options, $groups, $payload);
|
||||
|
||||
$this->message = 'Only the root organizational unit can not have a parent.';
|
||||
$this->message = 'validators.organizational_unit.parent.required';
|
||||
$this->selfParentMessage = 'validators.organizational_unit.parent.self_parent';
|
||||
}
|
||||
|
||||
public function getTargets(): array|string
|
||||
|
|
|
@ -21,5 +21,14 @@ class OrganizationalUnitParentValidator extends ConstraintValidator
|
|||
$this->context->buildViolation($constraint->message)->addViolation();
|
||||
return;
|
||||
}
|
||||
|
||||
// Validar que el parent no sea la misma entidad que se está modificando
|
||||
if ($value->parent && $value->getOriginalEntity()) {
|
||||
if ($value->parent->getEntity()->getId() === $value->getOriginalEntity()->getId()) {
|
||||
$this->context->buildViolation($constraint->selfParentMessage)
|
||||
->atPath('parent')
|
||||
->addViolation();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -57,6 +57,11 @@ validators:
|
|||
not_blank: 'The name should not be blank.'
|
||||
unique: 'The name should be unique.'
|
||||
|
||||
|
||||
organizational_unit:
|
||||
parent:
|
||||
self_parent: 'The parent should not be the same as the organizational unit.'
|
||||
required: 'The parent should be required.'
|
||||
subnet:
|
||||
name:
|
||||
not_blank: 'The name should not be blank.'
|
||||
|
|
|
@ -45,6 +45,11 @@ validators:
|
|||
not_blank: 'El nombre no debería estar vacío.'
|
||||
unique: 'El nombre debería ser único. Ya existe un archivo con ese nombre.'
|
||||
|
||||
organizational_unit:
|
||||
parent:
|
||||
self_parent: 'El padre no debería ser la misma unidad organizativa.'
|
||||
required: 'El padre debería ser requerido.'
|
||||
|
||||
network_settings:
|
||||
ip_address:
|
||||
invalid: 'La dirección IP "{{ value }}" no es válida.'
|
||||
|
|
Loading…
Reference in New Issue