Merge pull request 'New version 1.1.0' (#68) from develop into main
testing/ogcore-api/pipeline/head This commit looks good Details
ogcore-debian-package/pipeline/head This commit looks good Details
ogcore-debian-package/pipeline/tag This commit looks good Details

Reviewed-on: #68
main 1.1.0
Manuel Aranda Rosales 2025-10-16 12:40:49 +02:00
commit 7047ed7595
23 changed files with 1717 additions and 1099 deletions

View File

@ -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

1412
README.md

File diff suppressed because it is too large Load Diff

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 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.

View File

@ -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);

View File

@ -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);
}
}

View File

@ -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'
]
];

View File

@ -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;
}
}

View File

@ -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'])]

View File

@ -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'])]

View File

@ -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;
}
}

View File

@ -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

View File

@ -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'

View File

@ -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;
}
}

View File

@ -75,6 +75,7 @@ readonly class ClientProcessor implements ProcessorInterface
if ($defaultMenu && !$client->getMenu()) {
$client->setMenu($defaultMenu);
$client->setResolution($defaultMenu->getResolution());
}
if ($defaultPxe && !$client->getTemplate()) {

View File

@ -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();
}
}
}
}

View File

@ -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

View File

@ -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();
}
}
}

View File

@ -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

View File

@ -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();
}
}
}
}

View File

@ -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.'

View File

@ -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.'