develop #26

Merged
maranda merged 22 commits from develop into main 2025-03-26 06:42:35 +01:00
39 changed files with 526 additions and 196 deletions

View File

@ -1,26 +1,38 @@
# Changelog
## [0.10.0] - 2025-03-25
### Added
- Nuevo endpoint ogRepository. Convertir imagen en imagen virtual.
- Nuevo endpoint ogRepository. Importar imágenes externas al sistema.
- Nuevo método para desplegar imagenes sin cache.
---
## [0.9.5] - 2025-03-19
### Added
- Jenkinsfile updated to publish in repo
---
## [0.9.4] - 2025-03-17
### Fixed
- Mercure service behind nginx server for containers, expose port in docker compose for nginx
---
## [0.9.3] - 2025-03-17
### Fixed
- Mercure service behind nginx server for containers
---
## [0.9.2] - 2025-03-12
### Fixed
- Added mercure service in docker compose file for deployments.
---
## [0.9.1] - 2025-03-12
### 🐛 Fixed
### Fixed
- Corrección en la cancelacion de transmisiones p2p.
---
## [0.9.0] - 2025-03-04
### 🔹 Added
### Added
- Nueva funcionalidad para tener notificaciones en tiempo real. Instalación de bundle "Mercure".
- Creacion de EventListener en Symfony, para publicar mensajes en Mercure, cuando se realicen cambios en la base de datos ( cambio de estado en lo equipos, y trazas).
- Nuevo endpoint "backup image". Integracion con ogRepository.
@ -30,24 +42,24 @@
- Nueva funcionalidad para cancelar despliegues de imagenes.
- Añadido nuevo campo "cancelado" en trazas.
### Changed
### Changed
- Cambios en logs. Cambios en salida (stderror -> file.log)
- Modulo DHCP. Añadir equipos, ahora se gestiona con una unica llamada a la API.
- Acciones masivas en equipos. Se ha cambiado la respuesta para que no fallen las peticiones si uno o mas equipos no da respuesta.
---
## [0.8.1] - 2025-02-25
### 🐛 Fixed
### Fixed
- Corrección de bug en el deploy de imágenes
---
## [0.8.0] - 2025-01-10
### 🔹 Added
### Added
- Nuevos campos en "aulas" para la jerarquia en clientes.
- Nueva funcionalidad "imagen global". Integracion con ogRepository.
### Changed
### Changed
- Limpieza en campos "name" y "date" de ogLive. Es necesario parsear el campo "filename" para facilitar el uso al usuario en la web.
### 🐛 Fixed
- Corrección de bug que impedia borrar un cliente si tenia una traza enlazada.
@ -55,7 +67,7 @@
## [0.7.3] - 2025-01-03
### 🔹 Added
### Added
- Adaptados cambios en los endpoints para multiseleccion de clientes.
- Se agregó la funcionalidad de importar/exportar. Integración con ogRepository.
- Se agregó la funcionalidad de borrar imágenes. Integración con ogRepository.
@ -70,4 +82,5 @@
- **Added**: Secciones con nuevas características.
- **Fixed**: Corrección de errores y bugs.
- **Changed**: Modificaciones o mejoras en funcionalidades existentes.
- **Improved**: Mejoras en funcionalidades existentes.
- **Removed**: Funcionalidades o dependencias eliminadas.

View File

@ -56,6 +56,14 @@ resources:
uriTemplate: /image-image-repositories/{uuid}/backup-image
controller: App\Controller\OgRepository\Image\BackupImageAction
convert_image_to_virtual_image_ogrepository:
shortName: OgRepository Server
class: ApiPlatform\Metadata\Post
method: POST
input: App\Dto\Input\ConvertImageToVirtualInput
uriTemplate: /image-image-repositories/{uuid}/convert-image-to-virtual
controller: App\Controller\OgRepository\Image\ConvertImageToVirtualAction
trash_delete_image_ogrepository:
shortName: OgRepository Server
description: Delete Image in OgRepository
@ -88,7 +96,7 @@ resources:
description: Export Image in OgRepository
class: ApiPlatform\Metadata\Post
method: POST
input: App\Dto\Input\ExportImportImageRepositoryInput
input: App\Dto\Input\TransferGlobalImageInput
uriTemplate: /image-image-repositories/{uuid}/transfer-image
controller: App\Controller\OgRepository\Image\TransferAction
@ -101,6 +109,15 @@ resources:
uriTemplate: /image-image-repositories/server/{uuid}/status
controller: App\Controller\OgRepository\Image\GetStatusAction
transfer_global_image_repository:
shortName: OgRepository Server
description: Transfer Global Image in OgRepository
class: ApiPlatform\Metadata\Post
method: POST
input: false
uriTemplate: /image-image-repositories/server/{uuid}/transfer-global
controller: App\Controller\OgRepository\Image\TransferGlobalAction
properties:
App\Entity\ImageImageRepository:
id:

View File

@ -57,14 +57,23 @@ resources:
uriTemplate: /image-repositories/server/{uuid}/status
controller: App\Controller\OgRepository\StatusAction
export_image_ogrepository:
import_image_ogrepository:
shortName: OgRepository Server
description: Export Image in OgRepository
class: ApiPlatform\Metadata\Post
method: POST
input: App\Dto\Input\ExportImportImageRepositoryInput
uriTemplate: /image-repositories/{uuid}/export-image
controller: App\Controller\OgRepository\Image\ExportAction
input: App\Dto\Input\ImportImageRepositoryInput
uriTemplate: /image-repositories/{uuid}/import-image
controller: App\Controller\OgRepository\Image\ImportAction
convert_image_ogrepository:
shortName: OgRepository Server
description: Convert Image in OgRepository
class: ApiPlatform\Metadata\Post
method: POST
input: App\Dto\Input\ConvertImageRepositoryInput
uriTemplate: /image-repositories/{uuid}/convert-image
controller: App\Controller\OgRepository\Image\ConvertAction
properties:
App\Entity\ImageRepository:

View File

@ -12,6 +12,7 @@ framework:
handler_id: null
cookie_secure: auto
cookie_samesite: lax
storage_factory_id: session.storage.factory.native
#esi: true
#fragments: true

View File

@ -15,3 +15,11 @@ when@test:
framework:
profiler: { collect: false }
when@prod:
web_profiler:
toolbar: false
intercept_redirects: false
framework:
profiler: { collect: false }

View File

@ -82,7 +82,7 @@ services:
api_platform.filter.og_live.search:
parent: 'api_platform.doctrine.orm.search_filter'
arguments: [ { 'id': 'exact', 'name': 'partial', } ]
arguments: [ { 'id': 'exact', 'name': 'partial', 'status': 'exact' } ]
tags: [ 'api_platform.filter' ]
api_platform.filter.og_live.boolean:

View File

@ -15,7 +15,7 @@ use Symfony\Component\HttpKernel\Attribute\AsController;
use Symfony\Component\Routing\Annotation\Route;
#[AsController]
class OgAgentController extends AbstractController
class AgentSessionController extends AbstractController
{
public function __construct(
protected readonly EntityManagerInterface $entityManager

View File

@ -7,7 +7,7 @@ namespace App\Controller\OgBoot;
use App\Controller\OgBoot\PxeBootFile\PostAction;
use App\Service\Trace\CreateService;
use App\Service\Utils\ExtractOgLiveFilenameDateService;
use App\Service\Utils\SymflipyOgLiveFilenameService;
use App\Service\Utils\SimplifyOgLiveFilenameService;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
@ -34,7 +34,7 @@ abstract class AbstractOgBootController extends AbstractController
protected readonly EntityManagerInterface $entityManager,
protected readonly HttpClientInterface $httpClient,
protected readonly CreateService $createService,
protected readonly SymflipyOgLiveFilenameService $symflipyOgLiveFilenameService,
protected readonly SimplifyOgLiveFilenameService $simplifyOgLiveFilenameService,
protected readonly ExtractOgLiveFilenameDateService $extractOgLiveFilenameDateService,
)
{

View File

@ -30,7 +30,7 @@ class GetIsosAction extends AbstractOgBootController
}
$isos = array_map(function ($iso) {
$filename = $this->symflipyOgLiveFilenameService->__invoke($iso['filename']);
$filename = $this->simplifyOgLiveFilenameService->__invoke($iso['filename']);
return [
'id' => $iso['id'],

View File

@ -49,7 +49,7 @@ class InstallAction extends AbstractOgBootController
$this->createService->__invoke(null, CommandTypes::INSTALL_OGLIVE, TraceStatus::IN_PROGRESS, 'InstallOgLive_'.$data->getUuid(), $inputData);
$data->setName($this->symflipyOgLiveFilenameService->__invoke($data->getFilename()));
$data->setName($this->simplifyOgLiveFilenameService->__invoke($data->getFilename()));
$data->setDate(new \DateTime());
$data->setStatus(OgLiveStatus::PENDING);
$this->entityManager->persist($data);

View File

@ -20,16 +20,21 @@ use Symfony\Contracts\HttpClient\HttpClientInterface;
class SyncAction extends AbstractOgBootController
{
const string OG_BOOT_DIRECTORY = '/opt/opengnsys/ogboot/tftpboot//';
/**
* @throws TransportExceptionInterface
* @throws ServerExceptionInterface
* @throws RedirectionExceptionInterface
* @throws ClientExceptionInterface
* @throws \Exception
*/
public function __invoke(): JsonResponse
{
$content = $this->createRequest('GET', 'http://'.$this->ogBootApiUrl . '/ogboot/v1/oglives');
$allOgLives = $this->entityManager->getRepository(OgLive::class)->findAll();
$apiChecksums = array_map(fn($ogLive) => $ogLive['id'], $content['message']['installed_ogLives']);
foreach ($content['message']['installed_ogLives'] as $ogLive) {
$ogLiveEntity = $this->entityManager->getRepository(OgLive::class)->findOneBy(['checksum' => $ogLive['id']]);
@ -43,6 +48,17 @@ class SyncAction extends AbstractOgBootController
$this->extracted($ogLiveEntity, $ogLive);
$this->entityManager->persist($ogLiveEntity);
}
foreach ($allOgLives as $localOgLive) {
if ($localOgLive->getStatus() === OgLiveStatus::PENDING ) {
continue;
}
if (!in_array($localOgLive->getChecksum(), $apiChecksums)) {
$this->entityManager->remove($localOgLive);
}
}
$this->entityManager->flush();
if (isset($content['message']['default_oglive'])) {
@ -60,7 +76,7 @@ class SyncAction extends AbstractOgBootController
*/
private function extracted(OgLive $ogLiveEntity, mixed $ogLive): void
{
$ogLiveEntity->setName($this->symflipyOgLiveFilenameService->__invoke(str_replace(self::OG_BOOT_DIRECTORY, '', $ogLive['directory'])));
$ogLiveEntity->setName($this->simplifyOgLiveFilenameService->__invoke(str_replace(self::OG_BOOT_DIRECTORY, '', $ogLive['directory'])));
$ogLiveEntity->setDate(new \DateTime($this->extractOgLiveFilenameDateService->__invoke(str_replace(self::OG_BOOT_DIRECTORY, '', $ogLive['directory']))));
$ogLiveEntity->setInstalled(true);
$ogLiveEntity->setArchitecture($ogLive['architecture']);
@ -75,6 +91,13 @@ class SyncAction extends AbstractOgBootController
private function serDefaultOgLive(string $defaultOgLive): void
{
$oldDefaultOgLive = $this->entityManager->getRepository(OgLive::class)->findAll();
foreach ($oldDefaultOgLive as $oldOgLive) {
$oldOgLive->setIsDefault(false);
$this->entityManager->persist($oldOgLive);
}
$ogLiveEntity = $this->entityManager->getRepository(OgLive::class)->findOneBy(['filename' => $defaultOgLive]);
if (!$ogLiveEntity) {

View File

@ -6,7 +6,9 @@ use App\Controller\OgBoot\AbstractOgBootController;
use App\Entity\OgLive;
use App\Entity\Trace;
use App\Model\OgLiveStatus;
use App\Model\TraceStatus;
use App\Service\Utils\ExtractOgLiveFilenameDateService;
use App\Service\Utils\SimplifyOgLiveFilenameService;
use App\Service\Utils\SymflipyOgLiveFilenameService;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
@ -33,7 +35,7 @@ class InstallOgLiveResponseAction extends AbstractController
public function __construct(
protected readonly EntityManagerInterface $entityManager,
protected readonly LoggerInterface $logger,
protected readonly SymflipyOgLiveFilenameService $symflipyOgLiveFilenameService,
protected readonly SimplifyOgLiveFilenameService $simplifyOgLiveFilenameService,
protected readonly ExtractOgLiveFilenameDateService $extractOgLiveFilenameDateService,
)
{
@ -51,6 +53,8 @@ class InstallOgLiveResponseAction extends AbstractController
return new JsonResponse(['error' => 'Invalid JSON data'], Response::HTTP_BAD_REQUEST);
}
$this->logger->info('OgLive Webhook data received: '.json_encode($data));
$data = $data['webhookData'];
if ($data === null || !isset($data['message'], $data['ogCoreId'], $data['status'])) {
@ -69,8 +73,9 @@ class InstallOgLiveResponseAction extends AbstractController
}
if ($trace) {
$trace->setStatus($status === self::OG_LIVE_INSTALL_SUCCESS ? 'success' : 'failure');
$trace->setStatus($status === self::OG_LIVE_INSTALL_SUCCESS ? TraceStatus::SUCCESS : TraceStatus::FAILED);
$this->entityManager->persist($trace);
$this->entityManager->flush();
}
if ($ogLive->getStatus() === OgLiveStatus::ACTIVE) {
@ -86,12 +91,12 @@ class InstallOgLiveResponseAction extends AbstractController
/**
* @throws \Exception
*/
private function updateOgLive (OgLive $ogLive, array $details, string $status): void
private function updateOgLive (OgLive $ogLive, mixed $details, string $status): void
{
if ($status === self::OG_LIVE_INSTALL_SUCCESS) {
$ogLive->setName($this->symflipyOgLiveFilenameService->__invoke($details['filename']));
$ogLive->setDate(new \DateTime($this->extractOgLiveFilenameDateService->__invoke($details['filename'])));
$ogLive->setFilename(str_replace(self::OG_BOOT_DIRECTORY, '', $ogLive['directory']));
if ( is_array($details) && $status === self::OG_LIVE_INSTALL_SUCCESS) {
$ogLive->setName($this->simplifyOgLiveFilenameService->__invoke($details['directory']));
$ogLive->setDate(new \DateTime($this->extractOgLiveFilenameDateService->__invoke($details['directory'])));
$ogLive->setFilename(str_replace(self::OG_BOOT_DIRECTORY, '', $details['directory']));
$ogLive->setInstalled(true);
$ogLive->setSynchronized(true);
$ogLive->setChecksum($details['id']);
@ -100,21 +105,20 @@ class InstallOgLiveResponseAction extends AbstractController
$ogLive->setArchitecture($details['architecture']);
$ogLive->setRevision($details['revision']);
$ogLive->setDirectory($details['directory']);
$oldDefaultOgLive = $this->entityManager->getRepository(OgLive::class)->findAll();
foreach ($oldDefaultOgLive as $oldOgLive) {
$oldOgLive->setIsDefault(false);
$this->entityManager->persist($oldOgLive);
}
$ogLive->setIsDefault(true);
$this->entityManager->persist($ogLive);
}
$ogLive->setStatus($status === self::OG_LIVE_INSTALL_SUCCESS ? OgLiveStatus::ACTIVE : OgLiveStatus::FAILED);
$ogLive->setInstalled($status === self::OG_LIVE_INSTALL_SUCCESS);
$oldDefaultOgLive = $this->entityManager->getRepository(OgLive::class)->findBy(['isDefault' => true]);
foreach ($oldDefaultOgLive as $oldOgLive) {
$oldOgLive->setIsDefault(false);
$this->entityManager->persist($oldOgLive);
}
$ogLive->setIsDefault(true);
$this->entityManager->persist($ogLive);
$this->entityManager->flush();
}
}

View File

@ -55,7 +55,7 @@ class BackupImageAction extends AbstractOgRepositoryController
$inputData = [
'imageName' => $image->getName(),
'repositoryUuid' => $repository->getUuid(),
'imageUuid' => $imageImageRepository->getUuid(),
'imageImageRepositoryUuid' => $imageImageRepository->getUuid(),
'ID_img' => $imageImageRepository->getImageFullsum(),
'repo_ip' => $input->repoIp,
'remote_path' => $input->remotePath

View File

@ -59,7 +59,7 @@ class CancelTransmissionAction extends AbstractOgRepositoryController
$content = $this->createRequest('DELETE', 'http://'.$image->getRepository()->getIp().':8006/ogrepository/v1/'.$method.'/images/'.$image->getImageFullsum());
}
if (isset($content['error']) && $content['error'] === Response::HTTP_INTERNAL_SERVER_ERROR ) {
if (isset($content['error']) && $content['code'] === Response::HTTP_INTERNAL_SERVER_ERROR ) {
throw new ValidatorException('Error cancelling transmission');
}

View File

@ -0,0 +1,87 @@
<?php
namespace App\Controller\OgRepository\Image;
use App\Controller\OgRepository\AbstractOgRepositoryController;
use App\Dto\Input\ConvertImageRepositoryInput;
use App\Dto\Input\ImportImageRepositoryInput;
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\HttpKernel\Exception\NotFoundHttpException;
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;
#[AsController]
class ConvertAction extends AbstractOgRepositoryController
{
/**
* @throws TransportExceptionInterface
* @throws ServerExceptionInterface
* @throws RedirectionExceptionInterface
* @throws ClientExceptionInterface
*/
public function __invoke(ConvertImageRepositoryInput $input, ImageRepository $repository): JsonResponse
{
$fileSystem = $input->filesystem;
$image = pathinfo($input->name, PATHINFO_FILENAME);
$imageEntity = $this->entityManager->getRepository(Image::class)->findOneBy(['name' => $image]);
if (!$imageEntity){
$imageEntity = new Image();
$imageEntity->setName($image);
$imageEntity->setRemotePc(false);
$imageEntity->setIsGlobal(false);
$this->entityManager->persist($imageEntity);
}
$imageImageRepositoryEntity = $this->entityManager->getRepository(ImageImageRepository::class)->findOneBy(['image' => $imageEntity, 'repository' => $repository]);
if ($imageImageRepositoryEntity){
throw new ValidatorException('This image already exists in this repository');
}
$imageImageRepositoryEntity = new ImageImageRepository();
$imageImageRepositoryEntity->setStatus(ImageStatus::PENDING);
$imageImageRepositoryEntity->setImage($imageEntity);
$imageImageRepositoryEntity->setRepository($repository);
$this->entityManager->persist($imageImageRepositoryEntity);
$this->entityManager->flush();
$this->logger->info('Converting image', ['image' => $image]);
$params = [
'json' => [
'filesystem' => $fileSystem,
'virtual_image' => $input->name
]
];
$content = $this->createRequest('POST', 'http://'.$repository->getIp().':8006/ogrepository/v1/images/virtual', $params);
if (isset($content['error']) && $content['code'] === Response::HTTP_INTERNAL_SERVER_ERROR ) {
throw new ValidatorException('Error converting image');
}
$inputData = [
'imageName' => $image,
'imageImageRepositoryUuid' => $imageImageRepositoryEntity->getUuid(),
];
$this->createService->__invoke(null, CommandTypes::CONVERT_IMAGE, TraceStatus::IN_PROGRESS, $content['job_id'], $inputData);
return new JsonResponse(data: [], status: Response::HTTP_OK);
}
}

View File

@ -0,0 +1,66 @@
<?php
namespace App\Controller\OgRepository\Image;
use App\Controller\OgRepository\AbstractOgRepositoryController;
use App\Dto\Input\BackupImageInput;
use App\Dto\Input\ConvertImageToVirtualInput;
use App\Entity\ImageImageRepository;
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;
#[AsController]
class ConvertImageToVirtualAction extends AbstractOgRepositoryController
{
/**
* @throws TransportExceptionInterface
* @throws ServerExceptionInterface
* @throws RedirectionExceptionInterface
* @throws ClientExceptionInterface
*/
public function __invoke(ConvertImageToVirtualInput $input, ImageImageRepository $imageImageRepository): JsonResponse
{
$image = $imageImageRepository->getImage();
if (!$image->getName()) {
throw new ValidatorException('Name is required');
}
$params = [
'json' => [
'ID_img' => $imageImageRepository->getImageFullsum(),
'vm_extension' => $input->extension,
]
];
$this->logger->info('Convert image to virtual', ['image' => $image->getName()]);
$repository = $imageImageRepository->getRepository();
$content = $this->createRequest('PUT', 'http://'.$repository->getIp().':8006/ogrepository/v1/images/virtual', $params);
$inputData = [
'imageName' => $image->getName(),
'repositoryUuid' => $repository->getUuid(),
'imageImageRepositoryUuid' => $imageImageRepository->getUuid(),
'ID_img' => $imageImageRepository->getImageFullsum(),
];
$this->createService->__invoke($image->getClient(), CommandTypes::CONVERT_IMAGE_TO_VIRTUAL, TraceStatus::IN_PROGRESS, $content['job_id'], $inputData);
$imageImageRepository->setStatus(ImageStatus::TRANSFERRING);
$this->entityManager->persist($imageImageRepository);
$this->entityManager->flush();
return new JsonResponse(data: $content, status: Response::HTTP_OK);
}
}

View File

@ -51,7 +51,7 @@ class CreateAuxFilesAction extends AbstractOgRepositoryController
$inputData = [
'imageName' => $image->getName(),
'imageUuid' => $data->getUuid(),
'imageImageRepositoryUuid' => $data->getUuid(),
];
$this->createService->__invoke($image->getClient(), CommandTypes::CREATE_IMAGE_AUX_FILE, TraceStatus::IN_PROGRESS, $content['job_id'], $inputData);

View File

@ -1,69 +0,0 @@
<?php
namespace App\Controller\OgRepository\Image;
use App\Controller\OgRepository\AbstractOgRepositoryController;
use App\Dto\Input\ExportImportImageRepositoryInput;
use App\Entity\Image;
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;
#[AsController]
class ExportAction extends AbstractOgRepositoryController
{
/**
* @throws TransportExceptionInterface
* @throws ServerExceptionInterface
* @throws RedirectionExceptionInterface
* @throws ClientExceptionInterface
*/
public function __invoke(ExportImportImageRepositoryInput $input, ImageRepository $repository): JsonResponse
{
$images = $input->images;
foreach ($images as $imageEntity) {
/** @var Image $image */
$image = $imageEntity->getEntity();
if (!$image->getImageFullsum()) {
throw new ValidatorException('Fullsum is required');
}
$params = [
'json' => [
'ID_img' => $image->getImageFullsum(),
'repo_ip' => $repository->getIp(),
'user' => 'opengnsys',
]
];
$this->logger->info('Exporting image', ['image' => $image->getName(), 'repository' => $repository->getIp()]);
$content = $this->createRequest('PUT', 'http://'.$image->getRepository()->getIp().':8006/ogrepository/v1/repo/images', $params);
$inputData = [
'imageName' => $image->getName(),
'imageUuid' => $image->getUuid(),
'repositoryUuid' => $repository->getUuid(),
];
$this->createService->__invoke($image->getClient(), CommandTypes::EXPORT_IMAGE, TraceStatus::IN_PROGRESS, $content['job_id'], $inputData);
$image->setStatus(ImageStatus::TRANSFERING);
$this->entityManager->persist($image);
$this->entityManager->flush();
}
return new JsonResponse(data: [], status: Response::HTTP_OK);
}
}

View File

@ -0,0 +1,84 @@
<?php
namespace App\Controller\OgRepository\Image;
use App\Controller\OgRepository\AbstractOgRepositoryController;
use App\Dto\Input\ImportImageRepositoryInput;
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\HttpKernel\Exception\NotFoundHttpException;
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;
#[AsController]
class ImportAction extends AbstractOgRepositoryController
{
/**
* @throws TransportExceptionInterface
* @throws ServerExceptionInterface
* @throws RedirectionExceptionInterface
* @throws ClientExceptionInterface
*/
public function __invoke(ImportImageRepositoryInput $input, ImageRepository $repository): JsonResponse
{
$image = $input->name;
$imageEntity = $this->entityManager->getRepository(Image::class)->findOneBy(['name' => $image]);
if (!$imageEntity){
$imageEntity = new Image();
$imageEntity->setName($image);
$imageEntity->setRemotePc(false);
$imageEntity->setIsGlobal(false);
$this->entityManager->persist($imageEntity);
}
$imageImageRepositoryEntity = $this->entityManager->getRepository(ImageImageRepository::class)->findOneBy(['image' => $imageEntity, 'repository' => $repository]);
if ($imageImageRepositoryEntity){
throw new ValidatorException('This image already exists in this repository');
}
$imageImageRepositoryEntity = new ImageImageRepository();
$imageImageRepositoryEntity->setStatus(ImageStatus::AUX_FILES_PENDING);
$imageImageRepositoryEntity->setImage($imageEntity);
$imageImageRepositoryEntity->setRepository($repository);
$this->entityManager->persist($imageImageRepositoryEntity);
$this->entityManager->flush();
$this->logger->info('Creating aux files', ['image' => $image]);
$params = [
'json' => [
'image' => $image.'.img'
]
];
$content = $this->createRequest('POST', 'http://'.$repository->getIp().':8006/ogrepository/v1/images/torrentsum', $params);
if (isset($content['error']) && $content['code'] === Response::HTTP_INTERNAL_SERVER_ERROR ) {
throw new ValidatorException('Error importing image');
}
$inputData = [
'imageName' => $image,
'imageUuid' => $imageImageRepositoryEntity->getUuid(),
];
$this->createService->__invoke(null, CommandTypes::CREATE_IMAGE_AUX_FILE, TraceStatus::IN_PROGRESS, $content['job_id'], $inputData);
return new JsonResponse(data: [], status: Response::HTTP_OK);
}
}

View File

@ -4,6 +4,7 @@ namespace App\Controller\OgRepository\Image;
use App\Controller\OgRepository\AbstractOgRepositoryController;
use App\Dto\Input\ExportImportImageRepositoryInput;
use App\Dto\Input\TransferGlobalImageInput;
use App\Entity\Image;
use App\Entity\ImageImageRepository;
use App\Entity\ImageRepository;
@ -28,7 +29,7 @@ class TransferAction extends AbstractOgRepositoryController
* @throws RedirectionExceptionInterface
* @throws ClientExceptionInterface
*/
public function __invoke(ExportImportImageRepositoryInput $input, ImageImageRepository $imageImageRepository): JsonResponse
public function __invoke(TransferGlobalImageInput $input, ImageImageRepository $imageImageRepository): JsonResponse
{
$repositories = $input->repositories;
@ -45,7 +46,7 @@ class TransferAction extends AbstractOgRepositoryController
$params = [
'json' => [
'image' => $image->getName().'.img',
'repo_ip' => $image->getClient()->getRepository()->getIp(),
'repo_ip' => $imageImageRepository->getRepository()->getIp(),
'user' => 'opengnsys',
]
];

View File

@ -21,7 +21,7 @@ use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
#[AsController]
class TransferIsGlobalAction extends AbstractOgRepositoryController
class TransferGlobalAction extends AbstractOgRepositoryController
{
/**
* @throws TransportExceptionInterface
@ -29,26 +29,25 @@ class TransferIsGlobalAction extends AbstractOgRepositoryController
* @throws RedirectionExceptionInterface
* @throws ClientExceptionInterface
*/
public function __invoke(?array $repositories = [], Image $image): JsonResponse
public function __invoke(ImageImageRepository $imageImageRepository): JsonResponse
{
$repositories = $this->entityManager->getRepository(ImageRepository::class)->findAll();
foreach ($repositories as $repository) {
try {
$imageImageRepository = $this->entityManager->getRepository(ImageImageRepository::class)->findOneBy(['image' => $image, 'repository' => $repository]);
if ($imageImageRepository) {
$content = $this->createRequest('GET', 'http://'.$repository->getIp().':8006/ogrepository/v1/images/'.$imageImageRepository->getImageFullsum());
$this->logger->info('Image already exists', ['image' => $imageImageRepository->getImage()->getName(), 'repository' => $repository->getIp()]);
continue;
}
} catch ( \Exception $e) {
$hasImage = $this->entityManager->getRepository(ImageImageRepository::class)
->findOneBy(['image' => $imageImageRepository->getImage(), 'repository' => $repository]);
if ($hasImage) {
continue;
}
$image = $imageImageRepository->getImage();
$params = [
'json' => [
'image' => $image->getName().'.img',
'repo_ip' => $image->getClient()->getRepository()->getIp(),
'repo_ip' => $imageImageRepository->getRepository()->getIp(),
'user' => 'opengnsys',
]
];
@ -64,14 +63,18 @@ class TransferIsGlobalAction extends AbstractOgRepositoryController
$inputData = [
'imageName' => $image->getName(),
'imageUuid' => $image->getUuid(),
//'imageImageRepositoryUuid' => $imageImageRepository?->getUuid(),
'imageImageRepositoryUuid' => $imageImageRepository?->getUuid(),
'repositoryUuid' => $repository->getUuid(),
];
$this->createService->__invoke($image->getClient(), CommandTypes::TRANSFER_IMAGE, TraceStatus::IN_PROGRESS, $content['job_id'], $inputData);
//$imageImageRepository->setStatus(ImageStatus::TRANSFERRING);
$image->setIsGlobal(true);
$this->entityManager->persist($image);
$imageImageRepository->setStatus(ImageStatus::TRANSFERRING);
$this->entityManager->persist($imageImageRepository);
$this->entityManager->flush();
}

View File

@ -25,14 +25,24 @@ class SyncAction extends AbstractOgRepositoryController
*/
public function __invoke(ImageRepository $input): JsonResponse
{
$content = $this->createRequest('GET', 'http://'.$input->getIp(). ':8006/ogrepository/v1/images');
$content = $this->createRequest('GET', 'http://' . $input->getIp() . ':8006/ogrepository/v1/images');
if (!isset($content['output']['REPOSITORY']['images'])) {
return new JsonResponse(data: 'No images found', status: Response::HTTP_NOT_FOUND);
}
$repository = $this->entityManager->getRepository(ImageImageRepository::class);
$existingImages = $repository->findBy(['repository' => $input]);
$newImageFullsums = array_column($content['output']['REPOSITORY']['images'], 'fullsum');
foreach ($content['output']['REPOSITORY']['images'] as $image) {
$imageImageRepositoryEntity = $this->entityManager->getRepository(ImageImageRepository::class)->findOneBy(['imageFullsum' => $image['fullsum'], 'repository' => $input]);
$imageImageRepositoryEntity = $repository->findOneBy([
'imageFullsum' => $image['fullsum'],
'repository' => $input
]);
$imageEntity = $this->entityManager->getRepository(Image::class)->findOneBy(['name' => $image['name']]);
if (!$imageEntity) {
@ -53,10 +63,17 @@ class SyncAction extends AbstractOgRepositoryController
$this->entityManager->persist($imageImageRepositoryEntity);
}
}
foreach ($existingImages as $existingImage) {
if (!in_array($existingImage->getImageFullsum(), $newImageFullsums)) {
$this->entityManager->remove($existingImage);
}
}
$this->entityManager->flush();
return new JsonResponse(data: $content, status: Response::HTTP_OK);
}
}

View File

@ -37,6 +37,8 @@ class ResponseController extends AbstractOgRepositoryController
str_starts_with($action, "CreateAuxiliarFiles_") => $this->handleImageRepositoryAction($data, true),
str_starts_with($action, "TransferImage_"), str_starts_with($action, "ExportImage_") => $this->processImageAction($data),
str_starts_with($action, "BackupImage_") => $this->handleImageRepositoryAction($data),
str_starts_with($action, "ConvertImageToVirtual") => $this->handleImageRepositoryAction($data),
str_starts_with($action, "ConvertImageFromVirtual") => $this->handleImageRepositoryAction($data),
default => $this->jsonResponseError('Invalid action', Response::HTTP_BAD_REQUEST),
};
}
@ -65,15 +67,21 @@ class ResponseController extends AbstractOgRepositoryController
$trace = $this->getTrace($data['job_id']);
if (!$trace) return $this->jsonResponseError('Trace not found');
$image = $this->getImage($trace);
$repository = $this->getRepository($trace);
$originImageImageRepository = $this->getImageImageRepository($trace);
if (!$image) return $this->jsonResponseError('Image not found', Response::HTTP_NOT_FOUND, $trace);
if (!$repository) return $this->jsonResponseError('Repository not found', Response::HTTP_NOT_FOUND, $trace);
if (!$originImageImageRepository) return $this->jsonResponseError('ImageImageRepository not found', Response::HTTP_NOT_FOUND, $trace);
$originImageImageRepository->setStatus(ImageStatus::SUCCESS);
$this->entityManager->persist($originImageImageRepository);
$this->entityManager->flush();
if ($data['success'] !== true) {
return $this->jsonResponseError('Action failed', Response::HTTP_BAD_REQUEST, $trace);
}
$image = $this->getImage($trace);
$repository = $this->getRepository($trace);
if (!$image) return $this->jsonResponseError('Image not found', Response::HTTP_NOT_FOUND, $trace);
if (!$repository) return $this->jsonResponseError('Repository not found', Response::HTTP_NOT_FOUND, $trace);
$newImageRepo = new ImageImageRepository();
$newImageRepo->setImage($image);
$newImageRepo->setRepository($repository);
@ -127,7 +135,7 @@ class ResponseController extends AbstractOgRepositoryController
private function getImageImageRepository(Trace $trace): ?ImageImageRepository
{
return $this->entityManager->getRepository(ImageImageRepository::class)
->findOneBy(['uuid' => $trace->getInput()['imageUuid']]);
->findOneBy(['uuid' => $trace->getInput()['imageImageRepositoryUuid']]);
}
private function jsonResponseError(string $message, int $status = Response::HTTP_BAD_REQUEST, ?Trace $trace = null): JsonResponse

View File

@ -0,0 +1,19 @@
<?php
namespace App\Dto\Input;
use App\Dto\Output\ImageOutput;
use App\Dto\Output\ImageRepositoryOutput;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;
class ConvertImageRepositoryInput
{
#[Assert\NotNull]
#[Groups(['repository:write'])]
public ?string $name = '';
#[Assert\NotNull]
#[Groups(['repository:write'])]
public ?string $filesystem = '';
}

View File

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

View File

@ -13,6 +13,6 @@ class ExportImportImageRepositoryInput
* @var ImageOutput[]
*/
#[Assert\NotNull]
#[Groups(['image-image-repository:write'])]
public array $repositories = [];
#[Groups(['repository:write'])]
public array $images = [];
}

View File

@ -16,10 +16,13 @@ use Symfony\Component\Validator\Constraints as Assert;
final class ImageRepositoryInput
{
#[Assert\NotBlank]
#[Groups(['repository:write'])]
#[ApiProperty(description: 'The name of the repository', example: "Repository 1")]
public ?string $name = null;
#[Assert\NotBlank]
#[Assert\Ip]
#[Groups(['repository:write'])]
#[ApiProperty(description: 'The IP of the repository', example: "")]
public ?string $ip = null;

View File

@ -0,0 +1,15 @@
<?php
namespace App\Dto\Input;
use App\Dto\Output\ImageOutput;
use App\Dto\Output\ImageRepositoryOutput;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;
class ImportImageRepositoryInput
{
#[Assert\NotNull]
#[Groups(['repository:write'])]
public ?string $name = '';
}

View File

@ -0,0 +1,18 @@
<?php
namespace App\Dto\Input;
use App\Dto\Output\ImageOutput;
use App\Dto\Output\ImageRepositoryOutput;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;
class TransferGlobalImageInput
{
/**
* @var ImageOutput[]
*/
#[Assert\NotNull]
#[Groups(['image-image-repository:write'])]
public array $repositories = [];
}

View File

@ -21,10 +21,10 @@ final class ImageOutput extends AbstractOutput
#[Groups(['image:read'])]
public ?string $comments = '';
#[Groups(['image:read'])]
#[Groups(['image:read', 'image-image-repository:read'])]
public ?bool $remotePc = null;
#[Groups(['image:read'])]
#[Groups(['image:read', 'image-image-repository:read'])]
public ?bool $isGlobal = null;
#[Groups(['image:read'])]

View File

@ -18,9 +18,6 @@ class ImageRepositoryOutput extends AbstractOutput
#[Groups(['repository:read'])]
public ?string $comments = '';
#[Groups(['repository:read'])]
public ?bool $status = true;
#[Groups(['repository:read'])]
public \DateTime $createdAt;
@ -34,7 +31,6 @@ class ImageRepositoryOutput extends AbstractOutput
$this->name = $imageRepository->getName();
$this->ip = $imageRepository->getIp();
$this->comments = $imageRepository->getComments();
$this->status = $status;
$this->createdAt = $imageRepository->getCreatedAt();
$this->createdBy = $imageRepository->getCreatedBy();
}

View File

@ -3,7 +3,7 @@
namespace App\EventSubscriber;
use ApiPlatform\Symfony\EventListener\EventPriorities;
use App\Controller\OgRepository\Image\TransferIsGlobalAction;
use App\Controller\OgRepository\Image\TransferGlobalAction;
use App\Dto\Output\ImageRepositoryOutput;
use App\Dto\Output\OrganizationalUnitOutput;
use App\Entity\Image;
@ -21,8 +21,8 @@ use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
final readonly class ImageRepositorySubscriber implements EventSubscriberInterface
{
public function __construct(
private EntityManagerInterface $entityManager,
private readonly TransferIsGlobalAction $transferIsGlobalAction,
private EntityManagerInterface $entityManager,
private readonly TransferGlobalAction $transferIsGlobalAction,
)
{
@ -54,7 +54,7 @@ final readonly class ImageRepositorySubscriber implements EventSubscriberInterfa
$imagesToImport = $this->entityManager->getRepository(Image::class)->findBy(['isGlobal' => true]);
foreach($imagesToImport as $imageToImport) {
$this->transferIsGlobalAction->__invoke([$imageToImport], $imageRepositoryOutput->getEntity());
//$this->transferIsGlobalAction->__invoke($imageRepositoryOutput->getEntity());
}
}
}

View File

@ -7,10 +7,12 @@ final class CommandTypes
public const string DEPLOY_IMAGE = 'deploy-image';
public const string RESTORE_IMAGE = 'restore-image';
public const string CREATE_IMAGE = 'create-image';
public const string CONVERT_IMAGE = 'convert-image';
public const string CREATE_IMAGE_AUX_FILE = 'create-image-aux-file';
public const string BACKUP_IMAGE = 'backup-image';
public const string IMPORT_IMAGE = 'import-image';
public const string EXPORT_IMAGE = 'export-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';
public const string REBOOT = 'reboot';
@ -18,17 +20,18 @@ final class CommandTypes
public const string LOGIN = 'login';
public const string LOGOUT = 'logout';
public const string PARTITION_AND_FORMAT = 'partition-and-format';
public const string INSTALL_OGLIVE = 'install-oglive';
private const array COMMAND_TYPES = [
self::DEPLOY_IMAGE => 'Deploy Image',
self::RESTORE_IMAGE => 'Update Cache',
self::CREATE_IMAGE => 'Create Image',
self::CREATE_IMAGE_AUX_FILE => 'Crear fichero auxiliar en repositorio',
self::BACKUP_IMAGE => 'Crear backup de imagen',
self::IMPORT_IMAGE => 'Importar imagen',
self::EXPORT_IMAGE => 'Exportar imagen',
self::CONVERT_IMAGE => 'Convert Image',
self::CONVERT_IMAGE_TO_VIRTUAL => 'Convert Image to Virtual',
self::CREATE_IMAGE_AUX_FILE => 'Create Image Aux File',
self::BACKUP_IMAGE => 'Backup Image',
self::IMPORT_IMAGE => 'Import image',
self::EXPORT_IMAGE => 'Export image',
self::POWER_ON => 'Encender',
self::REBOOT => 'Reiniciar',
self::SHUTDOWN => 'Apagar',

View File

@ -8,7 +8,7 @@ final class DeployMethodTypes
public const string MULTICAST_UFTP = 'uftp';
public const string MULTICAST_UFTP_DIRECT = 'uftp-direct';
public const string MULTICAST_UDPCAST = 'udpcast';
public const string MULTICAST_UDPCAST_DIRECT = 'udp-direct';
public const string MULTICAST_UDPCAST_DIRECT = 'udpcast-direct';
public const string UNICAST = 'unicast';
public const string UNICAST_DIRECT = 'unicast-direct';
public const string TORRENT = 'p2p';

View File

@ -8,13 +8,12 @@ final class OgLiveStatus
public const string ACTIVE = 'active';
public const string INACTIVE = 'inactive';
public const string DELETED = 'deleted';
public const string FAILED = 'failed';
private const array OG_LIVE_STATUSES = [
self::PENDING => 'Pendiente',
self::ACTIVE => 'Activo',
self::INACTIVE => 'Inactivo',
self::PENDING => 'Instalando',
self::ACTIVE => 'Instalada',
self::INACTIVE => 'Sin instalar',
self::DELETED => 'Eliminado',
self::FAILED => 'Fallido',
];

View File

@ -0,0 +1,28 @@
<?php
namespace App\Service\Utils;
class SimplifyOgLiveFilenameService
{
private const string PATTERN = '/^ogLive-([^-]+)-(.+)-([^-]+)-r([0-9]+)(?:\.([a-f0-9]+))?_([0-9]+)(?:\.iso)?$/';
public function __invoke(string $filename): ?string
{
if (!preg_match(self::PATTERN, $filename, $matches)) {
return null;
}
$distro = $matches[1];
$kernelFull = $matches[2];
$arch = $matches[3];
$revision = $matches[4];
$commit = $matches[5] ?? null;
$date = $matches[6];
$kernel = in_array($arch, ['amd64', 'i386']) ? $kernelFull : "$kernelFull-$arch";
$arch = in_array($arch, ['amd64', 'i386']) ? $arch : 'i386';
return 'ogLive-'.$kernelFull.'-'.$date;
}
}

View File

@ -1,15 +0,0 @@
<?php
namespace App\Service\Utils;
class SymflipyOgLiveFilenameService
{
public function __invoke(string $filename): string
{
if (preg_match('/^(.+)-r\d+\.[a-f0-9]+_(\d{8})(?:\.iso)?$/', $filename, $matches)) {
return "{$matches[1]}-{$matches[2]}";
}
return $filename;
}
}

View File

@ -11,7 +11,6 @@ use ApiPlatform\State\ProcessorInterface;
use ApiPlatform\Validator\ValidatorInterface;
use App\Controller\OgAgent\CreateImageAction;
use App\Controller\OgRepository\Image\TransferAction;
use App\Controller\OgRepository\Image\TransferIsGlobalAction;
use App\Dto\Input\ImageInput;
use App\Dto\Input\ImageRepositoryInput;
use App\Dto\Output\ImageOutput;
@ -27,7 +26,6 @@ readonly class ImageProcessor implements ProcessorInterface
private ImageRepository $imageRepository,
private ValidatorInterface $validator,
private CreateImageAction $createImageActionController,
private TransferIsGlobalAction $transferActionController
)
{
}
@ -68,11 +66,6 @@ readonly class ImageProcessor implements ProcessorInterface
if ($data->source !== 'input') {
$response = $this->createImageActionController->__invoke($image);
} else {
if ($data->isGlobal === true) {
$repositories = $this->imageRepositoryRepository->findAll();
$this->transferActionController->__invoke($repositories, $image);
}
}
$this->imageRepository->save($image);

View File

@ -9,23 +9,17 @@ use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Put;
use ApiPlatform\State\Pagination\TraversablePaginator;
use ApiPlatform\State\ProviderInterface;
use App\Controller\OgRepository\StatusAction;
use App\Dto\Input\ImageInput;
use App\Dto\Input\ImageRepositoryInput;
use App\Dto\Output\ImageOutput;
use App\Dto\Output\ImageRepositoryOutput;
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;
readonly class ImageRepositoryProvider implements ProviderInterface
{
public function __construct(
private ProviderInterface $collectionProvider,
private ProviderInterface $itemProvider,
private StatusAction $statusAction
private ProviderInterface $collectionProvider,
private ProviderInterface $itemProvider
)
{
}
@ -43,23 +37,13 @@ readonly class ImageRepositoryProvider implements ProviderInterface
}
}
/**
* @throws TransportExceptionInterface
* @throws ServerExceptionInterface
* @throws RedirectionExceptionInterface
* @throws ClientExceptionInterface
*/
private function provideCollection(Operation $operation, array $uriVariables = [], array $context = []): object
{
$paginator = $this->collectionProvider->provide($operation, $uriVariables, $context);
$items = new \ArrayObject();
foreach ($paginator->getIterator() as $item){
$statusResponse = $this->statusAction->__invoke($item);
$content = json_decode($statusResponse->getContent(), true);
$status = !isset($content['error']);
$items[] = new ImageRepositoryOutput($item, $status);
$items[] = new ImageRepositoryOutput($item);
}
return new TraversablePaginator($items, $paginator->getCurrentPage(), $paginator->getItemsPerPage(), $paginator->getTotalItems());