develop #21

Merged
maranda merged 31 commits from develop into main 2025-03-06 07:47:30 +01:00
78 changed files with 1585 additions and 180 deletions

6
.env
View File

@ -50,3 +50,9 @@ UDS_AUTH_USERNAME="natiqindel"
UDS_AUTH_PASSWORD="correct horse battery staple" UDS_AUTH_PASSWORD="correct horse battery staple"
UDS_URL=https://localhost:8087/uds/rest/ UDS_URL=https://localhost:8087/uds/rest/
###< UDS ### ###< UDS ###
###> symfony/mercure-bundle ###
MERCURE_URL=http://ogcore-mercure:3000/.well-known/mercure
MERCURE_PUBLIC_URL=http://ogcore-mercure:3000/.well-known/mercure
MERCURE_JWT_SECRET="!ChangeThisMercureHubJWTSecretKey!"
###< symfony/mercure-bundle ###

26
.env.prod 100644
View File

@ -0,0 +1,26 @@
###> symfony/framework-bundle ###
APP_ENV=prod
APP_SECRET=e95c7f17da15ce1b03d77ad655379c34
###< symfony/framework-bundle ###
###> doctrine/doctrine-bundle ###
DATABASE_URL="mysql://root:root@127.0.0.1:3306/ogcore?serverVersion=10.11.2-MariaDB&charset=utf8mb4"
OG_1_DATABASE_URL="mysql://root:root@127.0.0.1:3306/ogcore_old_og?serverVersion=10.11.2-MariaDB&charset=utf8mb4"
###< doctrine/doctrine-bundle ###
###> nelmio/cors-bundle ###
CORS_ALLOW_ORIGIN='*'
###< nelmio/cors-bundle ###
###> lexik/jwt-authentication-bundle ###
JWT_SECRET_KEY=%kernel.project_dir%/config/jwt/private.pem
JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem
JWT_PASSPHRASE=8b9154df37ffa91ef9186ce095324e39e50ff3b023bb1ed34383abd019ba4515
###< lexik/jwt-authentication-bundle ###
###> symfony/mercure-bundle ###
MERCURE_URL=http://localhost:3000/.well-known/mercure
MERCURE_PUBLIC_URL=http://localhost:3000/.well-known/mercure
MERCURE_JWT_SECRET="!ChangeThisMercureHubJWTSecretKey!"
###< symfony/mercure-bundle ###

View File

@ -1,4 +1,38 @@
# Changelog # Changelog
## [0.9.0] - 2025-03-04
### 🔹 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.
- Nuevo campo en "usuarios" el cual permite escoger la visualizacion por defecto de la vista "grupos".
- Nuevo campo "dns" en "subredes" para gestionar los servidores DNS.
- Integracion endpoint ogRepository para checkear la integridad de una imagen.
- Nueva funcionalidad para cancelar despliegues de imagenes.
- Añadido nuevo campo "cancelado" en trazas.
### ⚡ 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
- Corrección de bug en el deploy de imágenes
---
## [0.8.0] - 2025-01-10
### 🔹 Added
- Nuevos campos en "aulas" para la jerarquia en clientes.
- Nueva funcionalidad "imagen global". Integracion con ogRepository.
### ⚡ 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.
---
## [0.7.3] - 2025-01-03 ## [0.7.3] - 2025-01-03
### 🔹 Added ### 🔹 Added
@ -7,13 +41,9 @@
- Se agregó la funcionalidad de borrar imágenes. Integración con ogRepository. - Se agregó la funcionalidad de borrar imágenes. Integración con ogRepository.
- Se agregó el modo "TORRENT" y "UDPCAST" en el despliegue de imágenes. - Se agregó el modo "TORRENT" y "UDPCAST" en el despliegue de imágenes.
### 🛠️ Fixed
### ⚡ Changed ### ⚡ Changed
- Refactorización del webhook de ogRepository. - Refactorización del webhook de ogRepository.
### 🛑 Removed
--- ---
## Formato de cambios: ## Formato de cambios:

5
DEBIAN/changelog 100644
View File

@ -0,0 +1,5 @@
ogcore (1.0) unstable; urgency=low
* Initial release.
-- Your Name <your.email@example.com> Thu, 01 Jan 1970 00:00:00 +0000

12
DEBIAN/control 100644
View File

@ -0,0 +1,12 @@
Package: ogcore
Version: %%VERSION%%
Section: base
Priority: optional
Architecture: all
Depends: mariadb-server, systemd, nginx, libzip-dev, zip, unzip, php8.3-opcache,
php8.3-bcmath, php8.3-cli, php8.3-curl, php8.3-fpm, php8.3-gd,
php8.3-ldap, php8.3-mbstring, php8.3-mysql, php8.3-common,
php8.3-xml, php8.3-zip
Maintainer: Nicolas Arenas <nicolas.arenas@qindel.com>
Description: Description of the ogcore package
This is a longer description of the ogcore package.

21
DEBIAN/copyright 100644
View File

@ -0,0 +1,21 @@
Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: ogcore
Source: <source URL>
Files: *
Copyright: 2023 Your Name <your.email@example.com>
License: GPL-3+
This package is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or
(at your option) any later version.
.
This package is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
.
You should have received a copy of the GNU General Public License
along with this package; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
MA 02110-1301 USA.

54
DEBIAN/postinst 100644
View File

@ -0,0 +1,54 @@
#!/bin/bash
set -e
# Asegurarse de que el usuario exista
USER="opengnsys"
# Provisionar base de datos si es necesario en caso de instalación.
# Detectar si es una instalación nueva o una actualización
if [ "$1" = "configure" ] && [ -z "$2" ]; then
cd /opt/opengnsys/ogcore/api
mariadb -e "GRANT ALL ON *.* to 'root'@'localhost' IDENTIFIED BY 'root' WITH GRANT OPTION;"
echo "Primera instalación"
# Provisionar base de datos si es necesario en caso de instalación.
echo "Creando par de claves para JWT"
php bin/console lexik:jwt:generate-keypair --overwrite
echo "Creando base de datos y cargando datos iniciales"
php bin/console doctrine:database:create --if-not-exists
php bin/console doctrine:migrations:migrate --no-interaction
# Cargar datos iniciales
echo "Cargando datos iniciales"
php bin/console opengnsys:load-default-user
echo "Cargando usuarios y grupos por defecto"
php bin/console app:load-default-user-groups
echo "Cargando comandos por defecto"
php bin/console app:load-default-commands
echo "Cargando menú por defecto"
php bin/console opengnsys:load-default-menu
elif [ "$1" = "configure" ] && [ -n "$2" ]; then
cd /opt/opengnsys/ogcore/api
echo "Actualización desde la versión $2"
# Ejecutar comandos específicos para la actualización
php bin/console doctrine:migrations:migrate --no-interaction
# Otros comandos específicos para la actualización
fi
# Cambiar la propiedad de los archivos al usuario especificado
chown opengnsys:www-data /opt/opengnsys/
chown -R opengnsys:www-data /opt/opengnsys/ogcore
# Install http server stuff
ln -s /opt/opengnsys/ogcore/etc/nginx/sites-available/ogcore.conf /etc/nginx/sites-enabled/ogcore.conf
ln -s /opt/opengnsys/ogcore/etc/php/8.3/fpm/pool.d/ogcore-fpm.conf /etc/php/8.3/fpm/pool.d/ogcore-fpm.conf
# Reiniciar servicios si es necesario
# systemctl restart nombre_del_servicio
systemctl daemon-reload
systemctl restart nginx
systemctl restart php8.3-fpm
exit 0

15
DEBIAN/preinst 100644
View File

@ -0,0 +1,15 @@
#!/bin/bash
set -e
# Asegurarse de que el usuario exista
USER="opengnsys"
HOME_DIR="/opt/opengnsys"
if id "$USER" &>/dev/null; then
echo "El usuario $USER ya existe."
else
echo "Creando el usuario $USER con home en $HOME_DIR."
useradd -m -d "$HOME_DIR" -s /bin/bash "$USER"
fi
exit 0

View File

@ -5,3 +5,9 @@ services:
ports: ports:
- "5432" - "5432"
###< doctrine/doctrine-bundle ### ###< doctrine/doctrine-bundle ###
###> symfony/mercure-bundle ###
mercure:
ports:
- "80"
###< symfony/mercure-bundle ###

View File

@ -27,6 +27,7 @@
"symfony/flex": "^2", "symfony/flex": "^2",
"symfony/framework-bundle": "6.4.*", "symfony/framework-bundle": "6.4.*",
"symfony/http-client": "6.4.*", "symfony/http-client": "6.4.*",
"symfony/mercure-bundle": "^0.3.9",
"symfony/monolog-bundle": "^3.10", "symfony/monolog-bundle": "^3.10",
"symfony/property-access": "6.4.*", "symfony/property-access": "6.4.*",
"symfony/property-info": "6.4.*", "symfony/property-info": "6.4.*",

169
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "2df97d4a1797242acadb47d83b3fbe98", "content-hash": "ac4764e765324b86e616d7eb47a55e63",
"packages": [ "packages": [
{ {
"name": "api-platform/core", "name": "api-platform/core",
@ -4981,6 +4981,173 @@
], ],
"time": "2024-11-27T12:49:36+00:00" "time": "2024-11-27T12:49:36+00:00"
}, },
{
"name": "symfony/mercure",
"version": "v0.6.5",
"source": {
"type": "git",
"url": "https://github.com/symfony/mercure.git",
"reference": "304cf84609ef645d63adc65fc6250292909a461b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/mercure/zipball/304cf84609ef645d63adc65fc6250292909a461b",
"reference": "304cf84609ef645d63adc65fc6250292909a461b",
"shasum": ""
},
"require": {
"php": ">=7.1.3",
"symfony/deprecation-contracts": "^2.0|^3.0|^4.0",
"symfony/http-client": "^4.4|^5.0|^6.0|^7.0",
"symfony/http-foundation": "^4.4|^5.0|^6.0|^7.0",
"symfony/polyfill-php80": "^1.22",
"symfony/web-link": "^4.4|^5.0|^6.0|^7.0"
},
"require-dev": {
"lcobucci/jwt": "^3.4|^4.0|^5.0",
"symfony/event-dispatcher": "^4.4|^5.0|^6.0|^7.0",
"symfony/http-kernel": "^4.4|^5.0|^6.0|^7.0",
"symfony/phpunit-bridge": "^5.2|^6.0|^7.0",
"symfony/stopwatch": "^4.4|^5.0|^6.0|^7.0",
"twig/twig": "^2.0|^3.0|^4.0"
},
"suggest": {
"symfony/stopwatch": "Integration with the profiler performances"
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/dunglas/mercure",
"name": "dunglas/mercure"
},
"branch-alias": {
"dev-main": "0.6.x-dev"
}
},
"autoload": {
"psr-4": {
"Symfony\\Component\\Mercure\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Kévin Dunglas",
"email": "dunglas@gmail.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony Mercure Component",
"homepage": "https://symfony.com",
"keywords": [
"mercure",
"push",
"sse",
"updates"
],
"support": {
"issues": "https://github.com/symfony/mercure/issues",
"source": "https://github.com/symfony/mercure/tree/v0.6.5"
},
"funding": [
{
"url": "https://github.com/dunglas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/mercure",
"type": "tidelift"
}
],
"time": "2024-04-08T12:51:34+00:00"
},
{
"name": "symfony/mercure-bundle",
"version": "v0.3.9",
"source": {
"type": "git",
"url": "https://github.com/symfony/mercure-bundle.git",
"reference": "77435d740b228e9f5f3f065b6db564f85f2cdb64"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/mercure-bundle/zipball/77435d740b228e9f5f3f065b6db564f85f2cdb64",
"reference": "77435d740b228e9f5f3f065b6db564f85f2cdb64",
"shasum": ""
},
"require": {
"lcobucci/jwt": "^3.4|^4.0|^5.0",
"php": ">=7.1.3",
"symfony/config": "^4.4|^5.0|^6.0|^7.0",
"symfony/dependency-injection": "^4.4|^5.4|^6.0|^7.0",
"symfony/http-kernel": "^4.4|^5.0|^6.0|^7.0",
"symfony/mercure": "^0.6.1",
"symfony/web-link": "^4.4|^5.0|^6.0|^7.0"
},
"require-dev": {
"symfony/phpunit-bridge": "^4.3.7|^5.0|^6.0|^7.0",
"symfony/stopwatch": "^4.3.7|^5.0|^6.0|^7.0",
"symfony/ux-turbo": "*",
"symfony/var-dumper": "^4.3.7|^5.0|^6.0|^7.0"
},
"suggest": {
"symfony/messenger": "To use the Messenger integration"
},
"type": "symfony-bundle",
"extra": {
"branch-alias": {
"dev-main": "0.3.x-dev"
}
},
"autoload": {
"psr-4": {
"Symfony\\Bundle\\MercureBundle\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Kévin Dunglas",
"email": "dunglas@gmail.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony MercureBundle",
"homepage": "https://symfony.com",
"keywords": [
"mercure",
"push",
"sse",
"updates"
],
"support": {
"issues": "https://github.com/symfony/mercure-bundle/issues",
"source": "https://github.com/symfony/mercure-bundle/tree/v0.3.9"
},
"funding": [
{
"url": "https://github.com/dunglas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/mercure-bundle",
"type": "tidelift"
}
],
"time": "2024-05-31T09:07:18+00:00"
},
{ {
"name": "symfony/monolog-bridge", "name": "symfony/monolog-bridge",
"version": "v6.4.13", "version": "v6.4.13",

View File

@ -7,6 +7,7 @@ resources:
groups: ['default', 'client:read'] groups: ['default', 'client:read']
denormalizationContext: denormalizationContext:
groups: ['client:write'] groups: ['client:write']
operations: operations:
ApiPlatform\Metadata\GetCollection: ApiPlatform\Metadata\GetCollection:
provider: App\State\Provider\ClientProvider provider: App\State\Provider\ClientProvider

View File

@ -48,6 +48,14 @@ resources:
uriTemplate: /image-image-repositories/{uuid}/deploy-image uriTemplate: /image-image-repositories/{uuid}/deploy-image
controller: App\Controller\DeployImageAction controller: App\Controller\DeployImageAction
backup_image_ogrepository:
shortName: OgRepository Server
class: ApiPlatform\Metadata\Post
method: POST
input: App\Dto\Input\BackupImageInput
uriTemplate: /image-image-repositories/{uuid}/backup-image
controller: App\Controller\OgRepository\Image\BackupImageAction
trash_delete_image_ogrepository: trash_delete_image_ogrepository:
shortName: OgRepository Server shortName: OgRepository Server
description: Delete Image in OgRepository description: Delete Image in OgRepository
@ -84,6 +92,15 @@ resources:
uriTemplate: /image-image-repositories/{uuid}/transfer-image uriTemplate: /image-image-repositories/{uuid}/transfer-image
controller: App\Controller\OgRepository\Image\TransferAction controller: App\Controller\OgRepository\Image\TransferAction
get_status_image_ogrepository:
shortName: OgRepository Server
description: Get Status Image in OgRepository
class: ApiPlatform\Metadata\Post
method: POST
input: false
uriTemplate: /image-image-repositories/server/{uuid}/status
controller: App\Controller\OgRepository\Image\GetStatusAction
properties: properties:
App\Entity\ImageImageRepository: App\Entity\ImageImageRepository:
id: id:

View File

@ -12,6 +12,15 @@ resources:
ApiPlatform\Metadata\Get: ApiPlatform\Metadata\Get:
provider: App\State\Provider\TraceProvider provider: App\State\Provider\TraceProvider
cancel_trace:
shortName: OgRepository Server
description: Cancel Trace in OgRepository
class: ApiPlatform\Metadata\Post
method: POST
input: false
uriTemplate: /traces/server/{uuid}/cancel
controller: App\Controller\OgRepository\Image\CancelTransmissionAction
order: order:
createdAt: DESC createdAt: DESC

View File

@ -17,4 +17,5 @@ return [
Stof\DoctrineExtensionsBundle\StofDoctrineExtensionsBundle::class => ['all' => true], Stof\DoctrineExtensionsBundle\StofDoctrineExtensionsBundle::class => ['all' => true],
DAMA\DoctrineTestBundle\DAMADoctrineTestBundle::class => ['test' => true], DAMA\DoctrineTestBundle\DAMADoctrineTestBundle::class => ['test' => true],
Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true], Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
Symfony\Bundle\MercureBundle\MercureBundle::class => ['all' => true],
]; ];

View File

@ -5,6 +5,9 @@ api_platform:
path_segment_name_generator: api_platform.path_segment_name_generator.dash path_segment_name_generator: api_platform.path_segment_name_generator.dash
defaults: defaults:
pagination_client_items_per_page: true pagination_client_items_per_page: true
mercure:
enabled: true
collection: collection:
pagination: pagination:
items_per_page_parameter_name: 'itemsPerPage' items_per_page_parameter_name: 'itemsPerPage'

View File

@ -0,0 +1,8 @@
mercure:
hubs:
default:
url: '%env(MERCURE_URL)%'
public_url: '%env(MERCURE_PUBLIC_URL)%'
jwt:
secret: '%env(MERCURE_JWT_SECRET)%'
publish: '*'

View File

@ -8,17 +8,9 @@ when@dev:
main: main:
type: stream type: stream
level: info level: info
path: php://stderr path: "%kernel.logs_dir%/%kernel.environment%.log"
formatter: App\Formatter\CustomLineFormatter formatter: App\Formatter\CustomLineFormatter
channels: ["!event"] channels: ["!event"]
# uncomment to get logging in your browser
# you may have to allow bigger header sizes in your Web server configuration
#firephp:
# type: firephp
# level: info
#chromephp:
# type: chromephp
# level: info
console: console:
type: console type: console
process_psr_3_messages: false process_psr_3_messages: false
@ -28,11 +20,10 @@ when@test:
monolog: monolog:
handlers: handlers:
main: main:
type: fingers_crossed type: stream
action_level: error path: "%kernel.logs_dir%/%kernel.environment%.log"
handler: nested level: error
excluded_http_codes: [404, 405]
channels: ["!event"]
nested: nested:
type: stream type: stream
path: "%kernel.logs_dir%/%kernel.environment%.log" path: "%kernel.logs_dir%/%kernel.environment%.log"
@ -42,16 +33,11 @@ when@prod:
monolog: monolog:
handlers: handlers:
main: main:
type: fingers_crossed
action_level: error
handler: nested
excluded_http_codes: [404, 405]
buffer_size: 50 # How many messages should be saved? Prevent memory leaks
nested:
type: stream type: stream
path: php://stderr path: "%kernel.logs_dir%/%kernel.environment%.log"
level: debug level: error
formatter: monolog.formatter.json formatter: App\Formatter\CustomLineFormatter
channels: ["!event"]
console: console:
type: console type: console
process_psr_3_messages: false process_psr_3_messages: false
@ -59,5 +45,6 @@ when@prod:
deprecation: deprecation:
type: stream type: stream
channels: [deprecation] channels: [deprecation]
path: php://stderr path: "%kernel.logs_dir%/%kernel.environment%.log"
formatter: monolog.formatter.json formatter: monolog.formatter.json

View File

@ -20,7 +20,7 @@ services:
api_platform.filter.client.search: api_platform.filter.client.search:
parent: 'api_platform.doctrine.orm.search_filter' parent: 'api_platform.doctrine.orm.search_filter'
arguments: [ { 'id': 'exact', 'name': 'partial', 'serialNumber': 'exact', 'template.id': 'exact', organizationalUnit.id: 'exact', mac: 'exact', ip: 'exact' } ] arguments: [ { 'id': 'exact', 'uuid': exact, 'name': 'partial', 'serialNumber': 'exact', 'template.id': 'exact', status: 'exact', organizationalUnit.id: 'exact', mac: 'exact', ip: 'exact', subnet.id: 'exact' } ]
tags: [ 'api_platform.filter' ] tags: [ 'api_platform.filter' ]
api_platform.filter.client.exist: api_platform.filter.client.exist:

View File

@ -0,0 +1,3 @@
services:
App\EventListener\ClientStatusNotifier:
tags: ~ # Esto elimina el listener en el entorno de test

View File

@ -42,7 +42,31 @@ services:
networks: networks:
- ogcore-network - ogcore-network
mercure:
image: dunglas/mercure
restart: unless-stopped
container_name: ogcore-mercure
environment:
# Uncomment the following line to disable HTTPS,
SERVER_NAME: ':3000'
MERCURE_PUBLISHER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
MERCURE_SUBSCRIBER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
MERCURE_EXTRA_DIRECTIVES: |
cors_origins *
# Comment the following line to disable the development mode
command: /usr/bin/caddy run --config /etc/caddy/dev.Caddyfile
ports:
- "3000:3000"
volumes:
- mercure_data:/data
- mercure_config:/config
networks:
- ogcore-network
volumes: volumes:
mercure_data:
mercure_config:
database_data: database_data:
networks: networks:

View File

@ -15,6 +15,17 @@ server {
ssl_certificate /etc/nginx/certs/ogcore.uds-test.net.crt.pem; ssl_certificate /etc/nginx/certs/ogcore.uds-test.net.crt.pem;
ssl_certificate_key /etc/nginx/certs/ogcore.uds-test.net.key.pem; ssl_certificate_key /etc/nginx/certs/ogcore.uds-test.net.key.pem;
location /.well-known/mercure {
proxy_pass https://mercure:3000/.well-known/mercure;
proxy_read_timeout 24h;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /opengnsys/rest/ous// { location /opengnsys/rest/ous// {
rewrite ^/opengnsys/rest/ous//([0-9]+)/images /opengnsys/rest/ous/$1/images; rewrite ^/opengnsys/rest/ous//([0-9]+)/images /opengnsys/rest/ous/$1/images;
rewrite ^/opengnsys/rest/ous//([0-9]+)/labs /opengnsys/rest/ous/$1/labs; rewrite ^/opengnsys/rest/ous//([0-9]+)/labs /opengnsys/rest/ous/$1/labs;

View File

@ -0,0 +1,37 @@
server {
listen 8443 ssl;
server_name _;
root /opt/opengnsys/ogcore/api/public/;
index index.html index.php;
ssl_certificate /opt/opengnsys/ogcore/etc/nginx/certs/ogcore.uds-test.net.crt.pem;
ssl_certificate_key /opt/opengnsys/ogcore/etc/nginx/certs/ogcore.uds-test.net.key.pem;
location /opengnsys/rest/ous// {
rewrite ^/opengnsys/rest/ous//([0-9]+)/images /opengnsys/rest/ous/$1/images;
rewrite ^/opengnsys/rest/ous//([0-9]+)/labs /opengnsys/rest/ous/$1/labs;
}
# Bloque principal para archivos
location / {
try_files $uri $uri/ /index.php?$args;
}
# Manejo de PHP
location ~ \.php$ {
include fastcgi_params;
fastcgi_pass unix:/var/run/php/php8.3-fpm-ogcore.sock;
fastcgi_split_path_info ^(.+\.php)(/.*)$;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $request_uri;
fastcgi_param PATH_TRANSLATED $document_root$fastcgi_script_name;
}
# Bloque para errores PHP
location ~ \.php$ {
return 404;
}
error_log /var/log/nginx/ogcore-error.log;
access_log /var/log/nginx/ogcore-access.log;
}

View File

@ -0,0 +1,12 @@
[ogcore]
user = opengnsys
group = www-data
listen = /run/php/php8.3-fpm-ogcore.sock
listen.owner = opengnsys
listen.group = www-data
pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3

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 Version20250225081416 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE network_settings ADD netiface 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 network_settings DROP netiface');
}
}

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 Version20250227095120 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 user ADD groups_view 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 user DROP groups_view');
}
}

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 Version20250227154452 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 subnet ADD dns 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 subnet DROP dns');
}
}

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 Version20250304115209 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 user CHANGE groups_view groups_view 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 user CHANGE groups_view groups_view VARCHAR(255) NOT NULL');
}
}

38
package.sh 100755
View File

@ -0,0 +1,38 @@
#!/bin/bash
set -x
set -e
VERSION=$1
DISTDIR=ogcore-$VERSION/opt/opengnsys/ogcore
export COMPOSER_ALLOW_SUPERUSER=1
export APP_ENV=prod
rm -rf ogcore-$VERSION
cd ogcore
composer dump-env prod
composer install --no-dev --no-interaction --no-progress --optimize-autoloader
cd ..
# Crear directorios necesarios
mkdir -p $DISTDIR/api
mkdir -p $DISTDIR/etc/nginx/certs
mkdir -p $DISTDIR/etc/nginx/conf
for i in bin config migrations public src swagger-assets templates translations var vendor ; do
cp -pr ogcore/$i $DISTDIR/api/$i
done
cp -pr ogcore/DEBIAN ogcore-$VERSION/
cp -pr ogcore/etc $DISTDIR/
cp ogcore/docker/certs/* $DISTDIR/etc/nginx/certs/
chmod 755 ogcore-$VERSION/DEBIAN/postinst
chmod 755 ogcore-$VERSION/DEBIAN/preinst
cp ogcore/composer.json $DISTDIR/api
cp ogcore/composer.lock $DISTDIR/api
cp ogcore/symfony.lock $DISTDIR/api
cp ogcore/.env.local.php $DISTDIR/api
cp ogcore/env.json $DISTDIR/api
sed -i "s/%%VERSION%%/$VERSION/g" ogcore-$VERSION/DEBIAN/control
# Imprimir el tag actual
echo "Empaquetando TAG $TAG"
rm -f ogcore-$VERSION.deb
dpkg-deb --build ogcore-$VERSION

View File

@ -15,6 +15,7 @@
<server name="SHELL_VERBOSITY" value="-1" /> <server name="SHELL_VERBOSITY" value="-1" />
<server name="SYMFONY_PHPUNIT_REMOVE" value="" /> <server name="SYMFONY_PHPUNIT_REMOVE" value="" />
<server name="SYMFONY_PHPUNIT_VERSION" value="9.6" /> <server name="SYMFONY_PHPUNIT_VERSION" value="9.6" />
<env name="SYMFONY_DEPRECATIONS_HELPER" value="weak" />
</php> </php>
<testsuites> <testsuites>

View File

@ -91,5 +91,4 @@ class LoadDefaultCommandsCommand extends Command
return Command::SUCCESS; return Command::SUCCESS;
} }
} }

View File

@ -0,0 +1,44 @@
<?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

@ -60,6 +60,10 @@ class DeployImageAction extends AbstractController
]; ];
$agentJobId = $this->deployImageOgAgentAction->__invoke($image, $input, $client->getEntity(), DeployMethodTypes::UNICAST); $agentJobId = $this->deployImageOgAgentAction->__invoke($image, $input, $client->getEntity(), DeployMethodTypes::UNICAST);
if (!$agentJobId){
continue;
}
$this->createService->__invoke($client->getEntity(), CommandTypes::DEPLOY_IMAGE, TraceStatus::IN_PROGRESS, $agentJobId, $inputData); $this->createService->__invoke($client->getEntity(), CommandTypes::DEPLOY_IMAGE, TraceStatus::IN_PROGRESS, $agentJobId, $inputData);
} }
break; break;
@ -85,11 +89,14 @@ class DeployImageAction extends AbstractController
try { try {
$this->deployImageOgRepositoryAction->__invoke($input, $image, $client->getEntity(), $this->httpClient); $this->deployImageOgRepositoryAction->__invoke($input, $image, $client->getEntity(), $this->httpClient);
} catch (\Exception $e) { } catch (\Exception $e) {
//return new JsonResponse(data: ['error' => $e->getMessage()], status: Response::HTTP_INTERNAL_SERVER_ERROR);
continue; continue;
} }
$agentJobId = $this->deployImageOgAgentAction->__invoke($image, $input, $client->getEntity(), DeployMethodTypes::MULTICAST); $agentJobId = $this->deployImageOgAgentAction->__invoke($image, $input, $client->getEntity(), DeployMethodTypes::MULTICAST);
if (!$agentJobId){
continue;
}
$this->createService->__invoke($client->getEntity(), CommandTypes::DEPLOY_IMAGE, TraceStatus::IN_PROGRESS, $agentJobId, $inputData); $this->createService->__invoke($client->getEntity(), CommandTypes::DEPLOY_IMAGE, TraceStatus::IN_PROGRESS, $agentJobId, $inputData);
} }
break; break;
@ -109,11 +116,14 @@ class DeployImageAction extends AbstractController
try { try {
$this->deployImageOgRepositoryAction->__invoke($input, $image, $client->getEntity(), $this->httpClient); $this->deployImageOgRepositoryAction->__invoke($input, $image, $client->getEntity(), $this->httpClient);
} catch (\Exception $e) { } catch (\Exception $e) {
//return new JsonResponse(data: ['error' => $e->getMessage()], status: Response::HTTP_INTERNAL_SERVER_ERROR);
continue; continue;
} }
$agentJobId = $this->deployImageOgAgentAction->__invoke($image, $input, $client->getEntity(), DeployMethodTypes::TORRENT); $agentJobId = $this->deployImageOgAgentAction->__invoke($image, $input, $client->getEntity(), DeployMethodTypes::TORRENT);
if (!$agentJobId){
continue;
}
$this->createService->__invoke($client->getEntity(), CommandTypes::DEPLOY_IMAGE, TraceStatus::IN_PROGRESS, $agentJobId, $inputData); $this->createService->__invoke($client->getEntity(), CommandTypes::DEPLOY_IMAGE, TraceStatus::IN_PROGRESS, $agentJobId, $inputData);
} }

View File

@ -18,6 +18,7 @@ use App\Service\Trace\CreateService;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpClient\Exception\TransportException;
use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;
@ -39,6 +40,12 @@ class DeployImageAction extends AbstractController
{ {
} }
/**
* @throws RedirectionExceptionInterface
* @throws ClientExceptionInterface
* @throws TransportExceptionInterface
* @throws ServerExceptionInterface
*/
public function __invoke(ImageImageRepository $imageImageRepository, DeployImageInput $input, Client $client, string $method) public function __invoke(ImageImageRepository $imageImageRepository, DeployImageInput $input, Client $client, string $method)
{ {
$image = $imageImageRepository->getImage(); $image = $imageImageRepository->getImage();
@ -95,11 +102,15 @@ class DeployImageAction extends AbstractController
]); ]);
$this->logger->info('Deploying image', ['image' => $image->getId()]); $this->logger->info('Deploying image', ['image' => $image->getId()]);
} catch (TransportExceptionInterface $e) { $jobId = json_decode($response->getContent(), true)['job_id'];
$this->logger->error('Error deploying image', ['image' => $image->getId(), 'error' => $e->getMessage()]); } catch (ClientExceptionInterface | ServerExceptionInterface | TransportExceptionInterface | TransportException $e) {
$this->logger->error('Error deploying image', [
'image' => $image->getId() ?? 'unknown',
'error' => $e->getMessage()
]);
return null;
} }
$jobId = json_decode($response->getContent(), true)['job_id'];
$client->setStatus(ClientStatus::BUSY); $client->setStatus(ClientStatus::BUSY);
$this->entityManager->persist($client); $this->entityManager->persist($client);

View File

@ -17,6 +17,7 @@ use App\Service\Trace\CreateService;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpClient\Exception\TransportException;
use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;
@ -41,12 +42,17 @@ class PowerOffAction extends AbstractController
public function __invoke(MultipleClientsInput $input): JsonResponse public function __invoke(MultipleClientsInput $input): JsonResponse
{ {
foreach ($input->clients as $clientEntity) { foreach ($input->clients as $clientEntity) {
/** @var Client $client */
$client = $clientEntity->getEntity(); $client = $clientEntity->getEntity();
if (!$client->getIp()) { if (!$client->getIp()) {
throw new ValidatorException('IP is required'); throw new ValidatorException('IP is required');
} }
if ($client->getStatus() === ClientStatus::OFF) {
continue;
}
$data = [ $data = [
'nfn' => 'Apagar', 'nfn' => 'Apagar',
'ids' => '0' 'ids' => '0'
@ -62,21 +68,23 @@ class PowerOffAction extends AbstractController
'json' => $data, 'json' => $data,
]); ]);
$this->logger->info('Powering off client', ['client' => $client->getId()]); $this->logger->info('Powering off client', ['client' => $client->getId()]);
$jobId = json_decode($response->getContent(), true)['job_id'];
} catch (TransportExceptionInterface $e) { } catch (ClientExceptionInterface | ServerExceptionInterface | TransportExceptionInterface | TransportException $e) {
$this->logger->error('Error powering off client', ['client' => $client->getId(), 'error' => $e->getMessage()]); $this->logger->error('Error power off client', [
'image' => $client->getIp(),
'error' => $e->getMessage()
]);
continue; continue;
} }
$jobId = json_decode($response->getContent(), true)['job_id']; $client->setStatus(ClientStatus::TURNING_OFF);
$client->setStatus(ClientStatus::OFF);
$this->entityManager->persist($client); $this->entityManager->persist($client);
$this->entityManager->flush(); $this->entityManager->flush();
$this->createService->__invoke($client, CommandTypes::SHUTDOWN, TraceStatus::SUCCESS, $jobId, []); $this->createService->__invoke($client, CommandTypes::SHUTDOWN, TraceStatus::SUCCESS, $jobId, []);
} }
return new JsonResponse(data: $client, status: Response::HTTP_OK); return new JsonResponse(data: [], status: Response::HTTP_OK);
} }
} }

View File

@ -34,6 +34,11 @@ use Symfony\Contracts\HttpClient\HttpClientInterface;
#[AsController] #[AsController]
class ClientsController extends AbstractController class ClientsController extends AbstractController
{ {
const string CREATE_IMAGE = 'RESPUESTA_CrearImagen';
const string RESTORE_IMAGE = 'RESPUESTA_RestaurarImagen';
const string CONFIGURE_IMAGE = 'RESPUESTA_Configurar';
public function __construct( public function __construct(
protected readonly EntityManagerInterface $entityManager, protected readonly EntityManagerInterface $entityManager,
public readonly CreateAuxFilesAction $createAuxFilesAction, public readonly CreateAuxFilesAction $createAuxFilesAction,
@ -54,7 +59,7 @@ class ClientsController extends AbstractController
public function index(Request $request): JsonResponse public function index(Request $request): JsonResponse
{ {
$data = $request->toArray(); $data = $request->toArray();
$requiredFields = ['nfn', 'res', 'der', 'job_id']; $requiredFields = ['job_id'];
foreach ($requiredFields as $field) { foreach ($requiredFields as $field) {
if (!isset($data[$field])) { if (!isset($data[$field])) {
@ -64,7 +69,16 @@ class ClientsController extends AbstractController
$this->logger->info('Webhook data received', $data); $this->logger->info('Webhook data received', $data);
if ($data['nfn'] === 'RESPUESTA_CrearImagen') { if (isset($data['progress'])){
$trace = $this->entityManager->getRepository(Trace::class)->findOneBy(['jobId' => $data['job_id']]);
if ($trace){
$trace->setProgress($data['progress'] * 1000);
$this->entityManager->persist($trace);
$this->entityManager->flush();
}
}
if (isset($data['nfn']) && $data['nfn'] === self::CREATE_IMAGE) {
$trace = $this->entityManager->getRepository(Trace::class)->findOneBy(['jobId' => $data['job_id']]); $trace = $this->entityManager->getRepository(Trace::class)->findOneBy(['jobId' => $data['job_id']]);
/** @var ImageImageRepository $imageImageRepository */ /** @var ImageImageRepository $imageImageRepository */
$imageImageRepository = $this->entityManager->getRepository(ImageImageRepository::class)->findOneBy(['uuid' => $data['idi']]); $imageImageRepository = $this->entityManager->getRepository(ImageImageRepository::class)->findOneBy(['uuid' => $data['idi']]);
@ -82,10 +96,8 @@ class ClientsController extends AbstractController
if ($data['res'] === 1) { if ($data['res'] === 1) {
$trace->setStatus(TraceStatus::SUCCESS); $trace->setStatus(TraceStatus::SUCCESS);
$trace->setFinishedAt(new \DateTime()); $trace->setFinishedAt(new \DateTime());
$imageImageRepository->setStatus(ImageStatus::AUX_FILES_PENDING); $imageImageRepository->setStatus(ImageStatus::AUX_FILES_PENDING);
$imageImageRepository->setCreated(true); $imageImageRepository->setCreated(true);
$this->entityManager->persist($imageImageRepository); $this->entityManager->persist($imageImageRepository);
$this->logger->info('Start partition creation. ', ['image' => (string) $imageImageRepository->getUuid()]); $this->logger->info('Start partition creation. ', ['image' => (string) $imageImageRepository->getUuid()]);
@ -95,11 +107,13 @@ class ClientsController extends AbstractController
$this->logger->info('Starting software profile creation. ', ['image' => (string) $imageImageRepository->getUuid()]); $this->logger->info('Starting software profile creation. ', ['image' => (string) $imageImageRepository->getUuid()]);
$this->createSoftwareProfile($data['inv_sft'], $imageImageRepository); $this->createSoftwareProfile($data['inv_sft'], $imageImageRepository);
$this->logger->info('Start aux files ogrepo API ', ['image' => (string) $imageImageRepository->getUuid()]); $this->logger->info('Start aux files ogrepo API ', ['image' => (string) $imageImageRepository->getUuid()]);
try { try {
$this->createAuxFilesAction->__invoke($imageImageRepository); $this->createAuxFilesAction->__invoke($imageImageRepository);
} catch (\Exception $e) { } catch (\Exception $e) {
$this->logger->error('Error creating aux files', ['image' => (string) $imageImageRepository->getUuid(), 'error' => $e->getMessage()]); $this->logger->error('Error creating aux files', ['image' => (string) $imageImageRepository->getUuid(), 'error' => $e->getMessage()]);
} }
$this->logger->info('End aux files ogrepo API ', ['image' => (string) $imageImageRepository->getUuid()]); $this->logger->info('End aux files ogrepo API ', ['image' => (string) $imageImageRepository->getUuid()]);
} else { } else {
$trace->setStatus(TraceStatus::FAILED); $trace->setStatus(TraceStatus::FAILED);
@ -119,9 +133,8 @@ class ClientsController extends AbstractController
$this->logger->info('Image updated. Success.', ['image' => (string) $imageImageRepository->getUuid()]); $this->logger->info('Image updated. Success.', ['image' => (string) $imageImageRepository->getUuid()]);
} }
if ($data['nfn'] === 'RESPUESTA_RestaurarImagen'|| $data['nfn'] === 'RESPUESTA_Configurar') { if (isset($data['nfn']) && ($data['nfn'] === self::RESTORE_IMAGE || $data['nfn'] === self::CONFIGURE_IMAGE)) {
$trace = $this->entityManager->getRepository(Trace::class)->findOneBy(['jobId' => $data['job_id']]); $trace = $this->entityManager->getRepository(Trace::class)->findOneBy(['jobId' => $data['job_id']]);
$client = $trace->getClient(); $client = $trace->getClient();
if ($data['res'] === 1) { if ($data['res'] === 1) {

View File

@ -46,7 +46,7 @@ class PostAction extends AbstractOgBootController
'router' => $client->getOrganizationalUnit()->getNetworkSettings()->getRouter(), 'router' => $client->getOrganizationalUnit()->getNetworkSettings()->getRouter(),
'netmask' => $client->getOrganizationalUnit()->getNetworkSettings() ? $client->getOrganizationalUnit()->getNetworkSettings()->getNetmask() : '255.255.255.0', 'netmask' => $client->getOrganizationalUnit()->getNetworkSettings() ? $client->getOrganizationalUnit()->getNetworkSettings()->getNetmask() : '255.255.255.0',
'computer_name' => $client->getName(), 'computer_name' => $client->getName(),
'netiface' => $client->getNetiface(), 'netiface' => $client->getNetiface() ? $client->getNetiface() : $client->getOrganizationalUnit()->getNetworkSettings()->getNetiface(),
'group' => $client->getOrganizationalUnit()->getName(), 'group' => $client->getOrganizationalUnit()->getName(),
'ogrepo' => $ogRepoIp, 'ogrepo' => $ogRepoIp,
'ogcore' => $this->ogCoreIP, 'ogcore' => $this->ogCoreIP,

View File

@ -33,6 +33,7 @@ class PostAction extends AbstractOgDhcpController
'nextServer' => $data->getNextServer(), 'nextServer' => $data->getNextServer(),
'bootFileName' => $data->getBootFileName(), 'bootFileName' => $data->getBootFileName(),
'router' => $data->getRouter(), 'router' => $data->getRouter(),
'DNS' => $data->getDns()
] ]
]; ];

View File

@ -29,27 +29,54 @@ class PostHostAction extends AbstractOgDhcpController
*/ */
public function __invoke(SubnetAddHostInput $input, Subnet $subnet): JsonResponse public function __invoke(SubnetAddHostInput $input, Subnet $subnet): JsonResponse
{ {
$client = $input->client; $clients = $input->clients;
/** @var Client $clientEntity */ $success = [];
$clientEntity = $client->getEntity(); $errors = [];
$data = [ foreach ($clients as $client) {
'host' => $clientEntity->getName(), /** @var Client $clientEntity */
'macAddress' => strtolower($clientEntity->getMac()), $clientEntity = $client->getEntity();
'address' => $clientEntity->getIp(),
];
$params = [ $data = [
'json' => $data 'host' => $clientEntity->getName(),
]; 'macAddress' => strtolower($clientEntity->getMac()),
'address' => $clientEntity->getIp(),
];
$content = $this->createRequest('POST', 'http://'.$this->ogDhcpApiUrl.'/ogdhcp/v1/subnets/'.$subnet->getServerId().'/hosts', $params); $params = ['json' => $data];
$subnet->addClient($clientEntity); try {
$this->entityManager->persist($subnet); $content = $this->createRequest(
$this->entityManager->flush(); 'POST',
'http://' . $this->ogDhcpApiUrl . '/ogdhcp/v1/subnets/' . $subnet->getServerId() . '/hosts',
$params
);
return new JsonResponse(data: $content, status: Response::HTTP_OK); // Guardar resultado exitoso
$success[] = [
'client' => $clientEntity->getName(),
'response' => $content
];
// Persistir solo si la llamada fue exitosa
$subnet->addClient($clientEntity);
$this->entityManager->persist($subnet);
$this->entityManager->flush();
} catch (\Throwable $e) { // Capturar cualquier error sin interrumpir
$errors[] = [
'client' => $clientEntity->getName(),
'error' => $e->getMessage()
];
}
}
return new JsonResponse(
[
'success' => $success,
'errors' => $errors
],
empty($errors) ? Response::HTTP_OK : Response::HTTP_MULTI_STATUS
);
} }
} }

View File

@ -63,7 +63,7 @@ class SyncAction extends AbstractOgDhcpController
{ {
$getParsedData = $this->getIpAddressAndNetmaskFromCIDRService->__invoke($subnet['subnet']); $getParsedData = $this->getIpAddressAndNetmaskFromCIDRService->__invoke($subnet['subnet']);
$subnetEntity->setName($subnet['id'].' - '.$subnet['subnet']); $subnetEntity->setName('Subred -'.$subnet['id']);
$subnetEntity->setBootFileName($subnet['boot-file-name'] ?? null); $subnetEntity->setBootFileName($subnet['boot-file-name'] ?? null);
$subnetEntity->setIpAddress($getParsedData['ip']); $subnetEntity->setIpAddress($getParsedData['ip']);
$subnetEntity->setNetmask($getParsedData['mask']); $subnetEntity->setNetmask($getParsedData['mask']);

View File

@ -55,6 +55,7 @@ abstract class AbstractOgRepositoryController extends AbstractController
$this->logger->error(sprintf('Client/Server error in request to %s: %s', $url, $e->getMessage())); $this->logger->error(sprintf('Client/Server error in request to %s: %s', $url, $e->getMessage()));
return [ return [
'code' => Response::HTTP_INTERNAL_SERVER_ERROR,
'error' => 'Client/Server error', 'error' => 'Client/Server error',
'details' => $e->getMessage(), 'details' => $e->getMessage(),
]; ];
@ -62,6 +63,7 @@ abstract class AbstractOgRepositoryController extends AbstractController
$this->logger->error(sprintf('Transport error in request to %s: %s', $url, $e->getMessage())); $this->logger->error(sprintf('Transport error in request to %s: %s', $url, $e->getMessage()));
return [ return [
'code' => Response::HTTP_INTERNAL_SERVER_ERROR,
'error' => 'Transport error', 'error' => 'Transport error',
'details' => $e->getMessage(), 'details' => $e->getMessage(),
]; ];

View File

@ -0,0 +1,73 @@
<?php
namespace App\Controller\OgRepository\Image;
use App\Controller\OgRepository\AbstractOgRepositoryController;
use App\Dto\Input\BackupImageInput;
use App\Dto\Input\DeleteImageInput;
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;
#[AsController]
class BackupImageAction extends AbstractOgRepositoryController
{
/**
* @throws TransportExceptionInterface
* @throws ServerExceptionInterface
* @throws RedirectionExceptionInterface
* @throws ClientExceptionInterface
*/
public function __invoke(BackupImageInput $input, ImageImageRepository $imageImageRepository): JsonResponse
{
$image = $imageImageRepository->getImage();
if (!$image->getName()) {
throw new ValidatorException('Name is required');
}
$params = [
'json' => [
'ID_img' => $imageImageRepository->getImageFullsum(),
'repo_ip' => $input->repoIp,
'remote_path' => $input->remotePath,
'user' => 'opengnsys'
]
];
$this->logger->info('Create backup image', ['image' => $image->getName()]);
$repository = $imageImageRepository->getRepository();
$content = $this->createRequest('PUT', 'http://'.$repository->getIp().':8006/ogrepository/v1/repo/images', $params);
$inputData = [
'imageName' => $image->getName(),
'repositoryUuid' => $repository->getUuid(),
'imageUuid' => $imageImageRepository->getUuid(),
'ID_img' => $imageImageRepository->getImageFullsum(),
'repo_ip' => $input->repoIp,
'remote_path' => $input->remotePath
];
$this->createService->__invoke($image->getClient(), CommandTypes::BACKUP_IMAGE, TraceStatus::IN_PROGRESS, $content['job_id'], $inputData);
$imageImageRepository->setStatus(ImageStatus::BACKUP);
$this->entityManager->persist($imageImageRepository);
$this->entityManager->flush();
return new JsonResponse(data: $content, status: Response::HTTP_OK);
}
}

View File

@ -0,0 +1,67 @@
<?php
namespace App\Controller\OgRepository\Image;
use App\Controller\OgRepository\AbstractOgRepositoryController;
use App\Entity\Client;
use App\Entity\Image;
use App\Entity\ImageImageRepository;
use App\Entity\Trace;
use App\Model\CommandTypes;
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 CancelTransmissionAction extends AbstractOgRepositoryController
{
/**
* @throws TransportExceptionInterface
* @throws ServerExceptionInterface
* @throws RedirectionExceptionInterface
* @throws ClientExceptionInterface
*/
public function __invoke(Trace $data): JsonResponse
{
if ($data->getCommand() !== CommandTypes::DEPLOY_IMAGE) {
throw new ValidatorException('Command is not DEPLOY_IMAGE');
}
$input = $data->getInput();
if (!isset($input['client']) || !isset($input['image']) || !isset($input['method'])) {
throw new ValidatorException('Client, image and method are required');
}
$client = $this->entityManager->getRepository(Client::class)->findOneBy(['uuid' => $input['client']]);
$image = $this->entityManager->getRepository(ImageImageRepository::class)->findOneBy(['uuid' => $input['image']]);
if (!$client || !$image) {
throw new ValidatorException('Client or image not found');
}
$method = $input['method'];
if (!$image->getImageFullsum()) {
throw new ValidatorException('Fullsum is required');
}
$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 ) {
throw new ValidatorException('Error cancelling transmission');
}
$data->setStatus(TraceStatus::CANCELLED);
$this->entityManager->persist($data);
$this->entityManager->flush();
return new JsonResponse(data: $content, status: Response::HTTP_OK);
}
}

View File

@ -45,7 +45,7 @@ class CreateAuxFilesAction extends AbstractOgRepositoryController
$this->logger->info('Creating aux files', ['image' => $image->getName()]); $this->logger->info('Creating aux files', ['image' => $image->getName()]);
$repository = $image->getClient()->getRepository(); $repository = $data->getRepository();
$content = $this->createRequest('POST', 'http://'.$repository->getIp().':8006/ogrepository/v1/images/torrentsum', $params); $content = $this->createRequest('POST', 'http://'.$repository->getIp().':8006/ogrepository/v1/images/torrentsum', $params);
@ -56,8 +56,6 @@ class CreateAuxFilesAction extends AbstractOgRepositoryController
$this->createService->__invoke($image->getClient(), CommandTypes::CREATE_IMAGE_AUX_FILE, TraceStatus::IN_PROGRESS, $content['job_id'], $inputData); $this->createService->__invoke($image->getClient(), CommandTypes::CREATE_IMAGE_AUX_FILE, TraceStatus::IN_PROGRESS, $content['job_id'], $inputData);
$this->logger->info('Aux files created successfully', ['image' => $image->getName()]);
$data->setStatus(ImageStatus::AUX_FILES_PENDING); $data->setStatus(ImageStatus::AUX_FILES_PENDING);
$this->entityManager->persist($data); $this->entityManager->persist($data);
$this->entityManager->flush(); $this->entityManager->flush();

View File

@ -37,8 +37,6 @@ class DeleteTrashAction extends AbstractOgRepositoryController
$this->logger->info('Deleting image', ['image' => $image->getName()]); $this->logger->info('Deleting image', ['image' => $image->getName()]);
$repository = $image->getClient()->getRepository();
$content = $this->createRequest('DELETE', 'http://'.$repository->getIp().':8006/ogrepository/v1/images/'.$imageImageRepository->getImageFullsum().'?method=trash'); $content = $this->createRequest('DELETE', 'http://'.$repository->getIp().':8006/ogrepository/v1/images/'.$imageImageRepository->getImageFullsum().'?method=trash');
$this->logger->info('Image deleted', ['image' => $image->getName()]); $this->logger->info('Image deleted', ['image' => $image->getName()]);

View File

@ -0,0 +1,41 @@
<?php
namespace App\Controller\OgRepository\Image;
use App\Controller\OgRepository\AbstractOgRepositoryController;
use App\Entity\Image;
use App\Entity\ImageImageRepository;
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 GetStatusAction extends AbstractOgRepositoryController
{
/**
* @throws TransportExceptionInterface
* @throws ServerExceptionInterface
* @throws RedirectionExceptionInterface
* @throws ClientExceptionInterface
*/
public function __invoke(ImageImageRepository $data): JsonResponse
{
if (!$data->getImageFullsum()) {
throw new ValidatorException('Fullsum is required');
}
$content = $this->createRequest('GET', 'http://'.$data->getRepository()->getIp().':8006/ogrepository/v1/status/images/'.$data->getImageFullsum());
if (isset($content['error']) && $content['error'] === Response::HTTP_INTERNAL_SERVER_ERROR ) {
throw new ValidatorException('Error getting status');
}
return new JsonResponse(data: $content, status: Response::HTTP_OK);
}
}

View File

@ -44,7 +44,7 @@ class RecoverAction extends AbstractOgRepositoryController
$this->logger->info('Recovering image', ['image' => $image->getName()]); $this->logger->info('Recovering image', ['image' => $image->getName()]);
$repository = $image->getClient()->getRepository(); $repository = $data->getRepository();
$content = $this->createRequest('POST', 'http://'.$repository->getIp().':8006/ogrepository/v1/trash/images', $params); $content = $this->createRequest('POST', 'http://'.$repository->getIp().':8006/ogrepository/v1/trash/images', $params);

View File

@ -7,7 +7,6 @@ use App\Entity\Image;
use App\Entity\ImageImageRepository; use App\Entity\ImageImageRepository;
use App\Entity\ImageRepository; use App\Entity\ImageRepository;
use App\Entity\Trace; use App\Entity\Trace;
use App\Model\CommandTypes;
use App\Model\ImageStatus; use App\Model\ImageStatus;
use App\Model\TraceStatus; use App\Model\TraceStatus;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
@ -18,117 +17,81 @@ use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\AsController; use Symfony\Component\HttpKernel\Attribute\AsController;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
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\Exception\TransportExceptionInterface;
#[AsController] #[AsController]
class ResponseController extends AbstractOgRepositoryController class ResponseController extends AbstractOgRepositoryController
{ {
/**
* @throws RedirectionExceptionInterface
* @throws ClientExceptionInterface
* @throws TransportExceptionInterface
* @throws ServerExceptionInterface
*/
#[Route('/og-repository/webhook', name: 'og_repository_webhook', methods: ['POST'])] #[Route('/og-repository/webhook', name: 'og_repository_webhook', methods: ['POST'])]
public function repositoryWebhook(Request $request): JsonResponse public function repositoryWebhook(Request $request): JsonResponse
{ {
$data = json_decode($request->getContent(), true); $data = json_decode($request->getContent(), true);
if (!isset($data['job_id'])) { if (!isset($data['job_id'])) {
return new JsonResponse(['message' => 'Invalid request'], Response::HTTP_BAD_REQUEST); return $this->jsonResponseError('Invalid request', Response::HTTP_BAD_REQUEST);
} }
$action = $data['job_id']; $action = $data['job_id'];
if (str_starts_with($action, "CreateAuxiliarFiles_")) { return match (true) {
$this->handleCreateAuxFiles($data); str_starts_with($action, "CreateAuxiliarFiles_") => $this->handleImageRepositoryAction($data, true),
} elseif (str_starts_with($action, "TransferImage_")) { str_starts_with($action, "TransferImage_"), str_starts_with($action, "ExportImage_") => $this->processImageAction($data),
$this->processImageAction($data, 'transfer'); str_starts_with($action, "BackupImage_") => $this->handleImageRepositoryAction($data),
} elseif (str_starts_with($action, "ExportImage_")) { default => $this->jsonResponseError('Invalid action', Response::HTTP_BAD_REQUEST),
$this->processImageAction($data, 'export'); };
} else {
return new JsonResponse(['message' => 'Invalid action'], Response::HTTP_BAD_REQUEST);
}
return new JsonResponse($data, Response::HTTP_OK);
} }
private function handleCreateAuxFiles(array $data): void private function handleImageRepositoryAction(array $data, bool $setFullsum = false): JsonResponse
{ {
$trace = $this->entityManager->getRepository(Trace::class)->findOneBy(['jobId' => $data['job_id']]); $trace = $this->getTrace($data['job_id']);
$imageUuid = $trace->getInput()['imageUuid']; if (!$trace) return $this->jsonResponseError('Trace not found');
/* @var ImageImageRepository $imageImageRepository */ $imageImageRepository = $this->getImageImageRepository($trace);
$imageImageRepository = $this->entityManager->getRepository(ImageImageRepository::class)->findOneBy(['uuid' => $imageUuid]); if (!$imageImageRepository) return $this->jsonResponseError('Image not found', Response::HTTP_NOT_FOUND, $trace);
if ($imageImageRepository === null) { if ($setFullsum) {
$this->updateTraceStatus($trace, TraceStatus::FAILED, 'Image not found'); $imageImageRepository->setImageFullsum($data['image_id']);
return;
} }
$imageImageRepository->setImageFullsum($data['image_id']);
$imageImageRepository->setStatus(ImageStatus::SUCCESS); $imageImageRepository->setStatus(ImageStatus::SUCCESS);
$this->entityManager->persist($imageImageRepository);
$this->entityManager->persist($imageImageRepository);
$this->updateTraceStatus($trace, TraceStatus::SUCCESS); $this->updateTraceStatus($trace, TraceStatus::SUCCESS);
return new JsonResponse(['message' => 'Success'], Response::HTTP_OK);
} }
/** private function processImageAction(array $data): JsonResponse
* @throws TransportExceptionInterface
* @throws ServerExceptionInterface
* @throws RedirectionExceptionInterface
* @throws ClientExceptionInterface
*/
private function processImageAction(array $data, string $actionType): void
{ {
$trace = $this->entityManager->getRepository(Trace::class)->findOneBy(['jobId' => $data['job_id']]); $trace = $this->getTrace($data['job_id']);
$imageUuid = $trace->getInput()['imageUuid']; if (!$trace) return $this->jsonResponseError('Trace not found');
$repositoryUuid = $trace->getInput()['repositoryUuid'];
$image = $this->entityManager->getRepository(Image::class)->findOneBy(['uuid' => $imageUuid]);
$repository = $this->entityManager->getRepository(ImageRepository::class)->findOneBy(['uuid' => $repositoryUuid]);
if ($data['success'] !== true) { if ($data['success'] !== true) {
$this->updateTraceStatus($trace, TraceStatus::FAILED, 'Action failed'); return $this->jsonResponseError('Action failed', Response::HTTP_BAD_REQUEST, $trace);
return;
} }
if ($image === null) { $image = $this->getImage($trace);
$this->updateTraceStatus($trace, TraceStatus::FAILED, 'Image not found'); $repository = $this->getRepository($trace);
return; 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 ($repository === null) { $newImageRepo = new ImageImageRepository();
$this->updateTraceStatus($trace, TraceStatus::FAILED, 'Repository not found'); $newImageRepo->setImage($image);
return; $newImageRepo->setRepository($repository);
} $newImageRepo->setStatus(ImageStatus::SUCCESS);
if (isset($trace->getInput()['imageImageRepositoryUuid'])) { if ($trace->getInput()['imageImageRepositoryUuid'] ?? false) {
$imageImageRepositoryUuid = $trace->getInput()['imageImageRepositoryUuid']; $existingRepo = $this->entityManager->getRepository(ImageImageRepository::class)
$imageImageRepository = $this->entityManager->getRepository(ImageImageRepository::class)->findOneBy(['uuid' => $imageImageRepositoryUuid]); ->findOneBy(['uuid' => $trace->getInput()['imageImageRepositoryUuid']]);
if ($imageImageRepository) { if ($existingRepo) {
$this->updateTraceStatus($trace, TraceStatus::FAILED, 'Image repository not found'); $newImageRepo->setImageFullsum($existingRepo->getImageFullsum());
$imageImageRepository->setStatus(ImageStatus::SUCCESS);
} }
} }
$this->logger->info("Image $actionType successful", ['image' => $image->getName()]); $this->entityManager->persist($newImageRepo);
// Creamos un objeto imagen nuevo, en el repositorio destino
$newImageImageRepository = new ImageImageRepository();
$newImageImageRepository->setImage($image);
$newImageImageRepository->setRepository($repository);
$newImageImageRepository->setStatus(ImageStatus::SUCCESS);
// Cambiamos el estado de la imagen anterior a SUCCESS
$this->entityManager->persist($newImageImageRepository);
$this->entityManager->persist($image);
$this->updateTraceStatus($trace, TraceStatus::SUCCESS); $this->updateTraceStatus($trace, TraceStatus::SUCCESS);
return new JsonResponse(['message' => 'Success'], Response::HTTP_OK);
} }
private function updateTraceStatus(Trace $trace, string $status, string $output = null): void private function updateTraceStatus(Trace $trace, string $status, string $output = null): void
@ -142,9 +105,36 @@ class ResponseController extends AbstractOgRepositoryController
$this->entityManager->persist($trace); $this->entityManager->persist($trace);
$this->entityManager->flush(); $this->entityManager->flush();
}
if ($status === TraceStatus::FAILED) { private function getTrace(string $jobId): ?Trace
new JsonResponse(['message' => $output], Response::HTTP_NOT_FOUND); {
return $this->entityManager->getRepository(Trace::class)->findOneBy(['jobId' => $jobId]);
}
private function getImage(Trace $trace): ?Image
{
return $this->entityManager->getRepository(Image::class)
->findOneBy(['uuid' => $trace->getInput()['imageUuid']]);
}
private function getRepository(Trace $trace): ?ImageRepository
{
return $this->entityManager->getRepository(ImageRepository::class)
->findOneBy(['uuid' => $trace->getInput()['repositoryUuid']]);
}
private function getImageImageRepository(Trace $trace): ?ImageImageRepository
{
return $this->entityManager->getRepository(ImageImageRepository::class)
->findOneBy(['uuid' => $trace->getInput()['imageUuid']]);
}
private function jsonResponseError(string $message, int $status = Response::HTTP_BAD_REQUEST, ?Trace $trace = null): JsonResponse
{
if ($trace) {
$this->updateTraceStatus($trace, TraceStatus::FAILED, $message);
} }
return new JsonResponse(['message' => $message], $status);
} }
} }

View File

@ -58,11 +58,16 @@ class WoLAction extends AbstractOgRepositoryController
$content = $this->createRequest('POST', 'http://'.$repository->getIp(). ':8006/ogrepository/v1/wol', $params); $content = $this->createRequest('POST', 'http://'.$repository->getIp(). ':8006/ogrepository/v1/wol', $params);
if (isset($content['error']) && $content['error'] === Response::HTTP_INTERNAL_SERVER_ERROR ) {
$this->logger->error('Error sending WoL to client', ['mac' => $client->getMac()]);
continue;
}
$client->setStatus(ClientStatus::INITIALIZING); $client->setStatus(ClientStatus::INITIALIZING);
$this->entityManager->persist($client); $this->entityManager->persist($client);
$this->entityManager->flush(); $this->entityManager->flush();
$this->createService->__invoke($client, CommandTypes::SHUTDOWN, TraceStatus::SUCCESS, '', []); $this->createService->__invoke($client, CommandTypes::POWER_ON, TraceStatus::SUCCESS, '', []);
} }
return new JsonResponse(data: [], status: Response::HTTP_OK); return new JsonResponse(data: [], status: Response::HTTP_OK);

View File

@ -0,0 +1,24 @@
<?php
namespace App\Dto\Input;
use ApiPlatform\Metadata\ApiProperty;
use App\Dto\Output\ClientOutput;
use App\Dto\Output\ImageOutput;
use App\Dto\Output\PartitionOutput;
use App\Validator\Constraints\ClientsHaveSamePartitionCount;
use App\Validator\Constraints\OrganizationalUnitMulticastMode;
use App\Validator\Constraints\OrganizationalUnitMulticastPort;
use App\Validator\Constraints\OrganizationalUnitP2PMode;
use Symfony\Component\Serializer\Annotation\Groups;
final class BackupImageInput
{
#[Groups(['image-image-repository:write'])]
#[ApiProperty(description: 'The repository ip', example: "")]
public ?string $repoIp = null;
#[Groups(['image-image-repository:write'])]
#[ApiProperty(description: 'The remote path', example: "")]
public ?string $remotePath = null;
}

View File

@ -41,6 +41,9 @@ class NetworkSettingsInput
#[Groups(['organizational-unit:write'])] #[Groups(['organizational-unit:write'])]
public ?string $ntp = null; public ?string $ntp = null;
#[Groups(['organizational-unit:write'])]
public ?string $netiface = null;
#[OrganizationalUnitP2PMode] #[OrganizationalUnitP2PMode]
#[Groups(['organizational-unit:write'])] #[Groups(['organizational-unit:write'])]
public ?string $p2pMode = null; public ?string $p2pMode = null;
@ -93,6 +96,7 @@ class NetworkSettingsInput
$this->netmask = $networkSettings->getNetmask(); $this->netmask = $networkSettings->getNetmask();
$this->router = $networkSettings->getRouter(); $this->router = $networkSettings->getRouter();
$this->ntp = $networkSettings->getNtp(); $this->ntp = $networkSettings->getNtp();
$this->netiface = $networkSettings->getNetiface();
$this->p2pMode = $networkSettings->getP2pMode(); $this->p2pMode = $networkSettings->getP2pMode();
$this->p2pTime = $networkSettings->getP2pTime(); $this->p2pTime = $networkSettings->getP2pTime();
$this->mcastIp = $networkSettings->getMcastIp(); $this->mcastIp = $networkSettings->getMcastIp();
@ -130,6 +134,7 @@ class NetworkSettingsInput
$networkSettings->setDns($this->dns); $networkSettings->setDns($this->dns);
$networkSettings->setNetmask($this->netmask); $networkSettings->setNetmask($this->netmask);
$networkSettings->setRouter($this->router); $networkSettings->setRouter($this->router);
$networkSettings->setNetiface($this->netiface);
$networkSettings->setNtp($this->ntp); $networkSettings->setNtp($this->ntp);
$networkSettings->setP2pMode($this->p2pMode); $networkSettings->setP2pMode($this->p2pMode);
$networkSettings->setP2pTime($this->p2pTime); $networkSettings->setP2pTime($this->p2pTime);

View File

@ -12,7 +12,10 @@ use Symfony\Component\Validator\Constraints as Assert;
final class SubnetAddHostInput final class SubnetAddHostInput
{ {
/**
* @var ClientOutput[]
*/
#[Assert\NotNull] #[Assert\NotNull]
#[Groups(['subnet:write'])] #[Groups(['subnet:write'])]
public ?ClientOutput $client = null; public ?array $clients = [];
} }

View File

@ -36,6 +36,10 @@ final class SubnetInput
#[ApiProperty(description: 'The router of the subnet', example: "")] #[ApiProperty(description: 'The router of the subnet', example: "")]
public ?string $router = null; public ?string $router = null;
#[Groups(['subnet:write'])]
#[ApiProperty(description: 'The dns server of the subnet', example: "")]
public ?string $dns = null;
#[Groups(['subnet:write'])] #[Groups(['subnet:write'])]
#[ApiProperty(description: 'The boot file name of the subnet', example: "")] #[ApiProperty(description: 'The boot file name of the subnet', example: "")]
public ?string $bootFileName = null; public ?string $bootFileName = null;
@ -58,6 +62,7 @@ final class SubnetInput
$this->ipAddress = $subnet->getIpAddress(); $this->ipAddress = $subnet->getIpAddress();
$this->router = $subnet->getRouter(); $this->router = $subnet->getRouter();
$this->nextServer = $subnet->getNextServer(); $this->nextServer = $subnet->getNextServer();
$this->dns = $subnet->getDns();
$this->bootFileName = $subnet->getBootFileName(); $this->bootFileName = $subnet->getBootFileName();
if ($subnet->getOrganizationalUnits()) { if ($subnet->getOrganizationalUnits()) {
@ -76,6 +81,7 @@ final class SubnetInput
$subnet->setName($this->name); $subnet->setName($this->name);
$subnet->setNetmask($this->netmask); $subnet->setNetmask($this->netmask);
$subnet->setIpAddress($this->ipAddress); $subnet->setIpAddress($this->ipAddress);
$subnet->setDns($this->dns);
$subnet->setNextServer($this->nextServer); $subnet->setNextServer($this->nextServer);
$subnet->setBootFileName($this->bootFileName); $subnet->setBootFileName($this->bootFileName);
$subnet->setRouter($this->router); $subnet->setRouter($this->router);

View File

@ -29,6 +29,9 @@ final class UserInput
#[Groups('user:write')] #[Groups('user:write')]
public ?string $password = null; public ?string $password = null;
#[Groups('user:write')]
public ?string $groupsView = null;
#[Assert\NotNull] #[Assert\NotNull]
#[Groups('user:write')] #[Groups('user:write')]
public ?bool $enabled = true; public ?bool $enabled = true;
@ -63,6 +66,7 @@ final class UserInput
$this->username = $user->getUsername(); $this->username = $user->getUsername();
$this->enabled= $user->isEnabled(); $this->enabled= $user->isEnabled();
$this->groupsView = $user->getGroupsView();
if ($user->getUserGroups()) { if ($user->getUserGroups()) {
foreach ($user->getUserGroups() as $userGroup) { foreach ($user->getUserGroups() as $userGroup) {
@ -85,6 +89,7 @@ final class UserInput
$user->setUsername($this->username); $user->setUsername($this->username);
$user->setEnabled($this->enabled); $user->setEnabled($this->enabled);
$user->setGroupsView($this->groupsView);
foreach ($this->userGroups as $userGroup) { foreach ($this->userGroups as $userGroup) {
$userGroupsToAdd[] = $userGroup->getEntity(); $userGroupsToAdd[] = $userGroup->getEntity();

View File

@ -30,6 +30,9 @@ final class NetworkSettingsOutput extends AbstractOutput
#[Groups(['network-settings:read', "organizational-unit:read", "client:read"])] #[Groups(['network-settings:read', "organizational-unit:read", "client:read"])]
public ?string $ntp = null; public ?string $ntp = null;
#[Groups(['network-settings:read', "organizational-unit:read", "client:read"])]
public ?string $netiface = null;
#[Groups(['network-settings:read', "organizational-unit:read", "client:read"])] #[Groups(['network-settings:read', "organizational-unit:read", "client:read"])]
public ?string $p2pMode = null; public ?string $p2pMode = null;
@ -80,6 +83,7 @@ final class NetworkSettingsOutput extends AbstractOutput
$this->netmask = $networkSettings->getNetmask(); $this->netmask = $networkSettings->getNetmask();
$this->router = $networkSettings->getRouter(); $this->router = $networkSettings->getRouter();
$this->ntp = $networkSettings->getNtp(); $this->ntp = $networkSettings->getNtp();
$this->netiface = $networkSettings->getNetiface();
$this->p2pMode = $networkSettings->getP2pMode(); $this->p2pMode = $networkSettings->getP2pMode();
$this->p2pTime = $networkSettings->getP2pTime(); $this->p2pTime = $networkSettings->getP2pTime();
$this->mcastIp = $networkSettings->getMcastIp(); $this->mcastIp = $networkSettings->getMcastIp();

View File

@ -38,7 +38,7 @@ final class OrganizationalUnitOutput extends AbstractOutput
#[Groups(['organizational-unit:read'])] #[Groups(['organizational-unit:read'])]
public ?self $parent = null; public ?self $parent = null;
#[Groups(['organizational-unit:read', 'organizational-unit:read:collection:short'])] #[Groups(['organizational-unit:read', 'client:read', 'organizational-unit:read:collection:short'])]
public string $path; public string $path;
#[Groups(['organizational-unit:read', "client:read"])] #[Groups(['organizational-unit:read', "client:read"])]

View File

@ -30,6 +30,9 @@ final class SubnetOutput extends AbstractOutput
#[Groups(['subnet:read'])] #[Groups(['subnet:read'])]
public ?string $router = null; public ?string $router = null;
#[Groups(['subnet:read'])]
public ?string $dns = null;
#[Groups(['subnet:read'])] #[Groups(['subnet:read'])]
public array $clients; public array $clients;
@ -54,6 +57,7 @@ final class SubnetOutput extends AbstractOutput
$this->ipAddress = $subnet->getIpAddress(); $this->ipAddress = $subnet->getIpAddress();
$this->nextServer = $subnet->getNextServer(); $this->nextServer = $subnet->getNextServer();
$this->router = $subnet->getRouter(); $this->router = $subnet->getRouter();
$this->dns = $subnet->getDns();
$this->bootFileName = $subnet->getBootFileName(); $this->bootFileName = $subnet->getBootFileName();
$this->synchronized = $subnet->isSynchronized(); $this->synchronized = $subnet->isSynchronized();
$this->serverId = $subnet->getServerId(); $this->serverId = $subnet->getServerId();

View File

@ -35,7 +35,7 @@ final class TraceOutput extends AbstractOutput
public ?\DateTimeInterface $finishedAt = null; public ?\DateTimeInterface $finishedAt = null;
#[Groups(['trace:read'])] #[Groups(['trace:read'])]
public ?int $progress = null; public ?float $progress = null;
#[Groups(['trace:read'])] #[Groups(['trace:read'])]
public \DateTime $createdAt; public \DateTime $createdAt;
@ -59,7 +59,7 @@ final class TraceOutput extends AbstractOutput
$this->output = $trace->getOutput(); $this->output = $trace->getOutput();
$this->input = $trace->getInput(); $this->input = $trace->getInput();
$this->finishedAt = $trace->getFinishedAt(); $this->finishedAt = $trace->getFinishedAt();
$this->progress = $trace->getProgress(); $this->progress = $trace->getProgress() / 100;
$this->createdAt = $trace->getCreatedAt(); $this->createdAt = $trace->getCreatedAt();
$this->createdBy = $trace->getCreatedBy(); $this->createdBy = $trace->getCreatedBy();
} }

View File

@ -23,6 +23,8 @@ final class UserOutput extends AbstractOutput
public array $allowedOrganizationalUnits; public array $allowedOrganizationalUnits;
#[Groups(['user:read'])] #[Groups(['user:read'])]
public array $userGroups; public array $userGroups;
#[Groups(['user:read'])]
public ?string $groupsView = 'card';
#[Groups(['user:read'])] #[Groups(['user:read'])]
public \DateTime $createdAt; public \DateTime $createdAt;
@ -37,6 +39,7 @@ final class UserOutput extends AbstractOutput
$this->username = $user->getUsername(); $this->username = $user->getUsername();
$this->roles = $user->getRoles(); $this->roles = $user->getRoles();
$this->enabled = $user->isEnabled(); $this->enabled = $user->isEnabled();
$this->groupsView = $user->getGroupsView();
$this->userGroups = $user->getUserGroups()->map( $this->userGroups = $user->getUserGroups()->map(
fn(UserGroup $userGroup) => new UserGroupOutput($userGroup) fn(UserGroup $userGroup) => new UserGroupOutput($userGroup)

View File

@ -2,6 +2,7 @@
namespace App\Entity; namespace App\Entity;
use ApiPlatform\Metadata\ApiResource;
use App\Repository\ClientRepository; use App\Repository\ClientRepository;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Collection;
@ -152,6 +153,7 @@ class Client extends AbstractEntity
return $this->status; return $this->status;
} }
#[ORM\PostPersist]
public function setStatus(?string $status): static public function setStatus(?string $status): static
{ {
$this->status = $status; $this->status = $status;

View File

@ -31,6 +31,9 @@ class NetworkSettings extends AbstractEntity
#[ORM\Column(length: 255, nullable: true)] #[ORM\Column(length: 255, nullable: true)]
private ?string $ntp = null; private ?string $ntp = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $netiface = null;
#[ORM\Column(nullable: true)] #[ORM\Column(nullable: true)]
private ?int $p2pTime = null; private ?int $p2pTime = null;
@ -161,6 +164,18 @@ class NetworkSettings extends AbstractEntity
return $this; return $this;
} }
public function getNetiface(): ?string
{
return $this->netiface;
}
public function setNetiface(?string $netiface): static
{
$this->netiface = $netiface;
return $this;
}
public function getP2pTime(): ?int public function getP2pTime(): ?int
{ {
return $this->p2pTime; return $this->p2pTime;

View File

@ -43,6 +43,9 @@ class Subnet extends AbstractEntity
#[ORM\Column(length: 255, nullable: true)] #[ORM\Column(length: 255, nullable: true)]
private ?string $router = null; private ?string $router = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $dns = null;
public function __construct() public function __construct()
{ {
parent::__construct(); parent::__construct();
@ -202,4 +205,16 @@ class Subnet extends AbstractEntity
return $this; return $this;
} }
public function getDns(): ?string
{
return $this->dns;
}
public function setDns(?string $dns): static
{
$this->dns = $dns;
return $this;
}
} }

View File

@ -51,6 +51,9 @@ class User extends AbstractEntity implements UserInterface, PasswordAuthenticate
private ?string $newPassword = null; private ?string $newPassword = null;
private ?string $repeatNewPassword = null; private ?string $repeatNewPassword = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $groupsView = null;
public function __construct() public function __construct()
{ {
@ -245,4 +248,16 @@ class User extends AbstractEntity implements UserInterface, PasswordAuthenticate
return $this; return $this;
} }
public function getGroupsView(): ?string
{
return $this->groupsView;
}
public function setGroupsView(?string $groupsView): static
{
$this->groupsView = $groupsView;
return $this;
}
} }

View File

@ -0,0 +1,52 @@
<?php
namespace App\EventListener;
use App\Entity\Client;
use Doctrine\Bundle\DoctrineBundle\Attribute\AsEntityListener;
use Doctrine\ORM\Events;
use Doctrine\ORM\Event\PostUpdateEventArgs;
use Psr\Log\LoggerInterface;
use Symfony\Component\Mercure\HubInterface;
use Symfony\Component\Mercure\Update;
#[AsEntityListener(event: Events::postUpdate, method: 'postUpdate', entity: Client::class)]
class ClientStatusNotifier
{
private LoggerInterface $logger;
public function __construct(
LoggerInterface $logger,
private readonly HubInterface $hub
)
{
$this->logger = $logger;
}
public function postUpdate(Client $client, PostUpdateEventArgs $event): void
{
try {
$this->notifyClientStatusChange($client);
} catch (\Exception $e) {
$this->logger->error('Error al notificar el cambio de estado de un cliente: ', [
'client' => $client->getUuid(),
'status' => $client->getStatus(),
'error' => $e->getMessage(),
]);
}
}
private function notifyClientStatusChange(Client $client): void
{
$update = new Update(
'clients',
json_encode(['@id' => '/clients/'.$client->getUuid(), 'status' => $client->getStatus()])
);
$this->hub->publish($update);
$this->logger->info('Evento Mercure disparado. Cambio en el estado de un cliente: ', [
'client' => $client->getUuid(),
'status' => $client->getStatus(),
]);
}
}

View File

@ -0,0 +1,55 @@
<?php
namespace App\EventListener;
use App\Entity\Client;
use App\Entity\Trace;
use Doctrine\Bundle\DoctrineBundle\Attribute\AsEntityListener;
use Doctrine\ORM\Events;
use Doctrine\ORM\Event\PostUpdateEventArgs;
use Psr\Log\LoggerInterface;
use Symfony\Component\Mercure\HubInterface;
use Symfony\Component\Mercure\Update;
#[AsEntityListener(event: Events::postUpdate, method: 'postUpdate', entity: Trace::class)]
class TraceStatusProgressNotifier
{
private LoggerInterface $logger;
public function __construct(
LoggerInterface $logger,
private readonly HubInterface $hub
)
{
$this->logger = $logger;
}
public function postUpdate(Trace $trace, PostUpdateEventArgs $event): void
{
try {
$this->notifyTraceStatusProgressChange($trace);
} catch (\Exception $e) {
$this->logger->error('Error al notificar el cambio de estado de una traza/log: ', [
'client' => $trace->getUuid(),
'status' => $trace->getStatus(),
'progress' => $trace->getProgress(),
'error' => $e->getMessage(),
]);
}
}
private function notifyTraceStatusProgressChange(Trace $trace): void
{
$update = new Update(
'traces',
json_encode(['@id' => '/traces/' . $trace->getUuid(), 'status' => $trace->getStatus(), 'progress' => $trace->getProgress() / 100])
);
$this->hub->publish($update);
$this->logger->info('Evento Mercure disparado. Cambio en el estado de una traza/log: ', [
'client' => $trace->getUuid(),
'status' => $trace->getStatus(),
'progress' => $trace->getProgress(),
]);
}
}

View File

@ -0,0 +1,66 @@
<?php
namespace App\EventSubscriber;
use ApiPlatform\Symfony\EventListener\EventPriorities;
use App\Dto\Output\ClientOutput;
use App\Entity\Client;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\ViewEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Psr\Log\LoggerInterface;
use Symfony\Component\Mercure\HubInterface;
use Symfony\Component\Mercure\Update;
class MercureSubscriber implements EventSubscriberInterface
{
private LoggerInterface $logger;
public function __construct(
LoggerInterface $logger,
private readonly HubInterface $hub
)
{
$this->logger = $logger;
}
public static function getSubscribedEvents(): array
{
return [
KernelEvents::VIEW => ['onKernelView', EventPriorities::POST_WRITE],
];
}
public function onKernelView(ViewEvent $event): void
{
$request = $event->getRequest();
$method = $request->getMethod();
$clientOutput = $event->getControllerResult();
if (!in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'])) {
return;
}
$method = $event->getRequest()->getMethod();
if (!$clientOutput instanceof ClientOutput || (Request::METHOD_POST !== $method && Request::METHOD_PUT !== $method && Request::METHOD_PATCH !== $method)) {
return;
}
/** @var Client $client */
$client = $clientOutput->getEntity();
$update = new Update(
'clients',
json_encode(['@id' => '/clients/'.$client->getUuid(), 'status' => $client->getStatus()])
);
$this->hub->publish($update);
$this->logger->info('Evento Mercure disparado', [
'method' => $method,
'path' => $request->getPathInfo()
]);
}
}

View File

@ -32,6 +32,7 @@ final class UserFactory extends ModelFactory
return [ return [
'password' => $hash, 'password' => $hash,
'roles' => [], 'roles' => [],
'groupsView' => 'card',
'username' => self::faker()->text(180), 'username' => self::faker()->text(180),
]; ];
} }

View File

@ -19,6 +19,7 @@ class CustomLineFormatter extends LineFormatter
'component' => 'ogcore', 'component' => 'ogcore',
'params' => $record['context'], 'params' => $record['context'],
'desc' => $record['message'], 'desc' => $record['message'],
'datetime' => $record['datetime']->format('Y-m-d H:i:s'),
]; ];
return json_encode($output, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . PHP_EOL; return json_encode($output, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . PHP_EOL;

View File

@ -23,6 +23,7 @@ class JWTCreatedListener
$payload['username'] = $user->getUsername(); $payload['username'] = $user->getUsername();
$payload['uuid'] = $user->getUuid(); $payload['uuid'] = $user->getUuid();
$payload['roles'] = $user->getRoles(); $payload['roles'] = $user->getRoles();
$payload['groupsView'] = $user->getGroupsView();
$event->setData($payload); $event->setData($payload);
} }

View File

@ -5,27 +5,20 @@ namespace App\Model;
final class ClientStatus final class ClientStatus
{ {
public const string OFF = 'off'; public const string OFF = 'off';
public const string INITIALIZING = 'initializing'; public const string INITIALIZING = 'initializing';
public const string TURNING_OFF = 'turning-off';
public const string OG_LIVE = 'og-live'; public const string OG_LIVE = 'og-live';
public const string BUSY = 'busy'; public const string BUSY = 'busy';
public const string LINUX = 'linux'; public const string LINUX = 'linux';
public const string LINUX_SESSION = 'linux-session'; public const string LINUX_SESSION = 'linux-session';
public const string MACOS = 'macos'; public const string MACOS = 'macos';
public const string MACOS_SESSION = 'macos-session'; public const string MACOS_SESSION = 'macos-session';
public const string WINDOWS = 'windows'; public const string WINDOWS = 'windows';
public const string WINDOWS_SESSION = 'windows-session'; public const string WINDOWS_SESSION = 'windows-session';
private const array CLIENT_STATUSES = [ private const array CLIENT_STATUSES = [
self::OFF => 'Apagado', self::OFF => 'Apagado',
self::TURNING_OFF => 'Apagando',
self::INITIALIZING => 'Inicializando', self::INITIALIZING => 'Inicializando',
self::OG_LIVE => 'OG Live', self::OG_LIVE => 'OG Live',
self::BUSY => 'Ocupado', self::BUSY => 'Ocupado',

View File

@ -8,6 +8,7 @@ final class CommandTypes
public const string RESTORE_IMAGE = 'restore-image'; public const string RESTORE_IMAGE = 'restore-image';
public const string CREATE_IMAGE = 'create-image'; public const string CREATE_IMAGE = 'create-image';
public const string CREATE_IMAGE_AUX_FILE = 'create-image-aux-file'; 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 IMPORT_IMAGE = 'import-image';
public const string EXPORT_IMAGE = 'export-image'; public const string EXPORT_IMAGE = 'export-image';
public const string TRANSFER_IMAGE = 'transfer-image'; public const string TRANSFER_IMAGE = 'transfer-image';
@ -25,6 +26,7 @@ final class CommandTypes
self::RESTORE_IMAGE => 'Update Cache', self::RESTORE_IMAGE => 'Update Cache',
self::CREATE_IMAGE => 'Create Image', self::CREATE_IMAGE => 'Create Image',
self::CREATE_IMAGE_AUX_FILE => 'Crear fichero auxiliar en repositorio', self::CREATE_IMAGE_AUX_FILE => 'Crear fichero auxiliar en repositorio',
self::BACKUP_IMAGE => 'Crear backup de imagen',
self::IMPORT_IMAGE => 'Importar imagen', self::IMPORT_IMAGE => 'Importar imagen',
self::EXPORT_IMAGE => 'Exportar imagen', self::EXPORT_IMAGE => 'Exportar imagen',
self::POWER_ON => 'Encender', self::POWER_ON => 'Encender',

View File

@ -11,6 +11,7 @@ final class ImageStatus
public const string TRASH = 'trash'; public const string TRASH = 'trash';
public const string FAILED = 'failed'; public const string FAILED = 'failed';
public const string TRANSFERRING = 'transferring'; public const string TRANSFERRING = 'transferring';
public const string BACKUP = 'backup';
private const array STATUS = [ private const array STATUS = [
self::PENDING => 'Pendiente', self::PENDING => 'Pendiente',

View File

@ -8,12 +8,14 @@ final class TraceStatus
public const string IN_PROGRESS = 'in-progress'; public const string IN_PROGRESS = 'in-progress';
public const string SUCCESS = 'success'; public const string SUCCESS = 'success';
public const string FAILED = 'failed'; public const string FAILED = 'failed';
public const string CANCELLED = 'cancelled';
private const array STATUS = [ private const array STATUS = [
self::PENDING => 'Pendiente', self::PENDING => 'Pendiente',
self::IN_PROGRESS => 'En progreso', self::IN_PROGRESS => 'En progreso',
self::SUCCESS => 'Finalizado con éxito', self::SUCCESS => 'Finalizado con éxito',
self::FAILED => 'Fallido', self::FAILED => 'Fallido',
self::CANCELLED => 'Cancelado',
]; ];
public static function getStatus(): array public static function getStatus(): array

View File

@ -16,23 +16,24 @@ class ClientRepository extends AbstractRepository
parent::__construct($registry, Client::class); parent::__construct($registry, Client::class);
} }
public function findClientsByOrganizationalUnitAndDescendants(int $organizationalUnitId): array public function findClientsByOrganizationalUnitAndDescendants(int $organizationalUnitId, array $filters = []): array
{ {
$query = $this->getEntityManager()->createQuery( $entityManager = $this->getEntityManager();
$query = $entityManager->createQuery(
'SELECT o.path 'SELECT o.path
FROM App\Entity\OrganizationalUnit o FROM App\Entity\OrganizationalUnit o
WHERE o.id = :id' WHERE o.id = :id'
)->setParameter('id', $organizationalUnitId); )->setParameter('id', $organizationalUnitId);
$result = $query->getOneOrNullResult(); $result = $query->getOneOrNullResult();
if (!$result) { if (!$result) {
return []; return [];
} }
$path = $result['path'] . '/%'; $path = $result['path'] . '/%';
$query = $this->getEntityManager()->createQuery( $query = $entityManager->createQuery(
'SELECT o.id 'SELECT o.id
FROM App\Entity\OrganizationalUnit o FROM App\Entity\OrganizationalUnit o
WHERE o.id = :id OR o.path LIKE :path' WHERE o.id = :id OR o.path LIKE :path'
@ -42,13 +43,122 @@ class ClientRepository extends AbstractRepository
$unitIds = array_column($query->getArrayResult(), 'id'); $unitIds = array_column($query->getArrayResult(), 'id');
$query = $this->getEntityManager()->createQuery( $qb = $entityManager->createQueryBuilder();
'SELECT c $qb->select('c')
FROM App\Entity\Client c ->from('App\Entity\Client', 'c')
WHERE c.organizationalUnit IN (:unitIds)' ->where('c.organizationalUnit IN (:unitIds)')
)
->setParameter('unitIds', $unitIds); ->setParameter('unitIds', $unitIds);
return $query->getResult(); foreach ($filters as $key => $value) {
if ($key === 'order' || $key === 'page' || $key === 'itemsPerPage' || $key === 'organizationalUnit.id') {
continue;
}
if ($key === 'exists') {
foreach ($value as $field => $existsValue) {
if ($existsValue === 'true') {
$qb->andWhere("c.$field IS NOT NULL");
} elseif ($existsValue === 'false') {
$qb->andWhere("c.$field IS NULL");
}
}
} elseif ($key === 'name') {
// Aplicar búsqueda con LIKE para el campo "name"
$qb->andWhere("c.name LIKE :name")
->setParameter('name', "%$value%");
} elseif ($key === 'query') {
// Búsqueda en múltiples campos (name, ip, mac)
$qb->andWhere($qb->expr()->orX(
$qb->expr()->like('c.name', ':searchQuery'),
$qb->expr()->like('c.ip', ':searchQuery'),
$qb->expr()->like('c.mac', ':searchQuery')
))->setParameter('searchQuery', "%$value%");
} else {
// Búsqueda exacta para otros campos
$qb->andWhere("c.$key = :$key")->setParameter($key, $value);
}
}
if (isset($filters['order']) && is_array($filters['order'])) {
foreach ($filters['order'] as $field => $direction) {
if (in_array(strtolower($direction), ['asc', 'desc'])) {
$qb->addOrderBy("c.$field", $direction);
}
}
}
if (isset($filters['page']) && isset($filters['itemsPerPage'])) {
$page = max(1, (int) $filters['page']);
$itemsPerPage = max(1, (int) $filters['itemsPerPage']);
$qb->setFirstResult(($page - 1) * $itemsPerPage)
->setMaxResults($itemsPerPage);
}
return $qb->getQuery()->getResult();
} }
public function countClientsByOrganizationalUnitAndDescendants(int $organizationalUnitId, array $filters = []): int
{
$entityManager = $this->getEntityManager();
// Obtener el path de la unidad organizativa raíz
$query = $entityManager->createQuery(
'SELECT o.path FROM App\Entity\OrganizationalUnit o WHERE o.id = :id'
)->setParameter('id', $organizationalUnitId);
$result = $query->getOneOrNullResult();
if (!$result) {
return 0;
}
$path = $result['path'] . '/%';
$query = $entityManager->createQuery(
'SELECT o.id FROM App\Entity\OrganizationalUnit o WHERE o.id = :id OR o.path LIKE :path'
)
->setParameter('id', $organizationalUnitId)
->setParameter('path', $path);
$unitIds = array_column($query->getArrayResult(), 'id');
$qb = $entityManager->createQueryBuilder();
$qb->select('COUNT(c.id)')
->from('App\Entity\Client', 'c')
->where('c.organizationalUnit IN (:unitIds)')
->setParameter('unitIds', $unitIds);
foreach ($filters as $key => $value) {
if ($key === 'order' || $key === 'page' || $key === 'itemsPerPage' || $key === 'organizationalUnit.id') {
continue;
}
if ($key === 'exists') {
foreach ($value as $field => $existsValue) {
if ($existsValue === 'true') {
$qb->andWhere("c.$field IS NOT NULL");
} elseif ($existsValue === 'false') {
$qb->andWhere("c.$field IS NULL");
}
}
} elseif ($key === 'name') {
$qb->andWhere("c.name LIKE :name")
->setParameter('name', "%$value%");
} elseif ($key === 'query') {
$qb->andWhere($qb->expr()->orX(
$qb->expr()->like('c.name', ':searchQuery'),
$qb->expr()->like('c.ip', ':searchQuery'),
$qb->expr()->like('c.mac', ':searchQuery')
))->setParameter('searchQuery', "%$value%");
} else {
$qb->andWhere("c.$key = :$key")->setParameter($key, $value);
}
}
return (int) $qb->getQuery()->getSingleScalarResult();
}
} }

View File

@ -39,23 +39,35 @@ readonly class ClientProvider implements ProviderInterface
public function provideCollection(Operation $operation, array $uriVariables = [], array $context = []): object public function provideCollection(Operation $operation, array $uriVariables = [], array $context = []): object
{ {
$organizationalUnitId = $context['filters']['organizationalUnit.id'] ?? null; $filters = $context['filters'] ?? [];
if ($organizationalUnitId) { if (isset($filters['organizationalUnit.id'])) {
$clients = $this->clientRepository->findClientsByOrganizationalUnitAndDescendants((int) $organizationalUnitId); $organizationalUnitId = (int) $filters['organizationalUnit.id'];
$totalClients = $this->clientRepository->countClientsByOrganizationalUnitAndDescendants($organizationalUnitId, $filters);
$clients = $this->clientRepository->findClientsByOrganizationalUnitAndDescendants($organizationalUnitId, $filters);
$items = new \ArrayObject(); $items = new \ArrayObject();
foreach ($clients as $client) { foreach ($clients as $client) {
$items[] = new ClientOutput($client); $items[] = new ClientOutput($client);
} }
return new TraversablePaginator($items, 1, count($clients), count($clients)); return new TraversablePaginator($items, $filters['page'] ?? 1, $filters['itemsPerPage'] ?? count($clients), $totalClients);
} } else {
$paginator = $this->collectionProvider->provide($operation, $uriVariables, $context);
return $this->collectionProvider->provide($operation, $uriVariables, $context); $items = new \ArrayObject();
foreach ($paginator->getIterator() as $item) {
$items[] = new ClientOutput($item);
}
return new TraversablePaginator($items, $paginator->getCurrentPage(), $paginator->getItemsPerPage(), $paginator->getTotalItems());
}
} }
public function provideItem(Operation $operation, array $uriVariables = [], array $context = []): object|array|null public function provideItem(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
{ {
$item = $this->itemProvider->provide($operation, $uriVariables, $context); $item = $this->itemProvider->provide($operation, $uriVariables, $context);

View File

@ -13,6 +13,18 @@
"src/ApiResource/.gitignore" "src/ApiResource/.gitignore"
] ]
}, },
"dama/doctrine-test-bundle": {
"version": "8.2",
"recipe": {
"repo": "github.com/symfony/recipes-contrib",
"branch": "main",
"version": "7.2",
"ref": "896306d79d4ee143af9eadf9b09fd34a8c391b70"
},
"files": [
"config/packages/dama_doctrine_test_bundle.yaml"
]
},
"doctrine/doctrine-bundle": { "doctrine/doctrine-bundle": {
"version": "2.13", "version": "2.13",
"recipe": { "recipe": {
@ -27,6 +39,18 @@
"src/Repository/.gitignore" "src/Repository/.gitignore"
] ]
}, },
"doctrine/doctrine-fixtures-bundle": {
"version": "3.7",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "3.0",
"ref": "1f5514cfa15b947298df4d771e694e578d4c204d"
},
"files": [
"src/DataFixtures/AppFixtures.php"
]
},
"doctrine/doctrine-migrations-bundle": { "doctrine/doctrine-migrations-bundle": {
"version": "3.3", "version": "3.3",
"recipe": { "recipe": {
@ -78,6 +102,20 @@
"config/packages/nelmio_cors.yaml" "config/packages/nelmio_cors.yaml"
] ]
}, },
"phpunit/phpunit": {
"version": "9.6",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "9.6",
"ref": "7364a21d87e658eb363c5020c072ecfdc12e2326"
},
"files": [
".env.test",
"phpunit.xml.dist",
"tests/bootstrap.php"
]
},
"ramsey/uuid-doctrine": { "ramsey/uuid-doctrine": {
"version": "2.1", "version": "2.1",
"recipe": { "recipe": {
@ -143,6 +181,27 @@
"src/Kernel.php" "src/Kernel.php"
] ]
}, },
"symfony/maker-bundle": {
"version": "1.61",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "1.0",
"ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f"
}
},
"symfony/mercure-bundle": {
"version": "0.3",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "0.3",
"ref": "528285147494380298f8f991ee8c47abebaf79db"
},
"files": [
"config/packages/mercure.yaml"
]
},
"symfony/monolog-bundle": { "symfony/monolog-bundle": {
"version": "3.10", "version": "3.10",
"recipe": { "recipe": {
@ -155,6 +214,21 @@
"config/packages/monolog.yaml" "config/packages/monolog.yaml"
] ]
}, },
"symfony/phpunit-bridge": {
"version": "7.2",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "6.3",
"ref": "a411a0480041243d97382cac7984f7dce7813c08"
},
"files": [
".env.test",
"bin/phpunit",
"phpunit.xml.dist",
"tests/bootstrap.php"
]
},
"symfony/routing": { "symfony/routing": {
"version": "6.4", "version": "6.4",
"recipe": { "recipe": {
@ -218,5 +292,30 @@
"files": [ "files": [
"config/packages/validator.yaml" "config/packages/validator.yaml"
] ]
},
"symfony/web-profiler-bundle": {
"version": "6.4",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "6.1",
"ref": "e42b3f0177df239add25373083a564e5ead4e13a"
},
"files": [
"config/packages/web_profiler.yaml",
"config/routes/web_profiler.yaml"
]
},
"zenstruck/foundry": {
"version": "1.38",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "1.10",
"ref": "37c2f894cc098ab4c08874b80cccc8e2f8de7976"
},
"files": [
"config/packages/zenstruck_foundry.yaml"
]
} }
} }

View File

@ -42,7 +42,9 @@ class ClientTest extends AbstractTest
{ {
UserFactory::createOne(['username' => self::USER_ADMIN, 'roles'=> [UserGroupPermissions::ROLE_SUPER_ADMIN]]); UserFactory::createOne(['username' => self::USER_ADMIN, 'roles'=> [UserGroupPermissions::ROLE_SUPER_ADMIN]]);
ClientFactory::createMany(10); $ou = OrganizationalUnitFactory::new(['type' => OrganizationalUnitTypes::ORGANIZATIONAL_UNIT])->create()->_disableAutoRefresh();
$ou->_save();
ClientFactory::createOne(['organizationalUnit' => $ou]);
$this->createClientWithCredentials()->request('GET', '/clients'); $this->createClientWithCredentials()->request('GET', '/clients');
$this->assertResponseStatusCodeSame(Response::HTTP_OK); $this->assertResponseStatusCodeSame(Response::HTTP_OK);
@ -51,7 +53,7 @@ class ClientTest extends AbstractTest
'@context' => '/contexts/Client', '@context' => '/contexts/Client',
'@id' => '/clients', '@id' => '/clients',
'@type' => 'hydra:Collection', '@type' => 'hydra:Collection',
'hydra:totalItems' => 10, 'hydra:totalItems' => 1,
]); ]);
} }

View File

@ -56,6 +56,7 @@ class UserTest extends AbstractTest
'username' => self::USER_CREATE, 'username' => self::USER_CREATE,
'password' => '12345678', 'password' => '12345678',
'enabled' => true, 'enabled' => true,
'groupsView' => 'card'
]]); ]]);
$this->assertResponseStatusCodeSame(201); $this->assertResponseStatusCodeSame(201);