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

Reviewed-on: #27
pull/29/head^2 0.11.0
Manuel Aranda Rosales 2025-04-11 12:00:09 +02:00
commit 351f952cba
74 changed files with 1369 additions and 147 deletions

View File

@ -1,4 +1,19 @@
# Changelog
## [0.11.0] - 2025-04-11
### Added
- Se ha añadido funcionalidad para renombrar imagenes en ogRepository. Nuevo sistema de versionado.
- Se ha añadido la integracion con el ogAgent para poder ejecutar scripts.
- Se ha añadido el poder añadir descripcion a una imagen.
- Se han añadido 2 nuevos campos en la gestion de los repositorios: usuario y puerto ssh.
- Se ha añadido funcionalidad para poder gestionar el estado de un equipo de manera automatica. En caso de no haber conexion con el cliente, la web sera notificada en un tiempo maximo de 5 min.
### Improved
- Se han modificado los logs para que puedan "salir" por syslog ademas de por fichero.
### Fixed
- Se ha corregido el bug que hacia que cuando habia demasiados clientes, no se mostraran en pantalla debido a un error de memoria.
---
## [0.10.1] - 2025-03-25
### Improved
- Se ha modificado el script de creación de usuarios, añadiendole la opcion del tipo de visionalizacion por defecto de la vista "grupos".

View File

@ -50,6 +50,13 @@ resources:
uriTemplate: /clients/server/{uuid}/get-pxe
controller: App\Controller\OgBoot\PxeBootFile\GetAction
login_client:
class: ApiPlatform\Metadata\Post
method: POST
input: App\Dto\Input\MultipleClientsInput
uriTemplate: /clients/server/login-client
controller: App\Controller\OgAgent\LoginAction
reboot_client:
class: ApiPlatform\Metadata\Post
method: POST

View File

@ -7,6 +7,8 @@ resources:
groups: ['default', 'command:read']
denormalizationContext:
groups: ['command:write']
order:
id: 'DESC'
operations:
ApiPlatform\Metadata\GetCollection:
provider: App\State\Provider\CommandProvider
@ -28,9 +30,8 @@ resources:
class: ApiPlatform\Metadata\Post
method: POST
input: App\Dto\Input\CommandExecuteInput
uriTemplate: /commands/{uuid}/execute
controller: App\Controller\CommandExecuteAction
uriTemplate: /commands/run-script
controller: App\Controller\OgAgent\RunScriptAction
properties:
App\Entity\Command:
id:

View File

@ -64,6 +64,14 @@ resources:
uriTemplate: /image-image-repositories/{uuid}/convert-image-to-virtual
controller: App\Controller\OgRepository\Image\ConvertImageToVirtualAction
rename_image_ogrepository:
shortName: OgRepository Server
class: ApiPlatform\Metadata\Post
method: POST
input: App\Dto\Input\RenameImageInput
uriTemplate: /image-image-repositories/{uuid}/rename-image
controller: App\Controller\OgRepository\Image\RenameAction
trash_delete_image_ogrepository:
shortName: OgRepository Server
description: Delete Image in OgRepository

View File

@ -15,6 +15,12 @@ when@dev:
type: console
process_psr_3_messages: false
channels: ["!event", "!doctrine", "!console"]
syslog:
type: syslog
ident: "ogcore"
level: info
formatter: App\Formatter\CustomLineFormatter
channels: ["!event"]
when@test:
monolog:
@ -42,6 +48,12 @@ when@prod:
type: console
process_psr_3_messages: false
channels: ["!event", "!doctrine"]
syslog:
type: syslog
ident: "ogcore"
level: info
formatter: App\Formatter\CustomLineFormatter
channels: ["!event"]
deprecation:
type: stream
channels: [deprecation]

View File

@ -42,7 +42,7 @@ services:
api_platform.filter.command.boolean:
parent: 'api_platform.doctrine.orm.boolean_filter'
arguments: [ { 'enabled': ~ } ]
arguments: [ { 'enabled': ~, 'readOnly': ~ } ]
tags: [ 'api_platform.filter' ]
api_platform.filter.hardware.order:

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 Version20250325075647 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 firmware_type 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 firmware_type');
}
}

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 Version20250326061450 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 command ADD parameters TINYINT(1) DEFAULT NULL');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE command DROP parameters');
}
}

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 Version20250331144522 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 image_image_repository ADD datasize 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 image_image_repository DROP datasize');
}
}

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 Version20250402060324 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 image ADD version INT DEFAULT NULL');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE image DROP version');
}
}

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 Version20250402081107 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_IMAGE_REPOSITORY ON image_image_repository');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE UNIQUE INDEX UNIQ_IDENTIFIER_IMAGE_REPOSITORY ON image_image_repository (image_id, repository_id)');
}
}

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 Version20250402094550 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 image_image_repository ADD version INT DEFAULT NULL');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE image_image_repository DROP version');
}
}

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 Version20250407063100 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 image_image_repository ADD description 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 image_image_repository DROP description');
}
}

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 Version20250407154425 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE INDEX IDX_STATUS ON client (status)');
$this->addSql('CREATE INDEX IDX_UPDATED_AT ON client (updated_at)');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('DROP INDEX IDX_STATUS ON client');
$this->addSql('DROP INDEX IDX_UPDATED_AT ON client');
}
}

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 Version20250407154620 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE INDEX IDX_STATUS_UPDATED_AT ON client (status, updated_at)');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('DROP INDEX IDX_STATUS_UPDATED_AT ON client');
}
}

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 Version20250408140101 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 image_repository ADD user VARCHAR(255) DEFAULT NULL, ADD ssh_port 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 image_repository DROP user, DROP ssh_port');
}
}

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 Version20250409093554 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 image_image_repository ADD name VARCHAR(255) NOT NULL');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE image_image_repository DROP name');
}
}

View File

@ -0,0 +1,98 @@
<?php
declare(strict_types=1);
namespace App\Command;
use App\Entity\Client;
use App\Entity\Trace;
use App\Model\ClientStatus;
use App\Model\TraceStatus;
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;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Mercure\HubInterface;
use Symfony\Component\Mercure\Update;
#[AsCommand(name: 'opengnsys:check-client-availability', description: 'Check client availability')]
class CheckClientAvailability extends Command
{
const int THRESHOLD_MINUTES = 3;
public function __construct(
private readonly HubInterface $hub,
private readonly EntityManagerInterface $entityManager
)
{
parent::__construct();
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$threshold = (new \DateTime())->modify(' - '.self::THRESHOLD_MINUTES . ' minutes');
$startQueryTime = microtime(true);
$query = $this->entityManager->createQuery(
'UPDATE App\Entity\Client c
SET c.status = :status
WHERE c.status = :currentStatus AND c.updatedAt < :threshold'
);
$query->setParameter('status', ClientStatus::DISCONNECTED);
$query->setParameter('currentStatus', ClientStatus::OG_LIVE);
$query->setParameter('threshold', $threshold);
$updatedCount = $query->execute();
$queryTime = microtime(true) - $startQueryTime;
$startMercureTime = microtime(true);
$clients = $this->entityManager->createQueryBuilder()
->select('c')
->from(Client::class, 'c')
->where('c.status = :status')
->andWhere('c.updatedAt < :threshold')
->setParameter('status', ClientStatus::DISCONNECTED)
->setParameter('threshold', $threshold)
->getQuery()
->getResult();
$this->dispatchMercureEvent($clients);
$mercureTime = microtime(true) - $startMercureTime;
$io->success("Updated $updatedCount clients to DISCONNECTED status.");
$io->note("Query time: " . round($queryTime, 3) . "s");
$io->note("Mercure dispatch time: " . round($mercureTime, 3) . "s");
return Command::SUCCESS;
}
private function dispatchMercureEvent(array $clients, int $chunkSize = 10000): void
{
$chunks = array_chunk($clients, $chunkSize);
foreach ($chunks as $chunk) {
$data = [];
foreach ($chunk as $client) {
$data[] = [
'@id' => '/clients/' . $client->getUuid(),
'status' => $client->getStatus(),
];
}
$update = new Update(
'clients',
json_encode($data)
);
$this->hub->publish($update);
}
}
}

View File

@ -1,44 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Command;
use App\Entity\Client;
use App\Entity\Trace;
use App\Model\ClientStatus;
use App\Model\TraceStatus;
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;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Mercure\HubInterface;
use Symfony\Component\Mercure\Update;
#[AsCommand(name: 'opengnsys:test', description: 'Hello PhpStorm')]
class TestCommand extends Command
{
public function __construct(
private readonly HubInterface $hub,
private readonly EntityManagerInterface $entityManager
)
{
parent::__construct();
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$trace = $this->entityManager->getRepository(Trace::class)->find(7236);
$trace->setStatus(TraceStatus::SUCCESS);
$trace->setProgress(1000);
$this->entityManager->persist($trace);
$this->entityManager->flush();
return Command::SUCCESS;
}
}

View File

@ -87,7 +87,7 @@ class DeployImageAction extends AbstractController
];
try {
$this->deployImageOgRepositoryAction->__invoke($input, $image, $client->getEntity(), $this->httpClient);
$this->deployImageOgRepositoryAction->__invoke($input, $image, $client->getEntity());
} catch (\Exception $e) {
continue;
}

View File

@ -8,6 +8,7 @@ use App\Entity\Client;
use App\Entity\Command;
use App\Entity\Image;
use App\Entity\ImageImageRepository;
use App\Entity\ImageRepository;
use App\Entity\Trace;
use App\Model\ClientStatus;
use App\Model\CommandTypes;
@ -56,10 +57,14 @@ class CreateImageAction extends AbstractController
$repository = $image->getClient()->getRepository();
$latestImageRepo = $this->entityManager->getRepository(ImageImageRepository::class)->findLatestVersionByImageAndRepository($image, $repository);
$imageImageRepository = new ImageImageRepository();
$imageImageRepository->setName($image->getName().'_v'.($latestImageRepo ? $latestImageRepo->getVersion() + 1 : 1));
$imageImageRepository->setImage($image);
$imageImageRepository->setRepository($repository);
$imageImageRepository->setStatus(ImageStatus::IN_PROGRESS);
$imageImageRepository->setVersion($latestImageRepo ? $latestImageRepo->getVersion() + 1 : 1);
$this->entityManager->persist($imageImageRepository);
@ -68,7 +73,7 @@ class CreateImageAction extends AbstractController
'par' => (string) $partitionInfo['numPartition'],
'cpt' => null,
'idi' => $imageImageRepository->getUuid(),
'nci' => $image->getName(),
'nci' => $image->getName().'_v'.$imageImageRepository->getVersion(),
'ipr' => $repository->getIp(),
'nfn' => 'CrearImagen',
'ids' => '0'
@ -90,7 +95,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', [
$response = $this->httpClient->request('POST', 'https://'.$image->getClient()->getIp().':8000/opengnsys/CrearImagen', [
'verify_peer' => false,
'verify_host' => false,
'headers' => [

View File

@ -92,7 +92,7 @@ class DeployImageAction extends AbstractController
];
try {
$response = $this->httpClient->request('POST', 'https://'.$client->getIp().':8000/CloningEngine/RestaurarImagen', [
$response = $this->httpClient->request('POST', 'https://'.$client->getIp().':8000/opengnsys/RestaurarImagen', [
'verify_peer' => false,
'verify_host' => false,
'headers' => [

View File

@ -0,0 +1,93 @@
<?php
declare(strict_types=1);
namespace App\Controller\OgAgent;
use App\Dto\Input\MultipleClientsInput;
use App\Entity\Client;
use App\Entity\Command;
use App\Entity\Image;
use App\Entity\Trace;
use App\Model\ClientStatus;
use App\Model\CommandTypes;
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;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Validator\Exception\ValidatorException;
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\Contracts\HttpClient\HttpClientInterface;
class LoginAction extends AbstractController
{
public function __construct(
protected readonly EntityManagerInterface $entityManager,
protected readonly HttpClientInterface $httpClient,
protected readonly CreateService $createService,
protected readonly LoggerInterface $logger,
)
{
}
public function __invoke(MultipleClientsInput $input): JsonResponse
{
foreach ($input->clients as $clientEntity) {
/** @var Client $client */
$client = $clientEntity->getEntity();
if (!$client->getIp()) {
throw new ValidatorException('IP is required');
}
if ($client->getStatus() !== ClientStatus::OG_LIVE) {
throw new ValidatorException('Client is not in OG_LIVE status');
}
$data = [
'nfn' => 'IniciarSesion',
'dsk' => '1',
'par' => '1',
'ids' => '0'
];
try {
$response = $this->httpClient->request('POST', 'https://' . $client->getIp() . ':8000/ogAdmClient/IniciarSesion', [
'verify_peer' => false,
'verify_host' => false,
'headers' => [
'Content-Type' => 'application/json',
],
'json' => $data,
]);
$this->logger->info('Login client', ['client' => $client->getId()]);
} catch (TransportExceptionInterface $e) {
$this->logger->error('Login rebooting client', ['client' => $client->getId(), 'error' => $e->getMessage()]);
return new JsonResponse(
data: ['error' => $e->getMessage()],
status: Response::HTTP_INTERNAL_SERVER_ERROR
);
}
$jobId = json_decode($response->getContent(), true)['job_id'];
$client->setStatus(ClientStatus::INITIALIZING);
$this->entityManager->persist($client);
$this->entityManager->flush();
$this->createService->__invoke($client, CommandTypes::REBOOT, TraceStatus::SUCCESS, $jobId, []);
}
return new JsonResponse(data: [], status: Response::HTTP_OK);
}
}

View File

@ -48,6 +48,7 @@ class PartitionAssistantAction extends AbstractController
}
foreach ($input->clients as $clientInput) {
/** @var Client $client */
$client = $clientInput->getEntity();
$disks = [];
@ -72,13 +73,10 @@ class PartitionAssistantAction extends AbstractController
];
}
if ($partition->filesystem === 'CACHE') {
$disks[$diskNumber]['diskData'] = [
'dis' => (string) $diskNumber,
'che' => "0",
'tch' => (string) ($partition->size * 1024),
];
}
$disks[$diskNumber]['diskData'] = [
'dis' => (string) $diskNumber,
'tch' => (string) ($partition->size * 1024),
];
$disks[$diskNumber]['partitionData'][] = [
'par' => (string) $partition->partitionNumber,
@ -104,7 +102,7 @@ class PartitionAssistantAction extends AbstractController
];
try {
$response = $this->httpClient->request('POST', 'https://' . $client->getIp() . ':8000/CloningEngine/Configurar', [
$response = $this->httpClient->request('POST', 'https://' . $client->getIp() . ':8000/opengnsys/Configurar', [
'verify_peer' => false,
'verify_host' => false,
'headers' => [
@ -130,5 +128,4 @@ class PartitionAssistantAction extends AbstractController
return new JsonResponse(data: [], status: Response::HTTP_OK);
}
}

View File

@ -53,13 +53,15 @@ class PowerOffAction extends AbstractController
continue;
}
$endpoint = $client->getStatus() === ClientStatus::OG_LIVE ? 'opengnsys/Apagar' : 'opengnsys/poweroff';
$data = [
'nfn' => 'Apagar',
'ids' => '0'
];
try {
$response = $this->httpClient->request('POST', 'https://'.$client->getIp().':8000/ogAdmClient/Apagar', [
$response = $this->httpClient->request('POST', 'https://' . $client->getIp() . ':8000/'.$endpoint, [
'verify_peer' => false,
'verify_host' => false,
'headers' => [

View File

@ -41,6 +41,7 @@ class RebootAction extends AbstractController
public function __invoke(MultipleClientsInput $input): JsonResponse
{
foreach ($input->clients as $clientEntity) {
/** @var Client $client */
$client = $clientEntity->getEntity();
@ -48,13 +49,15 @@ class RebootAction extends AbstractController
throw new ValidatorException('IP is required');
}
$endpoint = $client->getStatus() === ClientStatus::OG_LIVE ? 'opengnsys/Reiniciar' : '/opengnsys/reboot';
$data = [
'nfn' => 'Reiniciar',
'ids' => '0'
];
try {
$response = $this->httpClient->request('POST', 'https://' . $client->getIp() . ':8000/ogAdmClient/Reiniciar', [
$response = $this->httpClient->request('POST', 'https://' . $client->getIp() . ':8000/'.$endpoint, [
'verify_peer' => false,
'verify_host' => false,
'headers' => [
@ -62,6 +65,7 @@ class RebootAction extends AbstractController
],
'json' => $data,
]);
$this->logger->info('Rebooting client', ['client' => $client->getId()]);
} catch (TransportExceptionInterface $e) {

View File

@ -0,0 +1,81 @@
<?php
namespace App\Controller\OgAgent;
use App\Dto\Input\CommandExecuteInput;
use App\Dto\Input\MultipleClientsInput;
use App\Entity\Client;
use App\Model\ClientStatus;
use App\Model\CommandTypes;
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;
use Symfony\Component\Validator\Exception\ValidatorException;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
class RunScriptAction extends AbstractController
{
public function __construct(
protected readonly EntityManagerInterface $entityManager,
protected readonly HttpClientInterface $httpClient,
protected readonly CreateService $createService,
protected readonly LoggerInterface $logger,
)
{
}
public function __invoke(CommandExecuteInput $input): JsonResponse
{
/** @var Client $clientEntity */
foreach ($input->clients as $clientEntity) {
$client = $clientEntity->getEntity();
if (!$client->getIp()) {
throw new ValidatorException('IP is required');
}
$data = [
'nfn' => 'EjecutarScript',
'scp' => base64_encode($input->script),
'ids' => '0'
];
try {
$response = $this->httpClient->request('POST', 'https://' . $client->getIp() . ':8000/opengnsys/EjecutarScript', [
'verify_peer' => false,
'verify_host' => false,
'headers' => [
'Content-Type' => 'application/json',
],
'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
);
}
$jobId = json_decode($response->getContent(), true)['job_id'];
$this->entityManager->persist($client);
$this->entityManager->flush();
$inputData = [
'script' => $input->script,
];
$this->createService->__invoke($client, CommandTypes::RUN_SCRIPT, TraceStatus::SUCCESS, $jobId, $inputData);
}
return new JsonResponse(data: [], status: Response::HTTP_OK);
}
}

View File

@ -70,15 +70,19 @@ class StatusAction extends AbstractController
{
$this->logger->info('Checking client status', ['client' => $client->getId()]);
$params = [
'full-config' => false,
];
try {
$response = $this->httpClient->request('POST', 'https://' . $client->getIp() . ':8000/ogAdmClient/status', [
$response = $this->httpClient->request('POST', 'https://' . $client->getIp() . ':8000/opengnsys/status', [
'verify_peer' => false,
'verify_host' => false,
'timeout' => 30,
'timeout' => 10,
'headers' => [
'Content-Type' => 'application/json',
],
'json' => [],
'json' => $params,
]);
$statusCode = $response->getStatusCode();
$client->setStatus($statusCode === Response::HTTP_OK ? ClientStatus::OG_LIVE : ClientStatus::OFF);

View File

@ -7,6 +7,7 @@ namespace App\Controller\OgAgent\Webhook;
use App\Entity\Client;
use App\Model\ClientStatus;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
@ -18,7 +19,8 @@ use Symfony\Component\Routing\Annotation\Route;
class AgentSessionController extends AbstractController
{
public function __construct(
protected readonly EntityManagerInterface $entityManager
protected readonly EntityManagerInterface $entityManager,
protected readonly LoggerInterface $logger
)
{
}
@ -58,6 +60,14 @@ class AgentSessionController extends AbstractController
$this->entityManager->persist($client);
$this->entityManager->flush();
$this->logger->info('Client started', [
'mac' => $data['mac'],
'ip' => $data['ip'],
'ostype' => $data['ostype'],
'osversion' => $data['osversion'],
'agent_version' => $data['agent_version'],
]);
return new JsonResponse([], Response::HTTP_OK);
}
@ -82,6 +92,13 @@ class AgentSessionController extends AbstractController
$this->entityManager->persist($client);
$this->entityManager->flush();
$this->logger->info('Client stopped', [
'mac' => $data['mac'],
'ip' => $data['ip'],
'ostype' => $data['ostype'],
'osversion' => $data['osversion'],
]);
return new JsonResponse([], Response::HTTP_OK);
}
@ -117,6 +134,18 @@ class AgentSessionController extends AbstractController
return new JsonResponse(['message' => 'Invalid status'], Response::HTTP_BAD_REQUEST);
}
$this->entityManager->persist($client);
$this->entityManager->flush();
$this->logger->info('Client logged in', [
'ip' => $data['ip'],
'user' => $data['user'],
'language' => $data['language'],
'session' => $data['session'],
'ostype' => $data['ostype'],
'osversion' => $data['osversion'],
]);
return new JsonResponse([], Response::HTTP_OK);
}
@ -152,6 +181,15 @@ class AgentSessionController extends AbstractController
return new JsonResponse(['message' => 'Invalid status'], Response::HTTP_BAD_REQUEST);
}
$this->entityManager->persist($client);
$this->entityManager->flush();
$this->logger->info('Client logged out', [
'ip' => $data['ip'],
'user' => $data['user'],
'ostype' => $data['ostype'],
]);
return new JsonResponse([], Response::HTTP_OK);
}
}

View File

@ -3,6 +3,7 @@
namespace App\Controller\OgAgent\Webhook;
use App\Controller\OgRepository\Image\CreateAuxFilesAction;
use App\Entity\Client;
use App\Entity\Image;
use App\Entity\ImageImageRepository;
use App\Entity\OperativeSystem;
@ -32,7 +33,7 @@ use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
#[AsController]
class ClientsController extends AbstractController
class StatusController extends AbstractController
{
const string CREATE_IMAGE = 'RESPUESTA_CrearImagen';
const string RESTORE_IMAGE = 'RESPUESTA_RestaurarImagen';
@ -59,20 +60,28 @@ class ClientsController extends AbstractController
public function index(Request $request): JsonResponse
{
$data = $request->toArray();
$requiredFields = ['job_id'];
foreach ($requiredFields as $field) {
if (!isset($data[$field])) {
return new JsonResponse(['message' => "Missing parameter: $field"], Response::HTTP_BAD_REQUEST);
}
}
$this->logger->info('Webhook data received', $data);
// Esta parte del codigo nos indica si el cliente se encuentra activo
if (isset($data['iph']) && isset($data['timestamp'])) {
$client = $this->entityManager->getRepository(Client::class)->findOneBy(['ip' => $data['iph']]);
if (!$client) {
$this->logger->error('Client not found', $data);
return new JsonResponse(['message' => 'Client not found'], Response::HTTP_NOT_FOUND);
}
$updateAt = (new \DateTime())->setTimestamp((int)$data['timestamp']);
$client->setUpdatedAt($updateAt);
$this->entityManager->persist($client);
$this->entityManager->flush();
}
if (isset($data['progress'])){
$trace = $this->entityManager->getRepository(Trace::class)->findOneBy(['jobId' => $data['job_id']]);
if ($trace){
$trace->setProgress($data['progress'] * 1000);
$trace->setProgress($data['progress'] * 100);
$this->entityManager->persist($trace);
$this->entityManager->flush();
}

View File

@ -40,7 +40,7 @@ class PostAction extends AbstractOgBootController
'json' => [
'template_name' => $pxeTemplate->getName(),
'mac' => strtolower($client->getMac()),
'lang' => 'es_ES.UTF_8',
'lang' => 'es_ES.UTF-8',
'ip' => $client->getIp(),
'server_ip' => $this->ogBootApiUrl,
'router' => $client->getOrganizationalUnit()->getNetworkSettings()->getRouter(),

View File

@ -32,6 +32,7 @@ class BackupImageAction extends AbstractOgRepositoryController
public function __invoke(BackupImageInput $input, ImageImageRepository $imageImageRepository): JsonResponse
{
$image = $imageImageRepository->getImage();
$repository = $imageImageRepository->getRepository();
if (!$image->getName()) {
throw new ValidatorException('Name is required');
@ -41,8 +42,9 @@ class BackupImageAction extends AbstractOgRepositoryController
'json' => [
'ID_img' => $imageImageRepository->getImageFullsum(),
'repo_ip' => $input->repoIp,
'ssh_port' => $repository->getSshPort() ?? '22',
'remote_path' => $input->remotePath,
'user' => 'opengnsys'
'user' => $repository->getUser() ?? 'opengnsys',
]
];

View File

@ -53,12 +53,13 @@ class ConvertAction extends AbstractOgRepositoryController
}
$imageImageRepositoryEntity = new ImageImageRepository();
$imageImageRepositoryEntity->setName($imageEntity->getName().'_v'.$imageImageRepositoryEntity->getVersion() + 1);
$imageImageRepositoryEntity->setStatus(ImageStatus::PENDING);
$imageImageRepositoryEntity->setImage($imageEntity);
$imageImageRepositoryEntity->setRepository($repository);
$imageImageRepositoryEntity->setVersion(1);
$this->entityManager->persist($imageImageRepositoryEntity);
$this->entityManager->flush();
$this->logger->info('Converting image', ['image' => $image]);
@ -74,6 +75,7 @@ class ConvertAction extends AbstractOgRepositoryController
if (isset($content['error']) && $content['code'] === Response::HTTP_INTERNAL_SERVER_ERROR ) {
throw new ValidatorException('Error converting image');
}
$this->entityManager->flush();
$inputData = [
'imageName' => $image,

View File

@ -39,18 +39,18 @@ class CreateAuxFilesAction extends AbstractOgRepositoryController
$params = [
'json' => [
'image' => $image->getName().'.img'
'image' => $data->getName().'.img'
]
];
$this->logger->info('Creating aux files', ['image' => $image->getName()]);
$this->logger->info('Creating aux files', ['image' => $data->getName()]);
$repository = $data->getRepository();
$content = $this->createRequest('POST', 'http://'.$repository->getIp().':8006/ogrepository/v1/images/torrentsum', $params);
$inputData = [
'imageName' => $image->getName(),
'imageName' => $data->getName(),
'imageImageRepositoryUuid' => $data->getUuid(),
];

View File

@ -5,6 +5,7 @@ namespace App\Controller\OgRepository\Image;
use App\Controller\OgRepository\AbstractOgRepositoryController;
use App\Entity\Image;
use App\Entity\ImageImageRepository;
use App\Entity\ImageRepository;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\AsController;
@ -30,6 +31,7 @@ class DeletePermanentAction extends AbstractOgRepositoryController
throw new ValidatorException('Fullsum is required');
}
/** @var ImageRepository $image */
$image = $data->getImage();
$this->logger->info('Deleting image', ['image' => $image->getName()]);
@ -43,6 +45,13 @@ class DeletePermanentAction extends AbstractOgRepositoryController
$this->entityManager->remove($data);
$this->entityManager->flush();
$imageImageCollection = $image->getImageImageRepositories();
if ($imageImageCollection->isEmpty()) {
$this->entityManager->remove($image);
$this->entityManager->flush();
}
return new JsonResponse(data: $content, status: Response::HTTP_OK);
}
}

View File

@ -45,6 +45,13 @@ class DeleteTrashAction extends AbstractOgRepositoryController
$this->entityManager->persist($image);
$this->entityManager->flush();
$imageImageCollection = $image->getImageImageRepositories();
if ($imageImageCollection->isEmpty()) {
$this->entityManager->remove($image);
$this->entityManager->flush();
}
return new JsonResponse(data: $content, status: Response::HTTP_OK);
}
}

View File

@ -30,7 +30,7 @@ class DeployImageAction extends AbstractOgRepositoryController
* @throws ClientExceptionInterface
* @throws TransportExceptionInterface
*/
public function __invoke(DeployImageInput $input, ImageImageRepository $data, Client $client, HttpClientInterface $httpClient): JsonResponse
public function __invoke(DeployImageInput $input, ImageImageRepository $data, Client $client): JsonResponse
{
$params = [
'json' => [
@ -45,9 +45,9 @@ class DeployImageAction extends AbstractOgRepositoryController
];
$type = match ($input->method) {
'udpcast', 'udpcast_direct' => DeployMethodTypes::MULTICAST_UDPCAST,
'udpcast', 'udpcast-direct' => DeployMethodTypes::MULTICAST_UDPCAST,
'p2p' => DeployMethodTypes::TORRENT,
default => DeployMethodTypes::MULTICAST_UFTP,
default => null,
};
$repository = $client->getRepository();

View File

@ -51,9 +51,11 @@ class ImportAction extends AbstractOgRepositoryController
}
$imageImageRepositoryEntity = new ImageImageRepository();
$imageImageRepositoryEntity->setName($imageEntity->getName().'_v'.$imageImageRepositoryEntity->getVersion() + 1);
$imageImageRepositoryEntity->setStatus(ImageStatus::AUX_FILES_PENDING);
$imageImageRepositoryEntity->setImage($imageEntity);
$imageImageRepositoryEntity->setRepository($repository);
$imageImageRepositoryEntity->setVersion(1);
$this->entityManager->persist($imageImageRepositoryEntity);
$this->entityManager->flush();
@ -74,7 +76,7 @@ class ImportAction extends AbstractOgRepositoryController
$inputData = [
'imageName' => $image,
'imageUuid' => $imageImageRepositoryEntity->getUuid(),
'imageImageRepositoryUuid' => $imageImageRepositoryEntity->getUuid(),
];
$this->createService->__invoke(null, CommandTypes::CREATE_IMAGE_AUX_FILE, TraceStatus::IN_PROGRESS, $content['job_id'], $inputData);

View File

@ -0,0 +1,125 @@
<?php
namespace App\Controller\OgRepository\Image;
use App\Controller\OgRepository\AbstractOgRepositoryController;
use App\Dto\Input\DeployImageInput;
use App\Dto\Input\RenameImageInput;
use App\Entity\Image;
use App\Entity\ImageImageRepository;
use App\Entity\ImageRepository;
use App\Model\CommandTypes;
use App\Model\ImageStatus;
use App\Model\TraceStatus;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\AsController;
use Symfony\Component\Validator\Exception\ValidatorException;
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\Contracts\HttpClient\HttpClientInterface;
#[AsController]
class RenameAction extends AbstractOgRepositoryController
{
/**
* @throws TransportExceptionInterface
* @throws ServerExceptionInterface
* @throws RedirectionExceptionInterface
* @throws ClientExceptionInterface
*/
public function __invoke(RenameImageInput $input, ImageImageRepository $imageImageRepository): JsonResponse
{
$image = $imageImageRepository->getImage();
if ($image->isGlobal()) {
$repositories = $image->getImageImageRepositories();
if ($repositories->count() === 0) {
return $this->jsonError('Image is not in any repository');
}
if (!$this->isAvailableInAllRepositories($repositories)) {
$this->logger->info('Image is not available in all repositories', ['image' => $image->getName()]);
return $this->jsonError('Image is not available in all repositories');
}
$repoWithImage = $this->entityManager
->getRepository(ImageImageRepository::class)
->findBy(['image' => $image, 'repository' => $imageImageRepository->getRepository()]);
} else {
$repoWithImage = [$imageImageRepository];
}
$hasError = false;
foreach ($repoWithImage as $repository) {
$content = $this->renameImageInRepository($repository, $input->newName);
if (isset($content['error']) && $content['code'] === Response::HTTP_INTERNAL_SERVER_ERROR) {
$hasError = true;
}
$repository->setName($input->newName);
$this->entityManager->persist($repository);
}
$this->entityManager->flush();
if ($hasError) {
return new JsonResponse(['error' => 'Error renaming image'], Response::HTTP_INTERNAL_SERVER_ERROR);
}
return new JsonResponse([], Response::HTTP_OK);
}
/**
* @throws RedirectionExceptionInterface
* @throws ClientExceptionInterface
* @throws ServerExceptionInterface
*/
private function isAvailableInAllRepositories($repositories): bool
{
foreach ($repositories as $repository) {
try {
$this->createRequest('GET', 'http://' . $repository->getRepository()->getIp() . ':8006/ogrepository/v1/status');
} catch (TransportExceptionInterface $e) {
return false;
}
}
return true;
}
/**
* @throws TransportExceptionInterface
* @throws ServerExceptionInterface
* @throws RedirectionExceptionInterface
* @throws ClientExceptionInterface
*/
private function renameImageInRepository(ImageImageRepository $repository, string $newName): array
{
$params = [
'json' => [
'ID_img' => $repository->getImageFullsum(),
'image_new_name' => $newName,
]
];
return $this->createRequest(
'PUT',
'http://' . $repository->getRepository()->getIp() . ':8006/ogrepository/v1/images/rename',
$params
);
}
private function jsonError(string $message): JsonResponse
{
return new JsonResponse(
['error' => $message, 'code' => Response::HTTP_INTERNAL_SERVER_ERROR],
Response::HTTP_BAD_REQUEST
);
}
}

View File

@ -47,7 +47,8 @@ class TransferAction extends AbstractOgRepositoryController
'json' => [
'image' => $image->getName().'.img',
'repo_ip' => $imageImageRepository->getRepository()->getIp(),
'user' => 'opengnsys',
'user' => $repository->getUser(),
'ssh_port' => $repository->getSshPort()
]
];

View File

@ -48,7 +48,8 @@ class TransferGlobalAction extends AbstractOgRepositoryController
'json' => [
'image' => $image->getName().'.img',
'repo_ip' => $imageImageRepository->getRepository()->getIp(),
'user' => 'opengnsys',
'user' => $repository->getUser(),
'ssh_port' => $repository->getSshPort()
]
];

View File

@ -56,13 +56,16 @@ class SyncAction extends AbstractOgRepositoryController
if (!$imageImageRepositoryEntity) {
$imageImageRepositoryEntity = new ImageImageRepository();
$imageImageRepositoryEntity->setImageFullsum($image['fullsum']);
$imageImageRepositoryEntity->setStatus(ImageStatus::SUCCESS);
$imageImageRepositoryEntity->setImage($imageEntity);
$imageImageRepositoryEntity->setRepository($input);
$this->entityManager->persist($imageImageRepositoryEntity);
}
$imageImageRepositoryEntity->setImageFullsum($image['fullsum']);
$imageImageRepositoryEntity->setDatasize($image['datasize']);
$imageImageRepositoryEntity->setStatus(ImageStatus::SUCCESS);
$imageImageRepositoryEntity->setImage($imageEntity);
$imageImageRepositoryEntity->setRepository($input);
$this->entityManager->persist($imageImageRepositoryEntity);
}
foreach ($existingImages as $existingImage) {

View File

@ -51,10 +51,17 @@ class ResponseController extends AbstractOgRepositoryController
$imageImageRepository = $this->getImageImageRepository($trace);
if (!$imageImageRepository) return $this->jsonResponseError('Image not found', Response::HTTP_NOT_FOUND, $trace);
$imageImageRepository->setStatus(ImageStatus::SUCCESS);
$this->entityManager->persist($imageImageRepository);
if (isset($data['success']) && $data['success'] !== true) {
$this->updateTraceStatus($trace, TraceStatus::FAILED, $data['output'] ?? 'Action failed');
return new JsonResponse(['message' => 'Success'], Response::HTTP_OK);
}
if ($setFullsum) {
$imageImageRepository->setImageFullsum($data['image_id']);
}
$imageImageRepository->setStatus(ImageStatus::SUCCESS);
$this->entityManager->persist($imageImageRepository);
$this->updateTraceStatus($trace, TraceStatus::SUCCESS);
@ -83,6 +90,7 @@ class ResponseController extends AbstractOgRepositoryController
}
$newImageRepo = new ImageImageRepository();
$newImageRepo->setName($image->getName().'_v'.($originImageImageRepository->getVersion() + 1));
$newImageRepo->setImage($image);
$newImageRepo->setRepository($repository);
$newImageRepo->setStatus(ImageStatus::SUCCESS);

View File

@ -14,4 +14,9 @@ final class CommandExecuteInput
#[Assert\NotNull]
#[Groups(['command:write'])]
public array $clients = [];
#[Assert\NotNull]
#[Groups(['command:write'])]
public string $script = '';
}

View File

@ -40,6 +40,13 @@ final class CommandInput
)]
public ?bool $enabled = true;
#[Groups(['command:write'])]
#[ApiProperty(
description: 'Tiene parámetros?',
example: 'true',
)]
public ?bool $parameters = true;
#[Groups(['command:write'])]
#[ApiProperty(
description: 'Los comentarios del comando',
@ -57,6 +64,7 @@ final class CommandInput
$this->script = $command->getScript();
$this->enabled = $command->isEnabled();
$this->readOnly = $command->isReadOnly();
$this->parameters = $command->isParameters();
$this->comments = $command->getComments();
}
@ -70,6 +78,7 @@ final class CommandInput
$command->setScript($this->script);
$command->setEnabled($this->enabled);
$command->setReadOnly($this->readOnly);
$command->setParameters($this->parameters);
$command->setComments($this->comments);
return $command;

View File

@ -12,13 +12,8 @@ use Symfony\Component\Validator\Constraints as Assert;
final class ImageImageRepositoryInput
{
#[Assert\NotNull]
#[Groups(['image-image-repository:write'])]
public ?ImageRepositoryOutput $imageRepository = null;
#[Assert\NotNull]
#[Groups(['image-image-repository:write'])]
public ?string $status = '';
public ?string $description = '';
public function __construct(?ImageImageRepository $imageImageRepository = null)
{
@ -26,8 +21,7 @@ final class ImageImageRepositoryInput
return;
}
$this->imageRepository = new ImageRepositoryOutput($imageImageRepository->getRepository());
$this->status = $imageImageRepository->getStatus();
$this->description = $imageImageRepository->getDescription();
}
public function createOrUpdateEntity(?ImageImageRepository $imageImageRepository = null): ImageImageRepository
@ -36,8 +30,7 @@ final class ImageImageRepositoryInput
$imageImageRepository = new ImageImageRepository();
}
$imageImageRepository->setRepository($this->imageRepository);
$imageImageRepository->setStatus($this->status);
$imageImageRepository->setDescription($this->description);
return $imageImageRepository;
}

View File

@ -5,6 +5,7 @@ namespace App\Dto\Input;
use ApiPlatform\Metadata\ApiProperty;
use App\Dto\Output\ClientOutput;
use App\Dto\Output\ImageImageRepositoryOutput;
use App\Dto\Output\ImageOutput;
use App\Dto\Output\ImageRepositoryOutput;
use App\Dto\Output\OrganizationalUnitOutput;
use App\Dto\Output\PartitionOutput;
@ -37,6 +38,10 @@ final class ImageInput
#[ApiProperty(description: 'The type of the image', example: "Server")]
public ?string $source = 'input';
#[Groups(['image:write'])]
#[ApiProperty(description: 'The optional selected image')]
public ?ImageOutput $selectedImage = null;
#[Groups(['image:write'])]
#[ApiProperty(description: 'The software profile of the image')]
public ?SoftwareProfileOutput $softwareProfile = null;
@ -60,6 +65,10 @@ final class ImageInput
#[ApiProperty(description: 'The parent of the image')]
public ?self $parent = null;
#[Groups(['image:write'])]
#[ApiProperty(description: 'The parent of the image')]
public ?int $version = null;
#[Groups(['image:write'])]
#[ApiProperty(description: 'The remote pc of the image')]
public ?bool $remotePc = false;
@ -79,6 +88,7 @@ final class ImageInput
$this->comments = $image->getComments();
$this->remotePc = $image->isRemotePc();
$this->isGlobal = $image->isGlobal();
$this->version = $image->getVersion();
if ($image->getSoftwareProfile()) {
$this->softwareProfile = new SoftwareProfileOutput($image->getSoftwareProfile());

View File

@ -27,11 +27,19 @@ final class ImageRepositoryInput
#[ApiProperty(description: 'The IP of the repository', example: "")]
public ?string $ip = null;
#[Groups(['repository:write'])]
#[ApiProperty(description: 'The comments of the repository', example: "Repository 1 comments")]
public ?string $comments = null;
#[Assert\NotBlank]
#[Groups(['repository:write'])]
#[ApiProperty(description: 'The user of the repository', example: "Repository user")]
public ?string $user = 'opengnsys';
#[Groups(['repository:write'])]
#[ApiProperty(description: 'The sshPort of the repository', example: "Repository ssh port")]
public ?string $sshPort = '22';
public function __construct(?ImageRepository $repository = null)
{
@ -42,6 +50,8 @@ final class ImageRepositoryInput
$this->name = $repository->getName();
$this->ip = $repository->getIp();
$this->comments = $repository->getComments();
$this->user = $repository->getUser();
$this->sshPort = $repository->getSshPort();
}
public function createOrUpdateEntity(?ImageRepository $repository = null): ImageRepository
@ -53,6 +63,8 @@ final class ImageRepositoryInput
$repository->setName($this->name);
$repository->setIp($this->ip);
$repository->setComments($this->comments);
$repository->setUser($this->user);
$repository->setSshPort($this->sshPort);
return $repository;
}

View File

@ -0,0 +1,13 @@
<?php
namespace App\Dto\Input;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;
final class RenameImageInput
{
#[Assert\NotNull]
#[Groups(['image-image-repository:write'])]
public ?string $newName = '';
}

View File

@ -25,6 +25,9 @@ final class ClientOutput extends AbstractOutput
#[Groups(['client:read', 'organizational-unit:read', 'pxe-template:read', 'trace:read', 'subnet:read'])]
public ?string $mac = '';
#[Groups(['client:read'])]
public ?string $firmwareType = '';
#[Groups(['client:read', 'organizational-unit:read', 'trace:read'])]
public ?string $serialNumber = '';
@ -91,6 +94,7 @@ final class ClientOutput extends AbstractOutput
$this->ip = $client->getIp();
$this->netiface = $client->getNetiface();
$this->netDriver = $client->getNetDriver();
$this->firmwareType = $client->getFirmwareType();
if ($client->getOrganizationalUnit()) {
$this->organizationalUnit = new OrganizationalUnitOutput($client->getOrganizationalUnit());

View File

@ -21,6 +21,9 @@ final class CommandOutput extends AbstractOutput
#[Groups(['command:read'])]
public ?bool $enabled = true;
#[Groups(['command:read'])]
public ?bool $parameters = true;
#[Groups(['command:read'])]
public ?string $comments = '';
@ -37,6 +40,7 @@ final class CommandOutput extends AbstractOutput
$this->name = $command->getName();
$this->script = $command->getScript();
$this->readOnly = $command->isReadOnly();
$this->parameters = $command->isParameters();
$this->enabled = $command->isEnabled();
$this->comments = $command->getComments();
$this->createdAt = $command->getCreatedAt();

View File

@ -18,9 +18,21 @@ class ImageImageRepositoryOutput extends AbstractOutput
#[Groups(['image-image-repository:read', 'image:read'])]
public string $status;
#[Groups(['image-image-repository:read', 'image:read'])]
public string $name;
#[Groups(['image-image-repository:read', 'image:read'])]
public ?string $imageFullsum = null;
#[Groups(['image-image-repository:read', 'image:read'])]
public ?string $datasize = null;
#[Groups(['image-image-repository:read', 'image:read'])]
public ?string $description = null;
#[Groups(['image:read', 'image-image-repository:read'])]
public ?int $version = null;
#[Groups(['image-image-repository:read', 'image:read'])]
public \DateTime $createdAt;
@ -41,8 +53,12 @@ class ImageImageRepositoryOutput extends AbstractOutput
$this->imageRepository = new ImageRepositoryOutput($imageImageRepository->getRepository());
}
$this->name = $imageImageRepository->getName();
$this->version = $imageImageRepository->getVersion();
$this->status = $imageImageRepository->getStatus();
$this->imageFullsum = $imageImageRepository->getImageFullsum();
$this->datasize = $imageImageRepository->getDatasize();
$this->description = $imageImageRepository->getDescription();
$this->createdAt = $imageImageRepository->getCreatedAt();
$this->createdBy = $imageImageRepository->getCreatedBy();
}

View File

@ -39,6 +39,9 @@ final class ImageOutput extends AbstractOutput
#[Groups(['image:read'])]
public ?array $partitionInfo = null;
#[Groups(['image:read', 'image-image-repository:read'])]
public ?int $version = null;
#[Groups(['image:read'])]
public \DateTime $createdAt;
@ -58,6 +61,7 @@ final class ImageOutput extends AbstractOutput
fn(ImageImageRepository $image) => new ImageImageRepositoryOutput($image)
)->toArray();
$this->version = $image->getVersion();
$this->partitionInfo = json_decode($image->getPartitionInfo(), true);
$this->remotePc = $image->isRemotePc();
$this->isGlobal = $image->isGlobal();

View File

@ -18,6 +18,12 @@ class ImageRepositoryOutput extends AbstractOutput
#[Groups(['repository:read'])]
public ?string $comments = '';
#[Groups(['repository:read'])]
public ?string $sshPort = '';
#[Groups(['repository:read'])]
public ?string $user = '';
#[Groups(['repository:read'])]
public \DateTime $createdAt;
@ -31,6 +37,8 @@ class ImageRepositoryOutput extends AbstractOutput
$this->name = $imageRepository->getName();
$this->ip = $imageRepository->getIp();
$this->comments = $imageRepository->getComments();
$this->sshPort = $imageRepository->getSshPort();
$this->user = $imageRepository->getUser();
$this->createdAt = $imageRepository->getCreatedAt();
$this->createdBy = $imageRepository->getCreatedBy();
}

View File

@ -48,9 +48,6 @@ final class OrganizationalUnitOutput extends AbstractOutput
#[Groups(['organizational-unit:read'])]
public array $children = [];
#[Groups(['organizational-unit:read'])]
public array $clients = [];
#[Groups(['organizational-unit:read', "client:read"])]
#[ApiProperty(readableLink: true)]
public ?RemoteCalendarOutput $remoteCalendar = null;
@ -96,12 +93,6 @@ final class OrganizationalUnitOutput extends AbstractOutput
)->toArray();
}
if (isset($context['groups']) && in_array('organizational-unit:read', $context['groups'])) {
$this->clients = $organizationalUnit->getClients()->map(
fn(Client $client) => new ClientOutput($client)
)->toArray();
}
$this->excludeParentChanges = $organizationalUnit->isExcludeParentChanges();
$this->path = $organizationalUnit->getPath();
$this->createdAt = $organizationalUnit->getCreatedAt();

View File

@ -33,9 +33,6 @@ final class SubnetOutput extends AbstractOutput
#[Groups(['subnet:read'])]
public ?string $dns = null;
#[Groups(['subnet:read'])]
public array $clients;
#[Groups(['subnet:read'])]
public ?bool $synchronized = false;
@ -61,11 +58,6 @@ final class SubnetOutput extends AbstractOutput
$this->bootFileName = $subnet->getBootFileName();
$this->synchronized = $subnet->isSynchronized();
$this->serverId = $subnet->getServerId();
$this->clients = $subnet->getClients()->map(
fn(Client $client) => new ClientOutput($client)
)->toArray();
$this->createdAt = $subnet->getCreatedAt();
$this->createdBy = $subnet->getCreatedBy();
}

View File

@ -59,7 +59,7 @@ final class TraceOutput extends AbstractOutput
$this->output = $trace->getOutput();
$this->input = $trace->getInput();
$this->finishedAt = $trace->getFinishedAt();
$this->progress = $trace->getProgress() / 100;
$this->progress = $trace->getProgress();
$this->createdAt = $trace->getCreatedAt();
$this->createdBy = $trace->getCreatedBy();
}

View File

@ -14,6 +14,9 @@ use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
#[ORM\UniqueConstraint(name: 'UNIQ_IDENTIFIER_MAC', fields: ['mac'])]
#[UniqueEntity(fields: ['ip'], message: 'This IP address is already in use.')]
#[UniqueEntity(fields: ['mac'], message: 'This MAC address is already in use.')]
#[ORM\Index(fields: ['status'], name: 'IDX_STATUS')]
#[ORM\Index(fields: ['updatedAt'], name: 'IDX_UPDATED_AT')]
#[ORM\Index(fields: ['status', 'updatedAt'], name: 'IDX_STATUS_UPDATED_AT')]
class Client extends AbstractEntity
{
use NameableTrait;
@ -82,6 +85,9 @@ class Client extends AbstractEntity
#[ORM\Column(nullable: true)]
private ?bool $pxeSync = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $firmwareType = null;
public function __construct()
{
parent::__construct();
@ -335,4 +341,16 @@ class Client extends AbstractEntity
return $this;
}
public function getFirmwareType(): ?string
{
return $this->firmwareType;
}
public function setFirmwareType(?string $firmwareType): static
{
$this->firmwareType = $firmwareType;
return $this;
}
}

View File

@ -35,6 +35,9 @@ class Command extends AbstractEntity
#[ORM\ManyToMany(targetEntity: CommandTask::class, mappedBy: 'commands')]
private Collection $commandTasks;
#[ORM\Column(nullable: true)]
private ?bool $parameters = null;
public function __construct()
{
parent::__construct();
@ -131,4 +134,16 @@ class Command extends AbstractEntity
return $this;
}
public function isParameters(): ?bool
{
return $this->parameters;
}
public function setParameters(?bool $parameters): static
{
$this->parameters = $parameters;
return $this;
}
}

View File

@ -46,6 +46,9 @@ class Image extends AbstractEntity
#[ORM\OneToMany(mappedBy: 'image', targetEntity: ImageImageRepository::class, cascade: ['persist'], orphanRemoval: true)]
private Collection $imageImageRepositories;
#[ORM\Column(nullable: true)]
private ?int $version = null;
public function __construct()
{
parent::__construct();
@ -191,4 +194,16 @@ class Image extends AbstractEntity
return false;
}
public function getVersion(): ?int
{
return $this->version;
}
public function setVersion(?int $version): static
{
$this->version = $version;
return $this;
}
}

View File

@ -7,8 +7,6 @@ use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
#[ORM\Entity(repositoryClass: ImageImageRepositoryRepository::class)]
#[ORM\UniqueConstraint(name: 'UNIQ_IDENTIFIER_IMAGE_REPOSITORY', columns: ['image_id', 'repository_id'])]
#[UniqueEntity(fields: ['image', 'repository'], message: 'This image is already associated with this repository')]
class ImageImageRepository extends AbstractEntity
{
#[ORM\ManyToOne(targetEntity: Image::class, cascade: ['persist'], inversedBy: 'imageImageRepositories')]
@ -28,6 +26,18 @@ class ImageImageRepository extends AbstractEntity
#[ORM\Column(length: 255, nullable: true)]
private ?string $imageFullsum = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $datasize = null;
#[ORM\Column(nullable: true)]
private ?int $version = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $description = null;
#[ORM\Column(length: 255)]
private ?string $name = null;
public function getImage(): ?Image
{
return $this->image;
@ -87,4 +97,52 @@ class ImageImageRepository extends AbstractEntity
return $this;
}
public function getDatasize(): ?string
{
return $this->datasize;
}
public function setDatasize(?string $datasize): static
{
$this->datasize = $datasize;
return $this;
}
public function getVersion(): ?int
{
return $this->version;
}
public function setVersion(?int $version): static
{
$this->version = $version;
return $this;
}
public function getDescription(): ?string
{
return $this->description;
}
public function setDescription(?string $description): static
{
$this->description = $description;
return $this;
}
public function getName(): ?string
{
return $this->name;
}
public function setName(string $name): static
{
$this->name = $name;
return $this;
}
}

View File

@ -10,6 +10,8 @@ use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: ImageRepositoryRepository::class)]
class ImageRepository extends AbstractEntity
{
const string DEFAULT_USER = 'opengnsys';
use NameableTrait;
#[ORM\Column(length: 255)]
@ -24,6 +26,12 @@ class ImageRepository extends AbstractEntity
#[ORM\OneToMany(mappedBy: 'repository', targetEntity: ImageImageRepository::class)]
private Collection $imageImageRepositories;
#[ORM\Column(length: 255, nullable: true)]
private ?string $user = self::DEFAULT_USER;
#[ORM\Column(length: 255, nullable: true)]
private ?string $sshPort = null;
public function __construct()
{
parent::__construct();
@ -83,4 +91,28 @@ class ImageRepository extends AbstractEntity
return $this;
}
public function getUser(): ?string
{
return $this->user;
}
public function setUser(?string $user): static
{
$this->user = $user;
return $this;
}
public function getSshPort(): ?string
{
return $this->sshPort;
}
public function setSshPort(?string $sshPort): static
{
$this->sshPort = $sshPort;
return $this;
}
}

View File

@ -25,6 +25,14 @@ class ClientStatusNotifier
public function postUpdate(Client $client, PostUpdateEventArgs $event): void
{
$em = $event->getObjectManager();
$uow = $em->getUnitOfWork();
$changeSet = $uow->getEntityChangeSet($client);
if (!array_key_exists('status', $changeSet)) {
return;
}
try {
$this->notifyClientStatusChange($client);
} catch (\Exception $e) {
@ -38,9 +46,14 @@ class ClientStatusNotifier
private function notifyClientStatusChange(Client $client): void
{
$data[] = [
'@id' => '/clients/' . $client->getUuid(),
'status' => $client->getStatus(),
];
$update = new Update(
'clients',
json_encode(['@id' => '/clients/'.$client->getUuid(), 'status' => $client->getStatus()])
json_encode($data)
);
$this->hub->publish($update);

View File

@ -42,7 +42,7 @@ class TraceStatusProgressNotifier
{
$update = new Update(
'traces',
json_encode(['@id' => '/traces/' . $trace->getUuid(), 'status' => $trace->getStatus(), 'progress' => $trace->getProgress() / 100])
json_encode(['@id' => '/traces/' . $trace->getUuid(), 'status' => $trace->getStatus(), 'progress' => $trace->getProgress()])
);
$this->hub->publish($update);

View File

@ -58,9 +58,14 @@ class MercureSubscriber implements EventSubscriberInterface
/** @var Client $client */
$client = $clientOutput->getEntity();
$data[] = [
'@id' => '/clients/' . $client->getUuid(),
'status' => $client->getStatus(),
];
$update = new Update(
'clients',
json_encode(['@id' => '/clients/'.$client->getUuid(), 'status' => $client->getStatus()])
json_encode($data)
);
$this->hub->publish($update);

View File

@ -5,18 +5,25 @@ namespace App\EventSubscriber;
use ApiPlatform\Symfony\EventListener\EventPriorities;
use App\Controller\OgBoot\PxeBootFile\PostAction;
use App\Dto\Output\OrganizationalUnitOutput;
use App\Entity\Client;
use App\Entity\NetworkSettings;
use App\Entity\OrganizationalUnit;
use App\Model\OrganizationalUnitTypes;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\ViewEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
final readonly class OrganizationalUnitSubscriber implements EventSubscriberInterface
{
public function __construct(
private EntityManagerInterface $entityManager,
private readonly PostAction $postAction,
)
{
@ -29,6 +36,12 @@ final readonly class OrganizationalUnitSubscriber implements EventSubscriberInte
];
}
/**
* @throws TransportExceptionInterface
* @throws ServerExceptionInterface
* @throws RedirectionExceptionInterface
* @throws ClientExceptionInterface
*/
public function updateNetworkSettings(ViewEvent $event): void
{
$organizationalUnitOutput = $event->getControllerResult();
@ -50,7 +63,9 @@ final readonly class OrganizationalUnitSubscriber implements EventSubscriberInte
$this->updateChildrenNetworkSettings($organizationalUnitEntity, $newNetworkSettings);
$this->entityManager->flush();
if ($organizationalUnitEntity->getType() === OrganizationalUnitTypes::CLASSROOM) {
//$this->syncOgBoot($organizationalUnitEntity);
}
}
@ -58,14 +73,21 @@ final readonly class OrganizationalUnitSubscriber implements EventSubscriberInte
{
/** @var OrganizationalUnit $childUnit */
foreach ($parentUnit->getOrganizationalUnits() as $childUnit) {
//var_dump($childUnit->getNetworkSettings()->getMcastPort());
if ($childUnit->isExcludeParentChanges()) {
$childUnit->setNetworkSettings(null);
} else{
$childUnit->setNetworkSettings($networkSettings);
foreach ($childUnit->getClients() as $client) {
$client->setOgLive($networkSettings->getOgLive());
$client->setMenu($networkSettings->getMenu());
$client->setRepository($networkSettings->getRepository());
$this->entityManager->persist($client);
}
}
$this->entityManager->persist($childUnit);
$this->entityManager->flush();
$this->updateChildrenNetworkSettings($childUnit, $networkSettings);
}
@ -94,7 +116,32 @@ final readonly class OrganizationalUnitSubscriber implements EventSubscriberInte
$newNetworkSettings->setMenu($organizationalUnitEntity->getNetworkSettings()->getMenu());
$newNetworkSettings->setRepository($organizationalUnitEntity->getNetworkSettings()->getRepository());
$newNetworkSettings->setOgLive($organizationalUnitEntity->getNetworkSettings()->getOgLive());
$newNetworkSettings->setNetiface($organizationalUnitEntity->getNetworkSettings()->getNetiface());
return $newNetworkSettings;
}
/**
* @throws TransportExceptionInterface
* @throws ServerExceptionInterface
* @throws RedirectionExceptionInterface
* @throws ClientExceptionInterface
*/
private function syncOgBoot(OrganizationalUnit $organizationalUnitEntity): void
{
$clients = $this->entityManager->getRepository(Client::class)->findClientsByOrganizationalUnitAndDescendants($organizationalUnitEntity->getId(), []);
if (empty($clients)) {
return;
}
/** @var Client $client */
foreach ($clients as $client) {
if ($client->getTemplate() === null) {
continue;
}
$this->postAction->__invoke($client, $client->getTemplate());
}
}
}

View File

@ -0,0 +1,58 @@
<?php
namespace App\Factory;
use App\Entity\ImageImageRepository;
use App\Repository\ImageImageRepositoryRepository;
use Zenstruck\Foundry\ModelFactory;
use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory;
use Zenstruck\Foundry\Persistence\Proxy;
use Zenstruck\Foundry\Persistence\ProxyRepositoryDecorator;
/**
* @extends PersistentProxyObjectFactory<ImageImageRepository>
*/
final class ImageImageRepositoryFactory extends ModelFactory
{
/**
* @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#factories-as-services
*
* @todo inject services if required
*/
public function __construct()
{
parent::__construct();
}
/**
* @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#model-factories
*
* @todo add your default values here
*/
protected function getDefaults(): array
{
return [
'createdAt' => self::faker()->dateTime(),
'image' => ImageFactory::new(),
'name' => self::faker()->text(255),
'repository' => ImageRepositoryFactory::new(),
'status' => self::faker()->text(255),
'updatedAt' => self::faker()->dateTime(),
];
}
/**
* @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#initialization
*/
protected function initialize(): self
{
return $this
// ->afterInstantiate(function(ImageImageRepository $imageImageRepository): void {})
;
}
protected static function getClass(): string
{
return ImageImageRepository::class;
}
}

View File

@ -6,6 +6,7 @@ final class ClientStatus
{
public const string OFF = 'off';
public const string INITIALIZING = 'initializing';
public const string DISCONNECTED = 'disconnected';
public const string TURNING_OFF = 'turning-off';
public const string OG_LIVE = 'og-live';
public const string BUSY = 'busy';
@ -20,6 +21,7 @@ final class ClientStatus
self::OFF => 'Apagado',
self::TURNING_OFF => 'Apagando',
self::INITIALIZING => 'Inicializando',
self::DISCONNECTED => 'Conexión perdida',
self::OG_LIVE => 'OG Live',
self::BUSY => 'Ocupado',
self::LINUX => 'Linux',

View File

@ -12,6 +12,7 @@ final class CommandTypes
public const string BACKUP_IMAGE = 'backup-image';
public const string IMPORT_IMAGE = 'import-image';
public const string EXPORT_IMAGE = 'export-image';
public const string RENAME_IMAGE = 'rename-image';
public const string CONVERT_IMAGE_TO_VIRTUAL = 'convert-image-to-virtual';
public const string TRANSFER_IMAGE = 'transfer-image';
public const string POWER_ON = 'power-on';
@ -21,6 +22,7 @@ final class CommandTypes
public const string LOGOUT = 'logout';
public const string PARTITION_AND_FORMAT = 'partition-and-format';
public const string INSTALL_OGLIVE = 'install-oglive';
public const string RUN_SCRIPT = 'run-script';
private const array COMMAND_TYPES = [
self::DEPLOY_IMAGE => 'Deploy Image',
@ -32,6 +34,7 @@ final class CommandTypes
self::BACKUP_IMAGE => 'Backup Image',
self::IMPORT_IMAGE => 'Import image',
self::EXPORT_IMAGE => 'Export image',
self::RENAME_IMAGE => 'Rename Image',
self::POWER_ON => 'Encender',
self::REBOOT => 'Reiniciar',
self::SHUTDOWN => 'Apagar',
@ -40,6 +43,7 @@ final class CommandTypes
self::PARTITION_AND_FORMAT => 'Partition and Format',
self::TRANSFER_IMAGE => 'Transfer Image',
self::INSTALL_OGLIVE => 'Instalar OgLive',
self::RUN_SCRIPT => 'Run Script',
];
public static function getCommandTypes(): array

View File

@ -2,7 +2,9 @@
namespace App\Repository;
use App\Entity\Image;
use App\Entity\ImageImageRepository;
use App\Entity\ImageRepository as Repository;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
@ -15,4 +17,27 @@ class ImageImageRepositoryRepository extends AbstractRepository
{
parent::__construct($registry, ImageImageRepository::class);
}
public function findLatestVersionByImageAndRepository(Image $image, Repository $repository): ?ImageImageRepository
{
return $this->createQueryBuilder('i')
->andWhere('i.image = :imageId')
->setParameter('imageId', $image->getId())
->andWhere('i.repository = :repository')
->setParameter('repository', $repository->getId())
->orderBy('i.version', 'DESC')
->setMaxResults(1)
->getQuery()
->getOneOrNullResult();
}
public function findLatestVersion(): ?ImageImageRepository
{
return $this->createQueryBuilder('i')
->orderBy('i.version', 'DESC')
->setMaxResults(1)
->getQuery()
->getOneOrNullResult();
}
}

View File

@ -26,11 +26,19 @@ class CreatePartitionService
$receivedPartitions = [];
foreach ($data['cfg'] as $cfg) {
if (!isset($cfg['disk'], $cfg['par'], $cfg['tam'], $cfg['uso'], $cfg['fsi'])) {
continue;
}
$filteredCfg = array_filter($data['cfg'], function ($cfg) {
return isset($cfg['disk'], $cfg['par'], $cfg['tam'], $cfg['uso'], $cfg['fsi']);
});
foreach ($data['cfg'] as $cfg) {
if (!empty($cfg['fwt'])) {
$clientEntity->setFirmwareType($cfg['fwt']);
$this->entityManager->persist($clientEntity);
break;
}
}
foreach ($filteredCfg as $cfg) {
$partitionEntity = $this->entityManager->getRepository(Partition::class)
->findOneBy(['client' => $clientEntity, 'diskNumber' => $cfg['disk'], 'partitionNumber' => $cfg['par']]);
@ -38,6 +46,8 @@ class CreatePartitionService
$partitionEntity = new Partition();
}
$partitionEntity->setOperativeSystem(null);
if (isset($cfg['soi']) && $cfg['soi'] !== '') {
$operativeSystem = $this->entityManager->getRepository(OperativeSystem::class)
->findOneBy(['name' => $cfg['soi']]);

View File

@ -10,6 +10,7 @@ use ApiPlatform\Metadata\Put;
use ApiPlatform\State\ProcessorInterface;
use ApiPlatform\Validator\ValidatorInterface;
use App\Controller\OgAgent\CreateImageAction;
use App\Controller\OgRepository\Image\RenameAction;
use App\Controller\OgRepository\Image\TransferAction;
use App\Dto\Input\ImageInput;
use App\Dto\Input\ImageRepositoryInput;
@ -17,15 +18,16 @@ use App\Dto\Output\ImageOutput;
use App\Entity\ImageImageRepository;
use App\Repository\ImageRepository;
use App\Repository\ImageRepositoryRepository as ImageRepositoryRepository;
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
readonly class ImageProcessor implements ProcessorInterface
{
public function __construct(
private ImageRepositoryRepository $imageRepositoryRepository,
private ImageRepository $imageRepository,
private ValidatorInterface $validator,
private CreateImageAction $createImageActionController,
private KernelInterface $kernel,
)
{
}
@ -61,16 +63,24 @@ readonly class ImageProcessor implements ProcessorInterface
$entity = $this->imageRepository->findOneByUuid($uriVariables['uuid']);
}
$image = $data->createOrUpdateEntity($entity);
$this->validator->validate($image);
if ($data->source !== 'input') {
$response = $this->createImageActionController->__invoke($image);
if ($data->selectedImage){
//$content = $this->renameActionController->__invoke($data->selectedImage->getEntity());
$response = $this->createImageActionController->__invoke($data->selectedImage->getEntity());
} else {
$image = $data->createOrUpdateEntity($entity);
if ($this->kernel->getEnvironment() !== 'test') {
$response = $this->createImageActionController->__invoke($image);
}
$this->validator->validate($image);
$this->imageRepository->save($image);
}
$this->imageRepository->save($image);
return new ImageOutput($image);
return new ImageOutput($data->selectedImage?->getEntity() ?? $image);
}
private function processDelete($data, Operation $operation, array $uriVariables = [], array $context = []): null

View File

@ -9,6 +9,7 @@ use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Put;
use ApiPlatform\State\Pagination\TraversablePaginator;
use ApiPlatform\State\ProviderInterface;
use App\Dto\Input\ImageImageRepositoryInput;
use App\Dto\Output\ImageImageRepositoryOutput;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
@ -26,6 +27,9 @@ readonly class ImageImageRepositoryProvider implements ProviderInterface
switch ($operation){
case $operation instanceof GetCollection:
return $this->provideCollection($operation, $uriVariables, $context);
case $operation instanceof Patch:
case $operation instanceof Put:
return $this->provideInput($operation, $uriVariables, $context);
case $operation instanceof Get:
return $this->provideItem($operation, $uriVariables, $context);
}
@ -53,4 +57,15 @@ readonly class ImageImageRepositoryProvider implements ProviderInterface
return new ImageImageRepositoryOutput($item);
}
public function provideInput(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
{
if (isset($uriVariables['uuid'])) {
$item = $this->itemProvider->provide($operation, $uriVariables, $context);
return $item !== null ? new ImageImageRepositoryInput($item) : null;
}
return new ImageImageRepositoryInput();
}
}

View File

@ -9,6 +9,7 @@ use App\Entity\OrganizationalUnit;
use App\Entity\SoftwareProfile;
use App\Factory\ClientFactory;
use App\Factory\ImageFactory;
use App\Factory\ImageImageRepositoryFactory;
use App\Factory\ImageRepositoryFactory;
use App\Factory\OrganizationalUnitFactory;
use App\Factory\SoftwareProfileFactory;
@ -70,12 +71,12 @@ class ImageTest extends AbstractTest
SoftwareProfileFactory::createOne(['description' => self::SOFTWARE_PROFILE]);
$swPIri = $this->findIriBy(SoftwareProfile::class, ['description' => self::SOFTWARE_PROFILE]);
$imageRepositories = ImageRepositoryFactory::createMany(5);
$imageRepositories = ImageImageRepositoryFactory::createMany(5);
$this->createClientWithCredentials()->request('POST', '/images',['json' => [
'name' => self::IMAGE_CREATE,
'softwareProfile' => $swPIri,
'imageRepositories' => array_map(fn($repo) => '/image-repositories/'. $repo->getUuid(), $imageRepositories)
'imageImageRepositories' => array_map(fn($repo) => '/image-repositories/'. $repo->getUuid(), $imageRepositories)
]]);
$this->assertResponseStatusCodeSame(201);
@ -102,11 +103,11 @@ class ImageTest extends AbstractTest
ImageFactory::createOne(['name' => self::IMAGE_CREATE]);
$iri = $this->findIriBy(Image::class, ['name' => self::IMAGE_CREATE]);
$imageRepositories = ImageRepositoryFactory::createMany(5);
$imageRepositories = ImageImageRepositoryFactory::createMany(5);
$this->createClientWithCredentials()->request('PUT', $iri, ['json' => [
'name' => self::IMAGE_UPDATE,
'imageRepositories' => array_map(fn($repo) => '/image-repositories/'. $repo->getUuid(), $imageRepositories)
'imageImageRepositories' => array_map(fn($repo) => '/image-repositories/'. $repo->getUuid(), $imageRepositories)
]]);
$this->assertResponseIsSuccessful();