From 051bdeb646b1a846099947fe71432a6a55262062 Mon Sep 17 00:00:00 2001 From: Manuel Aranda Date: Tue, 18 Feb 2025 13:30:13 +0100 Subject: [PATCH 01/27] Fixed ogAgent status timeout --- src/Controller/OgAgent/StatusAction.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Controller/OgAgent/StatusAction.php b/src/Controller/OgAgent/StatusAction.php index c6f0c06..77b5699 100644 --- a/src/Controller/OgAgent/StatusAction.php +++ b/src/Controller/OgAgent/StatusAction.php @@ -74,7 +74,7 @@ class StatusAction extends AbstractController $response = $this->httpClient->request('POST', 'https://' . $client->getIp() . ':8000/ogAdmClient/status', [ 'verify_peer' => false, 'verify_host' => false, - 'timeout' => 10, + 'timeout' => 30, 'headers' => [ 'Content-Type' => 'application/json', ], -- 2.40.1 From aedffb55963ca17aae34df04cf8aa75b4206f8dc Mon Sep 17 00:00:00 2001 From: Manuel Aranda Date: Thu, 20 Feb 2025 08:55:18 +0100 Subject: [PATCH 02/27] Added new user admin command --- .env.prod | 20 +++++++++ src/Command/LoadDefaultUserAdminCommand.php | 48 +++++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 .env.prod create mode 100644 src/Command/LoadDefaultUserAdminCommand.php diff --git a/.env.prod b/.env.prod new file mode 100644 index 0000000..28089be --- /dev/null +++ b/.env.prod @@ -0,0 +1,20 @@ + +###> symfony/framework-bundle ### +APP_ENV=prod +APP_SECRET=e95c7f17da15ce1b03d77ad655379c34 +###< symfony/framework-bundle ### + +###> doctrine/doctrine-bundle ### +DATABASE_URL="mysql://root:root@ogcore-database:3306/ogcore?serverVersion=10.11.2-MariaDB&charset=utf8mb4" +OG_1_DATABASE_URL="mysql://root:root@ogcore-database: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 ### \ No newline at end of file diff --git a/src/Command/LoadDefaultUserAdminCommand.php b/src/Command/LoadDefaultUserAdminCommand.php new file mode 100644 index 0000000..39862ac --- /dev/null +++ b/src/Command/LoadDefaultUserAdminCommand.php @@ -0,0 +1,48 @@ + ['algorithm' => 'auto'], + ]); + $hasher = $factory->getPasswordHasher('auto'); + $hash = $hasher->hash(self::PLAIN_PASSWORD); + + $user = new User(); + $user->setUsername(self::USERNAME); + $user->setRoles([UserGroupPermissions::ROLE_SUPER_ADMIN]); + $user->setPassword($hash); + + $this->entityManager->persist($user); + $this->entityManager->flush(); + + return Command::SUCCESS; + } +} -- 2.40.1 From 819ec6a25d9ba910444954c9a2178ba4c4c86c1b Mon Sep 17 00:00:00 2001 From: Manuel Aranda Date: Thu, 20 Feb 2025 09:26:15 +0100 Subject: [PATCH 03/27] Added new user admin command --- src/Command/LoadDefaultUserGroupsCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Command/LoadDefaultUserGroupsCommand.php b/src/Command/LoadDefaultUserGroupsCommand.php index 51bc6fe..0c5ab0d 100644 --- a/src/Command/LoadDefaultUserGroupsCommand.php +++ b/src/Command/LoadDefaultUserGroupsCommand.php @@ -54,6 +54,6 @@ class LoadDefaultUserGroupsCommand extends Command $this->entityManager->flush(); - return 1; + return Command::SUCCESS; } } \ No newline at end of file -- 2.40.1 From ac8f6d1028b125d74ace4dc79f5249d2edd36ada Mon Sep 17 00:00:00 2001 From: Manuel Aranda Date: Thu, 20 Feb 2025 09:30:21 +0100 Subject: [PATCH 04/27] Added new user admin command --- src/Command/LoadDefaultCommandsCommand.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Command/LoadDefaultCommandsCommand.php b/src/Command/LoadDefaultCommandsCommand.php index e0f8c72..f64598c 100644 --- a/src/Command/LoadDefaultCommandsCommand.php +++ b/src/Command/LoadDefaultCommandsCommand.php @@ -89,7 +89,6 @@ class LoadDefaultCommandsCommand extends Command $this->entityManager->flush(); - return 1; + return Command::SUCCESS; } - } \ No newline at end of file -- 2.40.1 From 7098d4fcb1b13f931fb27759a4e295ba01ef2fca Mon Sep 17 00:00:00 2001 From: Manuel Aranda Date: Thu, 20 Feb 2025 13:00:41 +0100 Subject: [PATCH 05/27] fixed ip wrong check --- src/Controller/OgAgent/DeployImageAction.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Controller/OgAgent/DeployImageAction.php b/src/Controller/OgAgent/DeployImageAction.php index 2a19984..65a5de9 100644 --- a/src/Controller/OgAgent/DeployImageAction.php +++ b/src/Controller/OgAgent/DeployImageAction.php @@ -43,7 +43,7 @@ class DeployImageAction extends AbstractController { $image = $imageImageRepository->getImage(); - if (!$image->getClient()->getIp()) { + if (!$client->getIp()) { throw new ValidatorException('IP is required'); } -- 2.40.1 From f9feafa9b85e637ba098812f2b84775320546f17 Mon Sep 17 00:00:00 2001 From: Manuel Aranda Date: Thu, 20 Feb 2025 13:17:02 +0100 Subject: [PATCH 06/27] Changed monolog prod stderror --- config/packages/monolog.yaml | 26 +++++++------------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/config/packages/monolog.yaml b/config/packages/monolog.yaml index 717f13b..4dfd5a8 100644 --- a/config/packages/monolog.yaml +++ b/config/packages/monolog.yaml @@ -8,17 +8,9 @@ when@dev: main: type: stream level: info - path: php://stderr + path: "%kernel.logs_dir%/%kernel.environment%.log" formatter: App\Formatter\CustomLineFormatter 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: type: console process_psr_3_messages: false @@ -42,16 +34,11 @@ when@prod: monolog: handlers: 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 - path: php://stderr - level: debug - formatter: monolog.formatter.json + path: "%kernel.logs_dir%/%kernel.environment%.log" + level: error + formatter: App\Formatter\CustomLineFormatter + channels: ["!event"] console: type: console process_psr_3_messages: false @@ -59,5 +46,6 @@ when@prod: deprecation: type: stream channels: [deprecation] - path: php://stderr + path: "%kernel.logs_dir%/%kernel.environment%.log" formatter: monolog.formatter.json + -- 2.40.1 From 9292be3feb3dce8ce5e54a536f1555affb96ec4e Mon Sep 17 00:00:00 2001 From: Nicolas Arenas Date: Thu, 20 Feb 2025 13:57:36 +0100 Subject: [PATCH 07/27] refs #1570 Adds DEBIAN folder to create deb package of ogcore --- .env.prod | 4 +- DEBIAN/changelog | 5 +++ DEBIAN/control | 12 ++++++ DEBIAN/copyright | 21 ++++++++++ DEBIAN/postinst | 54 ++++++++++++++++++++++++++ DEBIAN/preinst | 15 +++++++ etc/nginx/sites-available/ogcore.conf | 37 ++++++++++++++++++ etc/php/8.3/fpm/pool.d/ogcore-fpm.conf | 12 ++++++ 8 files changed, 158 insertions(+), 2 deletions(-) create mode 100644 DEBIAN/changelog create mode 100644 DEBIAN/control create mode 100644 DEBIAN/copyright create mode 100644 DEBIAN/postinst create mode 100644 DEBIAN/preinst create mode 100644 etc/nginx/sites-available/ogcore.conf create mode 100644 etc/php/8.3/fpm/pool.d/ogcore-fpm.conf diff --git a/.env.prod b/.env.prod index 28089be..d660f74 100644 --- a/.env.prod +++ b/.env.prod @@ -5,8 +5,8 @@ APP_SECRET=e95c7f17da15ce1b03d77ad655379c34 ###< symfony/framework-bundle ### ###> doctrine/doctrine-bundle ### -DATABASE_URL="mysql://root:root@ogcore-database:3306/ogcore?serverVersion=10.11.2-MariaDB&charset=utf8mb4" -OG_1_DATABASE_URL="mysql://root:root@ogcore-database:3306/ogcore_old_og?serverVersion=10.11.2-MariaDB&charset=utf8mb4" +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 ### diff --git a/DEBIAN/changelog b/DEBIAN/changelog new file mode 100644 index 0000000..f59b142 --- /dev/null +++ b/DEBIAN/changelog @@ -0,0 +1,5 @@ +ogcore (1.0) unstable; urgency=low + + * Initial release. + + -- Your Name Thu, 01 Jan 1970 00:00:00 +0000 diff --git a/DEBIAN/control b/DEBIAN/control new file mode 100644 index 0000000..7f505c1 --- /dev/null +++ b/DEBIAN/control @@ -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 +Description: Description of the ogcore package + This is a longer description of the ogcore package. diff --git a/DEBIAN/copyright b/DEBIAN/copyright new file mode 100644 index 0000000..97ef204 --- /dev/null +++ b/DEBIAN/copyright @@ -0,0 +1,21 @@ +Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: ogcore +Source: + +Files: * +Copyright: 2023 Your Name +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. diff --git a/DEBIAN/postinst b/DEBIAN/postinst new file mode 100644 index 0000000..fd33a61 --- /dev/null +++ b/DEBIAN/postinst @@ -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 diff --git a/DEBIAN/preinst b/DEBIAN/preinst new file mode 100644 index 0000000..dc60472 --- /dev/null +++ b/DEBIAN/preinst @@ -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 \ No newline at end of file diff --git a/etc/nginx/sites-available/ogcore.conf b/etc/nginx/sites-available/ogcore.conf new file mode 100644 index 0000000..f8e7e0a --- /dev/null +++ b/etc/nginx/sites-available/ogcore.conf @@ -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; +} \ No newline at end of file diff --git a/etc/php/8.3/fpm/pool.d/ogcore-fpm.conf b/etc/php/8.3/fpm/pool.d/ogcore-fpm.conf new file mode 100644 index 0000000..69bb2dd --- /dev/null +++ b/etc/php/8.3/fpm/pool.d/ogcore-fpm.conf @@ -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 + -- 2.40.1 From 704b844d7e2cdd75c64ca7668240fba816f31bed Mon Sep 17 00:00:00 2001 From: Nicolas Arenas Date: Thu, 20 Feb 2025 14:01:55 +0100 Subject: [PATCH 08/27] refs #1570 Adds script to create Debian package --- package.sh | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100755 package.sh diff --git a/package.sh b/package.sh new file mode 100755 index 0000000..288c002 --- /dev/null +++ b/package.sh @@ -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 -- 2.40.1 From 560559bba97134f10f73919d3635b4324a9f2efa Mon Sep 17 00:00:00 2001 From: Manuel Aranda Date: Thu, 20 Feb 2025 16:17:13 +0100 Subject: [PATCH 09/27] refs #1555. Install Mercure --- .env | 6 + compose.override.yaml | 6 + composer.json | 1 + composer.lock | 169 +++++++++++++++++- config/api_platform/Client.yaml | 1 + config/bundles.php | 1 + config/packages/api_platform.yaml | 3 + config/packages/mercure.yaml | 8 + config/packages/monolog.yaml | 9 +- docker-compose.yaml | 24 +++ docker/default.conf | 11 ++ phpunit.xml.dist | 1 + src/Command/TestCommand.php | 44 +++++ src/Controller/OgAgent/DeployImageAction.php | 2 - src/Controller/OgAgent/PowerOffAction.php | 7 +- .../OgAgent/Webhook/ClientsController.php | 25 ++- .../Image/CreateAuxFilesAction.php | 2 +- .../OgRepository/Image/DeleteTrashAction.php | 2 - .../OgRepository/Image/RecoverAction.php | 2 +- src/Dto/Output/TraceOutput.php | 4 +- src/Entity/Client.php | 2 + src/EventListener/ClientStatusNotifier.php | 39 ++++ .../TraceStatusProgressNotifier.php | 41 +++++ src/EventSubscriber/MercureSubscriber.php | 66 +++++++ src/Model/ClientStatus.php | 11 +- symfony.lock | 99 ++++++++++ 26 files changed, 556 insertions(+), 30 deletions(-) create mode 100644 config/packages/mercure.yaml create mode 100644 src/Command/TestCommand.php create mode 100644 src/EventListener/ClientStatusNotifier.php create mode 100644 src/EventListener/TraceStatusProgressNotifier.php create mode 100644 src/EventSubscriber/MercureSubscriber.php diff --git a/.env b/.env index 3a0cfeb..24c54a5 100644 --- a/.env +++ b/.env @@ -50,3 +50,9 @@ UDS_AUTH_USERNAME="natiqindel" UDS_AUTH_PASSWORD="correct horse battery staple" UDS_URL=https://localhost:8087/uds/rest/ ###< 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 ### diff --git a/compose.override.yaml b/compose.override.yaml index c5612b0..013f7da 100644 --- a/compose.override.yaml +++ b/compose.override.yaml @@ -5,3 +5,9 @@ services: ports: - "5432" ###< doctrine/doctrine-bundle ### + +###> symfony/mercure-bundle ### + mercure: + ports: + - "80" +###< symfony/mercure-bundle ### diff --git a/composer.json b/composer.json index 9d05bf0..63cc9fa 100644 --- a/composer.json +++ b/composer.json @@ -27,6 +27,7 @@ "symfony/flex": "^2", "symfony/framework-bundle": "6.4.*", "symfony/http-client": "6.4.*", + "symfony/mercure-bundle": "^0.3.9", "symfony/monolog-bundle": "^3.10", "symfony/property-access": "6.4.*", "symfony/property-info": "6.4.*", diff --git a/composer.lock b/composer.lock index 958dc40..4a369dc 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "2df97d4a1797242acadb47d83b3fbe98", + "content-hash": "ac4764e765324b86e616d7eb47a55e63", "packages": [ { "name": "api-platform/core", @@ -4981,6 +4981,173 @@ ], "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", "version": "v6.4.13", diff --git a/config/api_platform/Client.yaml b/config/api_platform/Client.yaml index f2322f6..a4c48f5 100644 --- a/config/api_platform/Client.yaml +++ b/config/api_platform/Client.yaml @@ -7,6 +7,7 @@ resources: groups: ['default', 'client:read'] denormalizationContext: groups: ['client:write'] + operations: ApiPlatform\Metadata\GetCollection: provider: App\State\Provider\ClientProvider diff --git a/config/bundles.php b/config/bundles.php index f1a3820..15fa539 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -17,4 +17,5 @@ return [ Stof\DoctrineExtensionsBundle\StofDoctrineExtensionsBundle::class => ['all' => true], DAMA\DoctrineTestBundle\DAMADoctrineTestBundle::class => ['test' => true], Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true], + Symfony\Bundle\MercureBundle\MercureBundle::class => ['all' => true], ]; diff --git a/config/packages/api_platform.yaml b/config/packages/api_platform.yaml index 7adc4f3..b957e6a 100644 --- a/config/packages/api_platform.yaml +++ b/config/packages/api_platform.yaml @@ -5,6 +5,9 @@ api_platform: path_segment_name_generator: api_platform.path_segment_name_generator.dash defaults: pagination_client_items_per_page: true + mercure: + enabled: true + collection: pagination: items_per_page_parameter_name: 'itemsPerPage' diff --git a/config/packages/mercure.yaml b/config/packages/mercure.yaml new file mode 100644 index 0000000..f2a7395 --- /dev/null +++ b/config/packages/mercure.yaml @@ -0,0 +1,8 @@ +mercure: + hubs: + default: + url: '%env(MERCURE_URL)%' + public_url: '%env(MERCURE_PUBLIC_URL)%' + jwt: + secret: '%env(MERCURE_JWT_SECRET)%' + publish: '*' diff --git a/config/packages/monolog.yaml b/config/packages/monolog.yaml index 4dfd5a8..d0608d0 100644 --- a/config/packages/monolog.yaml +++ b/config/packages/monolog.yaml @@ -20,11 +20,10 @@ when@test: monolog: handlers: main: - type: fingers_crossed - action_level: error - handler: nested - excluded_http_codes: [404, 405] - channels: ["!event"] + type: stream + path: "%kernel.logs_dir%/%kernel.environment%.log" + level: error + nested: type: stream path: "%kernel.logs_dir%/%kernel.environment%.log" diff --git a/docker-compose.yaml b/docker-compose.yaml index b6c7909..c15177a 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -42,7 +42,31 @@ services: networks: - 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: + mercure_data: + mercure_config: database_data: networks: diff --git a/docker/default.conf b/docker/default.conf index 0cb5a45..c67e1ef 100644 --- a/docker/default.conf +++ b/docker/default.conf @@ -15,6 +15,17 @@ server { ssl_certificate /etc/nginx/certs/ogcore.uds-test.net.crt.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// { rewrite ^/opengnsys/rest/ous//([0-9]+)/images /opengnsys/rest/ous/$1/images; rewrite ^/opengnsys/rest/ous//([0-9]+)/labs /opengnsys/rest/ous/$1/labs; diff --git a/phpunit.xml.dist b/phpunit.xml.dist index f6da34e..7b99837 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -15,6 +15,7 @@ + diff --git a/src/Command/TestCommand.php b/src/Command/TestCommand.php new file mode 100644 index 0000000..fa8306b --- /dev/null +++ b/src/Command/TestCommand.php @@ -0,0 +1,44 @@ +entityManager->getRepository(Trace::class)->find(7236); + + $trace->setStatus(TraceStatus::SUCCESS); + $trace->setProgress(1000); + + $this->entityManager->persist($trace); + $this->entityManager->flush(); + + return Command::SUCCESS; + } +} diff --git a/src/Controller/OgAgent/DeployImageAction.php b/src/Controller/OgAgent/DeployImageAction.php index 65a5de9..1c5b1c9 100644 --- a/src/Controller/OgAgent/DeployImageAction.php +++ b/src/Controller/OgAgent/DeployImageAction.php @@ -47,8 +47,6 @@ class DeployImageAction extends AbstractController throw new ValidatorException('IP is required'); } - $partitionInfo = json_decode($image->getPartitionInfo(), true); - $method = match ($input->method) { DeployMethodTypes::MULTICAST_UFTP_DIRECT, DeployMethodTypes::MULTICAST_UDPCAST_DIRECT, => 'multicast-direct', DeployMethodTypes::MULTICAST, DeployMethodTypes::MULTICAST_UFTP, DeployMethodTypes::MULTICAST_UDPCAST => 'multicast', diff --git a/src/Controller/OgAgent/PowerOffAction.php b/src/Controller/OgAgent/PowerOffAction.php index 2f5f0b1..4817ca5 100644 --- a/src/Controller/OgAgent/PowerOffAction.php +++ b/src/Controller/OgAgent/PowerOffAction.php @@ -41,12 +41,17 @@ class PowerOffAction extends AbstractController public function __invoke(MultipleClientsInput $input): JsonResponse { foreach ($input->clients as $clientEntity) { + /** @var Client $client */ $client = $clientEntity->getEntity(); if (!$client->getIp()) { throw new ValidatorException('IP is required'); } + if ($client->getStatus() === ClientStatus::OFF) { + continue; + } + $data = [ 'nfn' => 'Apagar', 'ids' => '0' @@ -70,7 +75,7 @@ class PowerOffAction extends AbstractController $jobId = json_decode($response->getContent(), true)['job_id']; - $client->setStatus(ClientStatus::OFF); + $client->setStatus(ClientStatus::TURNING_OFF); $this->entityManager->persist($client); $this->entityManager->flush(); diff --git a/src/Controller/OgAgent/Webhook/ClientsController.php b/src/Controller/OgAgent/Webhook/ClientsController.php index c6c9573..b5970e3 100644 --- a/src/Controller/OgAgent/Webhook/ClientsController.php +++ b/src/Controller/OgAgent/Webhook/ClientsController.php @@ -34,6 +34,11 @@ use Symfony\Contracts\HttpClient\HttpClientInterface; #[AsController] 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( protected readonly EntityManagerInterface $entityManager, public readonly CreateAuxFilesAction $createAuxFilesAction, @@ -54,7 +59,7 @@ class ClientsController extends AbstractController public function index(Request $request): JsonResponse { $data = $request->toArray(); - $requiredFields = ['nfn', 'res', 'der', 'job_id']; + $requiredFields = ['job_id']; foreach ($requiredFields as $field) { if (!isset($data[$field])) { @@ -64,7 +69,16 @@ class ClientsController extends AbstractController $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']]); /** @var ImageImageRepository $imageImageRepository */ $imageImageRepository = $this->entityManager->getRepository(ImageImageRepository::class)->findOneBy(['uuid' => $data['idi']]); @@ -82,10 +96,8 @@ class ClientsController extends AbstractController if ($data['res'] === 1) { $trace->setStatus(TraceStatus::SUCCESS); $trace->setFinishedAt(new \DateTime()); - $imageImageRepository->setStatus(ImageStatus::AUX_FILES_PENDING); $imageImageRepository->setCreated(true); - $this->entityManager->persist($imageImageRepository); $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->createSoftwareProfile($data['inv_sft'], $imageImageRepository); $this->logger->info('Start aux files ogrepo API ', ['image' => (string) $imageImageRepository->getUuid()]); + try { $this->createAuxFilesAction->__invoke($imageImageRepository); } catch (\Exception $e) { $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()]); } else { $trace->setStatus(TraceStatus::FAILED); @@ -119,9 +133,8 @@ class ClientsController extends AbstractController $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']]); - $client = $trace->getClient(); if ($data['res'] === 1) { diff --git a/src/Controller/OgRepository/Image/CreateAuxFilesAction.php b/src/Controller/OgRepository/Image/CreateAuxFilesAction.php index 2f927a1..7a28bc2 100644 --- a/src/Controller/OgRepository/Image/CreateAuxFilesAction.php +++ b/src/Controller/OgRepository/Image/CreateAuxFilesAction.php @@ -45,7 +45,7 @@ class CreateAuxFilesAction extends AbstractOgRepositoryController $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); diff --git a/src/Controller/OgRepository/Image/DeleteTrashAction.php b/src/Controller/OgRepository/Image/DeleteTrashAction.php index 4a370af..8ff17d7 100644 --- a/src/Controller/OgRepository/Image/DeleteTrashAction.php +++ b/src/Controller/OgRepository/Image/DeleteTrashAction.php @@ -37,8 +37,6 @@ class DeleteTrashAction extends AbstractOgRepositoryController $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'); $this->logger->info('Image deleted', ['image' => $image->getName()]); diff --git a/src/Controller/OgRepository/Image/RecoverAction.php b/src/Controller/OgRepository/Image/RecoverAction.php index 123aa65..1b9e4bf 100644 --- a/src/Controller/OgRepository/Image/RecoverAction.php +++ b/src/Controller/OgRepository/Image/RecoverAction.php @@ -44,7 +44,7 @@ class RecoverAction extends AbstractOgRepositoryController $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); diff --git a/src/Dto/Output/TraceOutput.php b/src/Dto/Output/TraceOutput.php index bc3f35e..14ea96b 100644 --- a/src/Dto/Output/TraceOutput.php +++ b/src/Dto/Output/TraceOutput.php @@ -35,7 +35,7 @@ final class TraceOutput extends AbstractOutput public ?\DateTimeInterface $finishedAt = null; #[Groups(['trace:read'])] - public ?int $progress = null; + public ?float $progress = null; #[Groups(['trace:read'])] public \DateTime $createdAt; @@ -59,7 +59,7 @@ final class TraceOutput extends AbstractOutput $this->output = $trace->getOutput(); $this->input = $trace->getInput(); $this->finishedAt = $trace->getFinishedAt(); - $this->progress = $trace->getProgress(); + $this->progress = $trace->getProgress() / 100; $this->createdAt = $trace->getCreatedAt(); $this->createdBy = $trace->getCreatedBy(); } diff --git a/src/Entity/Client.php b/src/Entity/Client.php index b49aacd..6f210ba 100644 --- a/src/Entity/Client.php +++ b/src/Entity/Client.php @@ -2,6 +2,7 @@ namespace App\Entity; +use ApiPlatform\Metadata\ApiResource; use App\Repository\ClientRepository; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; @@ -152,6 +153,7 @@ class Client extends AbstractEntity return $this->status; } + #[ORM\PostPersist] public function setStatus(?string $status): static { $this->status = $status; diff --git a/src/EventListener/ClientStatusNotifier.php b/src/EventListener/ClientStatusNotifier.php new file mode 100644 index 0000000..e16e5b5 --- /dev/null +++ b/src/EventListener/ClientStatusNotifier.php @@ -0,0 +1,39 @@ +logger = $logger; + } + + public function postUpdate(Client $client, PostUpdateEventArgs $event): 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(), + ]); + } +} \ No newline at end of file diff --git a/src/EventListener/TraceStatusProgressNotifier.php b/src/EventListener/TraceStatusProgressNotifier.php new file mode 100644 index 0000000..29b2cc1 --- /dev/null +++ b/src/EventListener/TraceStatusProgressNotifier.php @@ -0,0 +1,41 @@ +logger = $logger; + } + + public function postUpdate(Trace $trace, PostUpdateEventArgs $event): 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(), + ]); + } +} \ No newline at end of file diff --git a/src/EventSubscriber/MercureSubscriber.php b/src/EventSubscriber/MercureSubscriber.php new file mode 100644 index 0000000..6b6a843 --- /dev/null +++ b/src/EventSubscriber/MercureSubscriber.php @@ -0,0 +1,66 @@ +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() + ]); + } +} diff --git a/src/Model/ClientStatus.php b/src/Model/ClientStatus.php index 88f2fe2..c9882bb 100644 --- a/src/Model/ClientStatus.php +++ b/src/Model/ClientStatus.php @@ -5,27 +5,20 @@ namespace App\Model; final class ClientStatus { public const string OFF = 'off'; - public const string INITIALIZING = 'initializing'; - + public const string TURNING_OFF = 'turning-off'; public const string OG_LIVE = 'og-live'; - public const string BUSY = 'busy'; - public const string LINUX = 'linux'; - public const string LINUX_SESSION = 'linux-session'; - public const string MACOS = 'macos'; - public const string MACOS_SESSION = 'macos-session'; - public const string WINDOWS = 'windows'; - public const string WINDOWS_SESSION = 'windows-session'; private const array CLIENT_STATUSES = [ self::OFF => 'Apagado', + self::TURNING_OFF => 'Apagando', self::INITIALIZING => 'Inicializando', self::OG_LIVE => 'OG Live', self::BUSY => 'Ocupado', diff --git a/symfony.lock b/symfony.lock index db1b578..03e0eb7 100644 --- a/symfony.lock +++ b/symfony.lock @@ -13,6 +13,18 @@ "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": { "version": "2.13", "recipe": { @@ -27,6 +39,18 @@ "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": { "version": "3.3", "recipe": { @@ -78,6 +102,20 @@ "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": { "version": "2.1", "recipe": { @@ -143,6 +181,27 @@ "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": { "version": "3.10", "recipe": { @@ -155,6 +214,21 @@ "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": { "version": "6.4", "recipe": { @@ -218,5 +292,30 @@ "files": [ "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" + ] } } -- 2.40.1 From 3ebe5cb1bca7c4fd8f0d486deafb622a07d06a12 Mon Sep 17 00:00:00 2001 From: Manuel Aranda Date: Fri, 21 Feb 2025 10:53:36 +0100 Subject: [PATCH 10/27] refs #1603. OGrepo backupImage integration --- config/api_platform/ImageImageRepository.yaml | 8 +++ .../OgRepository/Image/BackupImageAction.php | 67 +++++++++++++++++++ .../Image/CreateAuxFilesAction.php | 2 - .../Webhook/ResponseController.php | 10 ++- src/Dto/Input/BackupImageInput.php | 24 +++++++ src/Model/CommandTypes.php | 2 + 6 files changed, 109 insertions(+), 4 deletions(-) create mode 100644 src/Controller/OgRepository/Image/BackupImageAction.php create mode 100644 src/Dto/Input/BackupImageInput.php diff --git a/config/api_platform/ImageImageRepository.yaml b/config/api_platform/ImageImageRepository.yaml index 1d77384..3e13b96 100644 --- a/config/api_platform/ImageImageRepository.yaml +++ b/config/api_platform/ImageImageRepository.yaml @@ -48,6 +48,14 @@ resources: uriTemplate: /image-image-repositories/{uuid}/deploy-image 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: shortName: OgRepository Server description: Delete Image in OgRepository diff --git a/src/Controller/OgRepository/Image/BackupImageAction.php b/src/Controller/OgRepository/Image/BackupImageAction.php new file mode 100644 index 0000000..cb742fc --- /dev/null +++ b/src/Controller/OgRepository/Image/BackupImageAction.php @@ -0,0 +1,67 @@ +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(), + 'imageUuid' => $imageImageRepository->getUuid(), + 'ID_img' => $imageImageRepository->getImageFullsum(), + 'repo_ip' => $imageImageRepository->getRepository()->getIp(), + 'remote_path' => '/var/lib/ogrepository/images', + ]; + + $this->createService->__invoke($image->getClient(), CommandTypes::BACKUP_IMAGE, TraceStatus::IN_PROGRESS, $content['job_id'], $inputData); + + return new JsonResponse(data: $content, status: Response::HTTP_OK); + } +} \ No newline at end of file diff --git a/src/Controller/OgRepository/Image/CreateAuxFilesAction.php b/src/Controller/OgRepository/Image/CreateAuxFilesAction.php index 7a28bc2..06c09dd 100644 --- a/src/Controller/OgRepository/Image/CreateAuxFilesAction.php +++ b/src/Controller/OgRepository/Image/CreateAuxFilesAction.php @@ -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->logger->info('Aux files created successfully', ['image' => $image->getName()]); - $data->setStatus(ImageStatus::AUX_FILES_PENDING); $this->entityManager->persist($data); $this->entityManager->flush(); diff --git a/src/Controller/OgRepository/Webhook/ResponseController.php b/src/Controller/OgRepository/Webhook/ResponseController.php index c0487b5..4dc729c 100644 --- a/src/Controller/OgRepository/Webhook/ResponseController.php +++ b/src/Controller/OgRepository/Webhook/ResponseController.php @@ -49,6 +49,8 @@ class ResponseController extends AbstractOgRepositoryController $this->processImageAction($data, 'transfer'); } elseif (str_starts_with($action, "ExportImage_")) { $this->processImageAction($data, 'export'); + } elseif (str_starts_with($action, "BackupImage_")) { + $this->processImageAction($data, 'backup'); } else { return new JsonResponse(['message' => 'Invalid action'], Response::HTTP_BAD_REQUEST); } @@ -84,6 +86,8 @@ class ResponseController extends AbstractOgRepositoryController */ private function processImageAction(array $data, string $actionType): void { + $imageImageRepository = null; + $trace = $this->entityManager->getRepository(Trace::class)->findOneBy(['jobId' => $data['job_id']]); $imageUuid = $trace->getInput()['imageUuid']; $repositoryUuid = $trace->getInput()['repositoryUuid']; @@ -118,13 +122,15 @@ class ResponseController extends AbstractOgRepositoryController $this->logger->info("Image $actionType successful", ['image' => $image->getName()]); - // 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 + if ($imageImageRepository){ + $newImageImageRepository->setImageFullsum($imageImageRepository->getImageFullsum()); + } + $this->entityManager->persist($newImageImageRepository); $this->entityManager->persist($image); diff --git a/src/Dto/Input/BackupImageInput.php b/src/Dto/Input/BackupImageInput.php new file mode 100644 index 0000000..32542ee --- /dev/null +++ b/src/Dto/Input/BackupImageInput.php @@ -0,0 +1,24 @@ + 'Update Cache', self::CREATE_IMAGE => 'Create Image', self::CREATE_IMAGE_AUX_FILE => 'Crear fichero auxiliar en repositorio', + self::BACKUP_IMAGE => 'Crear backup de imagen', self::IMPORT_IMAGE => 'Importar imagen', self::EXPORT_IMAGE => 'Exportar imagen', self::POWER_ON => 'Encender', -- 2.40.1 From c3ebc6426400fbc0b185b1e5f6455e42ad5f57be Mon Sep 17 00:00:00 2001 From: Manuel Aranda Date: Fri, 21 Feb 2025 12:49:59 +0100 Subject: [PATCH 11/27] refs #1603. Backup image changes --- src/Controller/OgRepository/Image/BackupImageAction.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Controller/OgRepository/Image/BackupImageAction.php b/src/Controller/OgRepository/Image/BackupImageAction.php index cb742fc..159c509 100644 --- a/src/Controller/OgRepository/Image/BackupImageAction.php +++ b/src/Controller/OgRepository/Image/BackupImageAction.php @@ -54,10 +54,11 @@ class BackupImageAction extends AbstractOgRepositoryController $inputData = [ 'imageName' => $image->getName(), + 'repositoryUuid' => $repository->getUuid(), 'imageUuid' => $imageImageRepository->getUuid(), 'ID_img' => $imageImageRepository->getImageFullsum(), - 'repo_ip' => $imageImageRepository->getRepository()->getIp(), - 'remote_path' => '/var/lib/ogrepository/images', + 'repo_ip' => $input->repoIp, + 'remote_path' => $input->remotePath ]; $this->createService->__invoke($image->getClient(), CommandTypes::BACKUP_IMAGE, TraceStatus::IN_PROGRESS, $content['job_id'], $inputData); -- 2.40.1 From d881c763cfa8497ffd07f262a37b1631d45e58ba Mon Sep 17 00:00:00 2001 From: Manuel Aranda Date: Fri, 21 Feb 2025 13:16:04 +0100 Subject: [PATCH 12/27] refs #1603. Backup image changes --- .../OgRepository/Image/BackupImageAction.php | 5 + .../Webhook/ResponseController.php | 148 ++++++++---------- src/Model/ImageStatus.php | 1 + 3 files changed, 72 insertions(+), 82 deletions(-) diff --git a/src/Controller/OgRepository/Image/BackupImageAction.php b/src/Controller/OgRepository/Image/BackupImageAction.php index 159c509..549a46b 100644 --- a/src/Controller/OgRepository/Image/BackupImageAction.php +++ b/src/Controller/OgRepository/Image/BackupImageAction.php @@ -63,6 +63,11 @@ class BackupImageAction extends AbstractOgRepositoryController $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); } } \ No newline at end of file diff --git a/src/Controller/OgRepository/Webhook/ResponseController.php b/src/Controller/OgRepository/Webhook/ResponseController.php index 4dc729c..ae66f39 100644 --- a/src/Controller/OgRepository/Webhook/ResponseController.php +++ b/src/Controller/OgRepository/Webhook/ResponseController.php @@ -7,7 +7,6 @@ use App\Entity\Image; use App\Entity\ImageImageRepository; use App\Entity\ImageRepository; use App\Entity\Trace; -use App\Model\CommandTypes; use App\Model\ImageStatus; use App\Model\TraceStatus; use Doctrine\ORM\EntityManagerInterface; @@ -18,123 +17,81 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Attribute\AsController; 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; #[AsController] class ResponseController extends AbstractOgRepositoryController { - /** - * @throws RedirectionExceptionInterface - * @throws ClientExceptionInterface - * @throws TransportExceptionInterface - * @throws ServerExceptionInterface - */ #[Route('/og-repository/webhook', name: 'og_repository_webhook', methods: ['POST'])] public function repositoryWebhook(Request $request): JsonResponse { $data = json_decode($request->getContent(), true); 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']; - if (str_starts_with($action, "CreateAuxiliarFiles_")) { - $this->handleCreateAuxFiles($data); - } elseif (str_starts_with($action, "TransferImage_")) { - $this->processImageAction($data, 'transfer'); - } elseif (str_starts_with($action, "ExportImage_")) { - $this->processImageAction($data, 'export'); - } elseif (str_starts_with($action, "BackupImage_")) { - $this->processImageAction($data, 'backup'); - } else { - return new JsonResponse(['message' => 'Invalid action'], Response::HTTP_BAD_REQUEST); - } - - return new JsonResponse($data, Response::HTTP_OK); + return match (true) { + str_starts_with($action, "CreateAuxiliarFiles_") => $this->handleImageRepositoryAction($data, true), + str_starts_with($action, "TransferImage_"), str_starts_with($action, "ExportImage_") => $this->processImageAction($data), + str_starts_with($action, "BackupImage_") => $this->handleImageRepositoryAction($data), + default => $this->jsonResponseError('Invalid action', Response::HTTP_BAD_REQUEST), + }; } - 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']]); - $imageUuid = $trace->getInput()['imageUuid']; + $trace = $this->getTrace($data['job_id']); + if (!$trace) return $this->jsonResponseError('Trace not found'); - /* @var ImageImageRepository $imageImageRepository */ - $imageImageRepository = $this->entityManager->getRepository(ImageImageRepository::class)->findOneBy(['uuid' => $imageUuid]); + $imageImageRepository = $this->getImageImageRepository($trace); + if (!$imageImageRepository) return $this->jsonResponseError('Image not found', Response::HTTP_NOT_FOUND, $trace); - if ($imageImageRepository === null) { - $this->updateTraceStatus($trace, TraceStatus::FAILED, 'Image not found'); - return; + if ($setFullsum) { + $imageImageRepository->setImageFullsum($data['image_id']); } - - $imageImageRepository->setImageFullsum($data['image_id']); $imageImageRepository->setStatus(ImageStatus::SUCCESS); - $this->entityManager->persist($imageImageRepository); + $this->entityManager->persist($imageImageRepository); $this->updateTraceStatus($trace, TraceStatus::SUCCESS); + + return new JsonResponse(['message' => 'Success'], Response::HTTP_OK); } - /** - * @throws TransportExceptionInterface - * @throws ServerExceptionInterface - * @throws RedirectionExceptionInterface - * @throws ClientExceptionInterface - */ - private function processImageAction(array $data, string $actionType): void + private function processImageAction(array $data): JsonResponse { - $imageImageRepository = null; - - $trace = $this->entityManager->getRepository(Trace::class)->findOneBy(['jobId' => $data['job_id']]); - $imageUuid = $trace->getInput()['imageUuid']; - $repositoryUuid = $trace->getInput()['repositoryUuid']; - - $image = $this->entityManager->getRepository(Image::class)->findOneBy(['uuid' => $imageUuid]); - $repository = $this->entityManager->getRepository(ImageRepository::class)->findOneBy(['uuid' => $repositoryUuid]); + $trace = $this->getTrace($data['job_id']); + if (!$trace) return $this->jsonResponseError('Trace not found'); if ($data['success'] !== true) { - $this->updateTraceStatus($trace, TraceStatus::FAILED, 'Action failed'); - return; + return $this->jsonResponseError('Action failed', Response::HTTP_BAD_REQUEST, $trace); } - if ($image === null) { - $this->updateTraceStatus($trace, TraceStatus::FAILED, 'Image not found'); - return; - } + $image = $this->getImage($trace); + $repository = $this->getRepository($trace); + if (!$image) return $this->jsonResponseError('Image not found', Response::HTTP_NOT_FOUND, $trace); + if (!$repository) return $this->jsonResponseError('Repository not found', Response::HTTP_NOT_FOUND, $trace); - if ($repository === null) { - $this->updateTraceStatus($trace, TraceStatus::FAILED, 'Repository not found'); - return; - } + $newImageRepo = new ImageImageRepository(); + $newImageRepo->setImage($image); + $newImageRepo->setRepository($repository); + $newImageRepo->setStatus(ImageStatus::SUCCESS); - if (isset($trace->getInput()['imageImageRepositoryUuid'])) { - $imageImageRepositoryUuid = $trace->getInput()['imageImageRepositoryUuid']; - $imageImageRepository = $this->entityManager->getRepository(ImageImageRepository::class)->findOneBy(['uuid' => $imageImageRepositoryUuid]); + if ($trace->getInput()['imageImageRepositoryUuid'] ?? false) { + $existingRepo = $this->entityManager->getRepository(ImageImageRepository::class) + ->findOneBy(['uuid' => $trace->getInput()['imageImageRepositoryUuid']]); - if ($imageImageRepository) { - $this->updateTraceStatus($trace, TraceStatus::FAILED, 'Image repository not found'); - $imageImageRepository->setStatus(ImageStatus::SUCCESS); + if ($existingRepo) { + $newImageRepo->setImageFullsum($existingRepo->getImageFullsum()); } } - $this->logger->info("Image $actionType successful", ['image' => $image->getName()]); - - $newImageImageRepository = new ImageImageRepository(); - $newImageImageRepository->setImage($image); - $newImageImageRepository->setRepository($repository); - $newImageImageRepository->setStatus(ImageStatus::SUCCESS); - - if ($imageImageRepository){ - $newImageImageRepository->setImageFullsum($imageImageRepository->getImageFullsum()); - } - - $this->entityManager->persist($newImageImageRepository); - $this->entityManager->persist($image); - + $this->entityManager->persist($newImageRepo); $this->updateTraceStatus($trace, TraceStatus::SUCCESS); + + return new JsonResponse(['message' => 'Success'], Response::HTTP_OK); } private function updateTraceStatus(Trace $trace, string $status, string $output = null): void @@ -148,9 +105,36 @@ class ResponseController extends AbstractOgRepositoryController $this->entityManager->persist($trace); $this->entityManager->flush(); + } - if ($status === TraceStatus::FAILED) { - new JsonResponse(['message' => $output], Response::HTTP_NOT_FOUND); + private function getTrace(string $jobId): ?Trace + { + 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); } } diff --git a/src/Model/ImageStatus.php b/src/Model/ImageStatus.php index 9eaf9ba..341eee6 100644 --- a/src/Model/ImageStatus.php +++ b/src/Model/ImageStatus.php @@ -11,6 +11,7 @@ final class ImageStatus public const string TRASH = 'trash'; public const string FAILED = 'failed'; public const string TRANSFERRING = 'transferring'; + public const string BACKUP = 'backup'; private const array STATUS = [ self::PENDING => 'Pendiente', -- 2.40.1 From a00dc7a59ff5220dc48f95bb4c51fe723d5b8462 Mon Sep 17 00:00:00 2001 From: Manuel Aranda Date: Tue, 25 Feb 2025 08:48:53 +0100 Subject: [PATCH 13/27] refs #1555. Changes eventListener Mercure --- src/EventListener/ClientStatusNotifier.php | 13 +++++++++++++ .../TraceStatusProgressNotifier.php | 16 +++++++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/EventListener/ClientStatusNotifier.php b/src/EventListener/ClientStatusNotifier.php index e16e5b5..8ec0ff9 100644 --- a/src/EventListener/ClientStatusNotifier.php +++ b/src/EventListener/ClientStatusNotifier.php @@ -24,6 +24,19 @@ class ClientStatusNotifier } 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', diff --git a/src/EventListener/TraceStatusProgressNotifier.php b/src/EventListener/TraceStatusProgressNotifier.php index 29b2cc1..53eaff9 100644 --- a/src/EventListener/TraceStatusProgressNotifier.php +++ b/src/EventListener/TraceStatusProgressNotifier.php @@ -25,10 +25,24 @@ class TraceStatusProgressNotifier } 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]) + json_encode(['@id' => '/traces/' . $trace->getUuid(), 'status' => $trace->getStatus(), 'progress' => $trace->getProgress() / 100]) ); $this->hub->publish($update); -- 2.40.1 From b3b3bf892d5ffdf952be6fccd87b5d5ad8d435b9 Mon Sep 17 00:00:00 2001 From: Manuel Aranda Date: Thu, 27 Feb 2025 11:12:27 +0100 Subject: [PATCH 14/27] refs #1558. Add client/subnet new UX modal --- CHANGELOG.md | 31 ++++++++-- config/services/api_platform.yaml | 2 +- migrations/Version20250225081416.php | 31 ++++++++++ migrations/Version20250227095120.php | 31 ++++++++++ .../OgBoot/PxeBootFile/PostAction.php | 2 +- .../OgDhcp/Subnet/PostHostAction.php | 59 ++++++++++++++----- src/Dto/Input/NetworkSettingsInput.php | 5 ++ src/Dto/Input/SubnetAddHostInput.php | 5 +- src/Dto/Input/UserInput.php | 5 ++ src/Dto/Output/NetworkSettingsOutput.php | 4 ++ src/Dto/Output/OrganizationalUnitOutput.php | 2 +- src/Dto/Output/UserOutput.php | 3 + src/Entity/NetworkSettings.php | 15 +++++ src/Entity/User.php | 15 +++++ src/Factory/UserFactory.php | 1 + src/Formatter/CustomLineFormatter.php | 1 + src/Lexik/JWTCreatedListener.php | 1 + src/Repository/ClientRepository.php | 40 +++++++++---- src/State/Provider/ClientProvider.php | 19 ++++-- 19 files changed, 233 insertions(+), 39 deletions(-) create mode 100644 migrations/Version20250225081416.php create mode 100644 migrations/Version20250227095120.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 0db93f5..2d3566e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,31 @@ # Changelog +## [0.8.2] - 2025-03-04 +### 🔹 Added +- Nueva funcionalidad para tener notificaciones en tiempo real. Instalación de bundle "Mercure". +- Nuevo endpoint "backup image". Integracion con ogRepository. + +### ⚡ Changed +- Cambios en logs. Cambios en salida (stderror -> file.log) + +--- +## [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 ### 🔹 Added @@ -7,13 +34,9 @@ - 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. -### 🛠️ Fixed - ### ⚡ Changed - Refactorización del webhook de ogRepository. -### 🛑 Removed - --- ## Formato de cambios: diff --git a/config/services/api_platform.yaml b/config/services/api_platform.yaml index 2240693..379733d 100644 --- a/config/services/api_platform.yaml +++ b/config/services/api_platform.yaml @@ -20,7 +20,7 @@ services: api_platform.filter.client.search: 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', 'name': 'partial', 'serialNumber': 'exact', 'template.id': 'exact', status: 'exact', organizationalUnit.id: 'exact', mac: 'exact', ip: 'exact', subnet.id: 'exact' } ] tags: [ 'api_platform.filter' ] api_platform.filter.client.exist: diff --git a/migrations/Version20250225081416.php b/migrations/Version20250225081416.php new file mode 100644 index 0000000..2c72bcd --- /dev/null +++ b/migrations/Version20250225081416.php @@ -0,0 +1,31 @@ +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'); + } +} diff --git a/migrations/Version20250227095120.php b/migrations/Version20250227095120.php new file mode 100644 index 0000000..8aef7e3 --- /dev/null +++ b/migrations/Version20250227095120.php @@ -0,0 +1,31 @@ +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'); + } +} diff --git a/src/Controller/OgBoot/PxeBootFile/PostAction.php b/src/Controller/OgBoot/PxeBootFile/PostAction.php index 73ce878..b2245b8 100644 --- a/src/Controller/OgBoot/PxeBootFile/PostAction.php +++ b/src/Controller/OgBoot/PxeBootFile/PostAction.php @@ -46,7 +46,7 @@ class PostAction extends AbstractOgBootController 'router' => $client->getOrganizationalUnit()->getNetworkSettings()->getRouter(), 'netmask' => $client->getOrganizationalUnit()->getNetworkSettings() ? $client->getOrganizationalUnit()->getNetworkSettings()->getNetmask() : '255.255.255.0', 'computer_name' => $client->getName(), - 'netiface' => $client->getNetiface(), + 'netiface' => $client->getNetiface() ? $client->getNetiface() : $client->getOrganizationalUnit()->getNetworkSettings()->getNetiface(), 'group' => $client->getOrganizationalUnit()->getName(), 'ogrepo' => $ogRepoIp, 'ogcore' => $this->ogCoreIP, diff --git a/src/Controller/OgDhcp/Subnet/PostHostAction.php b/src/Controller/OgDhcp/Subnet/PostHostAction.php index e2f4027..ba9de31 100644 --- a/src/Controller/OgDhcp/Subnet/PostHostAction.php +++ b/src/Controller/OgDhcp/Subnet/PostHostAction.php @@ -29,27 +29,54 @@ class PostHostAction extends AbstractOgDhcpController */ public function __invoke(SubnetAddHostInput $input, Subnet $subnet): JsonResponse { - $client = $input->client; + $clients = $input->clients; - /** @var Client $clientEntity */ - $clientEntity = $client->getEntity(); + $success = []; + $errors = []; - $data = [ - 'host' => $clientEntity->getName(), - 'macAddress' => strtolower($clientEntity->getMac()), - 'address' => $clientEntity->getIp(), - ]; + foreach ($clients as $client) { + /** @var Client $clientEntity */ + $clientEntity = $client->getEntity(); - $params = [ - 'json' => $data - ]; + $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); - $this->entityManager->persist($subnet); - $this->entityManager->flush(); + try { + $content = $this->createRequest( + '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 + ); } } \ No newline at end of file diff --git a/src/Dto/Input/NetworkSettingsInput.php b/src/Dto/Input/NetworkSettingsInput.php index 46cec1f..fe3ef70 100644 --- a/src/Dto/Input/NetworkSettingsInput.php +++ b/src/Dto/Input/NetworkSettingsInput.php @@ -41,6 +41,9 @@ class NetworkSettingsInput #[Groups(['organizational-unit:write'])] public ?string $ntp = null; + #[Groups(['organizational-unit:write'])] + public ?string $netiface = null; + #[OrganizationalUnitP2PMode] #[Groups(['organizational-unit:write'])] public ?string $p2pMode = null; @@ -93,6 +96,7 @@ class NetworkSettingsInput $this->netmask = $networkSettings->getNetmask(); $this->router = $networkSettings->getRouter(); $this->ntp = $networkSettings->getNtp(); + $this->netiface = $networkSettings->getNetiface(); $this->p2pMode = $networkSettings->getP2pMode(); $this->p2pTime = $networkSettings->getP2pTime(); $this->mcastIp = $networkSettings->getMcastIp(); @@ -130,6 +134,7 @@ class NetworkSettingsInput $networkSettings->setDns($this->dns); $networkSettings->setNetmask($this->netmask); $networkSettings->setRouter($this->router); + $networkSettings->setNetiface($this->netiface); $networkSettings->setNtp($this->ntp); $networkSettings->setP2pMode($this->p2pMode); $networkSettings->setP2pTime($this->p2pTime); diff --git a/src/Dto/Input/SubnetAddHostInput.php b/src/Dto/Input/SubnetAddHostInput.php index 0129a86..d903357 100644 --- a/src/Dto/Input/SubnetAddHostInput.php +++ b/src/Dto/Input/SubnetAddHostInput.php @@ -12,7 +12,10 @@ use Symfony\Component\Validator\Constraints as Assert; final class SubnetAddHostInput { + /** + * @var ClientOutput[] + */ #[Assert\NotNull] #[Groups(['subnet:write'])] - public ?ClientOutput $client = null; + public ?array $clients = []; } \ No newline at end of file diff --git a/src/Dto/Input/UserInput.php b/src/Dto/Input/UserInput.php index 5ecc8bb..aea6947 100644 --- a/src/Dto/Input/UserInput.php +++ b/src/Dto/Input/UserInput.php @@ -29,6 +29,9 @@ final class UserInput #[Groups('user:write')] public ?string $password = null; + #[Groups('user:write')] + public ?string $groupsView = null; + #[Assert\NotNull] #[Groups('user:write')] public ?bool $enabled = true; @@ -63,6 +66,7 @@ final class UserInput $this->username = $user->getUsername(); $this->enabled= $user->isEnabled(); + $this->groupsView = $user->getGroupsView(); if ($user->getUserGroups()) { foreach ($user->getUserGroups() as $userGroup) { @@ -85,6 +89,7 @@ final class UserInput $user->setUsername($this->username); $user->setEnabled($this->enabled); + $user->setGroupsView($this->groupsView); foreach ($this->userGroups as $userGroup) { $userGroupsToAdd[] = $userGroup->getEntity(); diff --git a/src/Dto/Output/NetworkSettingsOutput.php b/src/Dto/Output/NetworkSettingsOutput.php index 2224045..00c089a 100644 --- a/src/Dto/Output/NetworkSettingsOutput.php +++ b/src/Dto/Output/NetworkSettingsOutput.php @@ -30,6 +30,9 @@ final class NetworkSettingsOutput extends AbstractOutput #[Groups(['network-settings:read', "organizational-unit:read", "client:read"])] 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"])] public ?string $p2pMode = null; @@ -80,6 +83,7 @@ final class NetworkSettingsOutput extends AbstractOutput $this->netmask = $networkSettings->getNetmask(); $this->router = $networkSettings->getRouter(); $this->ntp = $networkSettings->getNtp(); + $this->netiface = $networkSettings->getNetiface(); $this->p2pMode = $networkSettings->getP2pMode(); $this->p2pTime = $networkSettings->getP2pTime(); $this->mcastIp = $networkSettings->getMcastIp(); diff --git a/src/Dto/Output/OrganizationalUnitOutput.php b/src/Dto/Output/OrganizationalUnitOutput.php index b77decf..7adb715 100644 --- a/src/Dto/Output/OrganizationalUnitOutput.php +++ b/src/Dto/Output/OrganizationalUnitOutput.php @@ -38,7 +38,7 @@ final class OrganizationalUnitOutput extends AbstractOutput #[Groups(['organizational-unit:read'])] 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; #[Groups(['organizational-unit:read', "client:read"])] diff --git a/src/Dto/Output/UserOutput.php b/src/Dto/Output/UserOutput.php index 7960699..cdab043 100644 --- a/src/Dto/Output/UserOutput.php +++ b/src/Dto/Output/UserOutput.php @@ -23,6 +23,8 @@ final class UserOutput extends AbstractOutput public array $allowedOrganizationalUnits; #[Groups(['user:read'])] public array $userGroups; + #[Groups(['user:read'])] + public string $groupsView; #[Groups(['user:read'])] public \DateTime $createdAt; @@ -37,6 +39,7 @@ final class UserOutput extends AbstractOutput $this->username = $user->getUsername(); $this->roles = $user->getRoles(); $this->enabled = $user->isEnabled(); + $this->groupsView = $user->getGroupsView(); $this->userGroups = $user->getUserGroups()->map( fn(UserGroup $userGroup) => new UserGroupOutput($userGroup) diff --git a/src/Entity/NetworkSettings.php b/src/Entity/NetworkSettings.php index a62e8ed..bab7b65 100644 --- a/src/Entity/NetworkSettings.php +++ b/src/Entity/NetworkSettings.php @@ -31,6 +31,9 @@ class NetworkSettings extends AbstractEntity #[ORM\Column(length: 255, nullable: true)] private ?string $ntp = null; + #[ORM\Column(length: 255, nullable: true)] + private ?string $netiface = null; + #[ORM\Column(nullable: true)] private ?int $p2pTime = null; @@ -161,6 +164,18 @@ class NetworkSettings extends AbstractEntity return $this; } + public function getNetiface(): ?string + { + return $this->netiface; + } + + public function setNetiface(?string $netiface): static + { + $this->netiface = $netiface; + + return $this; + } + public function getP2pTime(): ?int { return $this->p2pTime; diff --git a/src/Entity/User.php b/src/Entity/User.php index 4b821ce..984b0e9 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -51,6 +51,9 @@ class User extends AbstractEntity implements UserInterface, PasswordAuthenticate private ?string $newPassword = null; private ?string $repeatNewPassword = null; + #[ORM\Column(length: 255)] + private ?string $groupsView = null; + public function __construct() { @@ -245,4 +248,16 @@ class User extends AbstractEntity implements UserInterface, PasswordAuthenticate return $this; } + + public function getGroupsView(): ?string + { + return $this->groupsView; + } + + public function setGroupsView(string $groupsView): static + { + $this->groupsView = $groupsView; + + return $this; + } } diff --git a/src/Factory/UserFactory.php b/src/Factory/UserFactory.php index 01cca77..b29a018 100644 --- a/src/Factory/UserFactory.php +++ b/src/Factory/UserFactory.php @@ -32,6 +32,7 @@ final class UserFactory extends ModelFactory return [ 'password' => $hash, 'roles' => [], + 'groupsView' => 'card', 'username' => self::faker()->text(180), ]; } diff --git a/src/Formatter/CustomLineFormatter.php b/src/Formatter/CustomLineFormatter.php index ee8b6bc..f9ab5ca 100644 --- a/src/Formatter/CustomLineFormatter.php +++ b/src/Formatter/CustomLineFormatter.php @@ -19,6 +19,7 @@ class CustomLineFormatter extends LineFormatter 'component' => 'ogcore', 'params' => $record['context'], '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; diff --git a/src/Lexik/JWTCreatedListener.php b/src/Lexik/JWTCreatedListener.php index 34ca078..ae37549 100644 --- a/src/Lexik/JWTCreatedListener.php +++ b/src/Lexik/JWTCreatedListener.php @@ -23,6 +23,7 @@ class JWTCreatedListener $payload['username'] = $user->getUsername(); $payload['uuid'] = $user->getUuid(); $payload['roles'] = $user->getRoles(); + $payload['groupsView'] = $user->getGroupsView(); $event->setData($payload); } diff --git a/src/Repository/ClientRepository.php b/src/Repository/ClientRepository.php index c94c0c2..4e96160 100644 --- a/src/Repository/ClientRepository.php +++ b/src/Repository/ClientRepository.php @@ -16,23 +16,24 @@ class ClientRepository extends AbstractRepository 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 FROM App\Entity\OrganizationalUnit o WHERE o.id = :id' )->setParameter('id', $organizationalUnitId); $result = $query->getOneOrNullResult(); - if (!$result) { return []; } $path = $result['path'] . '/%'; - $query = $this->getEntityManager()->createQuery( + $query = $entityManager->createQuery( 'SELECT o.id FROM App\Entity\OrganizationalUnit o WHERE o.id = :id OR o.path LIKE :path' @@ -42,13 +43,32 @@ class ClientRepository extends AbstractRepository $unitIds = array_column($query->getArrayResult(), 'id'); - $query = $this->getEntityManager()->createQuery( - 'SELECT c - FROM App\Entity\Client c - WHERE c.organizationalUnit IN (:unitIds)' - ) + $qb = $entityManager->createQueryBuilder(); + $qb->select('c') + ->from(Client::class, 'c') + ->where('c.organizationalUnit IN (: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"); + } + } + } else { + $qb->andWhere("c.$key = :$key")->setParameter($key, $value); + } + } + + return $qb->getQuery()->getResult(); } + + } diff --git a/src/State/Provider/ClientProvider.php b/src/State/Provider/ClientProvider.php index 364e8da..a6b0fad 100644 --- a/src/State/Provider/ClientProvider.php +++ b/src/State/Provider/ClientProvider.php @@ -39,10 +39,11 @@ readonly class ClientProvider implements ProviderInterface public function provideCollection(Operation $operation, array $uriVariables = [], array $context = []): object { - $organizationalUnitId = $context['filters']['organizationalUnit.id'] ?? null; + $filters = $context['filters'] ?? []; - if ($organizationalUnitId) { - $clients = $this->clientRepository->findClientsByOrganizationalUnitAndDescendants((int) $organizationalUnitId); + if (isset($filters['organizationalUnit.id'])) { + $organizationalUnitId = (int) $filters['organizationalUnit.id']; + $clients = $this->clientRepository->findClientsByOrganizationalUnitAndDescendants($organizationalUnitId, $filters); $items = new \ArrayObject(); foreach ($clients as $client) { @@ -50,12 +51,20 @@ readonly class ClientProvider implements ProviderInterface } return new TraversablePaginator($items, 1, count($clients), count($clients)); - } + } 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 { $item = $this->itemProvider->provide($operation, $uriVariables, $context); -- 2.40.1 From 6b26b789958b423299bd85c726b201a33dfee573 Mon Sep 17 00:00:00 2001 From: Manuel Aranda Date: Fri, 28 Feb 2025 09:12:44 +0100 Subject: [PATCH 15/27] refs #1567. New subnet field: 'dns' --- CHANGELOG.md | 7 +++-- migrations/Version20250227154452.php | 31 +++++++++++++++++++++ src/Controller/OgDhcp/Subnet/PostAction.php | 1 + src/Controller/OgDhcp/Subnet/SyncAction.php | 2 +- src/Dto/Input/SubnetInput.php | 6 ++++ src/Dto/Output/SubnetOutput.php | 4 +++ src/Entity/Subnet.php | 15 ++++++++++ 7 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 migrations/Version20250227154452.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d3566e..d8d2f70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,15 @@ # Changelog ## [0.8.2] - 2025-03-04 ### 🔹 Added -- Nueva funcionalidad para tener notificaciones en tiempo real. Instalación de bundle "Mercure". +- 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. ### ⚡ 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. --- ## [0.8.1] - 2025-02-25 @@ -23,7 +27,6 @@ - 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. -- --- diff --git a/migrations/Version20250227154452.php b/migrations/Version20250227154452.php new file mode 100644 index 0000000..2b6d346 --- /dev/null +++ b/migrations/Version20250227154452.php @@ -0,0 +1,31 @@ +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'); + } +} diff --git a/src/Controller/OgDhcp/Subnet/PostAction.php b/src/Controller/OgDhcp/Subnet/PostAction.php index 7a87fc6..9cfc41d 100644 --- a/src/Controller/OgDhcp/Subnet/PostAction.php +++ b/src/Controller/OgDhcp/Subnet/PostAction.php @@ -33,6 +33,7 @@ class PostAction extends AbstractOgDhcpController 'nextServer' => $data->getNextServer(), 'bootFileName' => $data->getBootFileName(), 'router' => $data->getRouter(), + 'DNS' => $data->getDns() ] ]; diff --git a/src/Controller/OgDhcp/Subnet/SyncAction.php b/src/Controller/OgDhcp/Subnet/SyncAction.php index 8b2c945..a409eff 100644 --- a/src/Controller/OgDhcp/Subnet/SyncAction.php +++ b/src/Controller/OgDhcp/Subnet/SyncAction.php @@ -63,7 +63,7 @@ class SyncAction extends AbstractOgDhcpController { $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->setIpAddress($getParsedData['ip']); $subnetEntity->setNetmask($getParsedData['mask']); diff --git a/src/Dto/Input/SubnetInput.php b/src/Dto/Input/SubnetInput.php index efbcf6a..494d87e 100644 --- a/src/Dto/Input/SubnetInput.php +++ b/src/Dto/Input/SubnetInput.php @@ -36,6 +36,10 @@ final class SubnetInput #[ApiProperty(description: 'The router of the subnet', example: "")] public ?string $router = null; + #[Groups(['subnet:write'])] + #[ApiProperty(description: 'The dns server of the subnet', example: "")] + public ?string $dns = null; + #[Groups(['subnet:write'])] #[ApiProperty(description: 'The boot file name of the subnet', example: "")] public ?string $bootFileName = null; @@ -58,6 +62,7 @@ final class SubnetInput $this->ipAddress = $subnet->getIpAddress(); $this->router = $subnet->getRouter(); $this->nextServer = $subnet->getNextServer(); + $this->dns = $subnet->getDns(); $this->bootFileName = $subnet->getBootFileName(); if ($subnet->getOrganizationalUnits()) { @@ -76,6 +81,7 @@ final class SubnetInput $subnet->setName($this->name); $subnet->setNetmask($this->netmask); $subnet->setIpAddress($this->ipAddress); + $subnet->setDns($this->dns); $subnet->setNextServer($this->nextServer); $subnet->setBootFileName($this->bootFileName); $subnet->setRouter($this->router); diff --git a/src/Dto/Output/SubnetOutput.php b/src/Dto/Output/SubnetOutput.php index 7e84c0b..0a9b540 100644 --- a/src/Dto/Output/SubnetOutput.php +++ b/src/Dto/Output/SubnetOutput.php @@ -30,6 +30,9 @@ final class SubnetOutput extends AbstractOutput #[Groups(['subnet:read'])] public ?string $router = null; + #[Groups(['subnet:read'])] + public ?string $dns = null; + #[Groups(['subnet:read'])] public array $clients; @@ -54,6 +57,7 @@ final class SubnetOutput extends AbstractOutput $this->ipAddress = $subnet->getIpAddress(); $this->nextServer = $subnet->getNextServer(); $this->router = $subnet->getRouter(); + $this->dns = $subnet->getDns(); $this->bootFileName = $subnet->getBootFileName(); $this->synchronized = $subnet->isSynchronized(); $this->serverId = $subnet->getServerId(); diff --git a/src/Entity/Subnet.php b/src/Entity/Subnet.php index 08ef819..0ce6a1e 100644 --- a/src/Entity/Subnet.php +++ b/src/Entity/Subnet.php @@ -43,6 +43,9 @@ class Subnet extends AbstractEntity #[ORM\Column(length: 255, nullable: true)] private ?string $router = null; + #[ORM\Column(length: 255, nullable: true)] + private ?string $dns = null; + public function __construct() { parent::__construct(); @@ -202,4 +205,16 @@ class Subnet extends AbstractEntity return $this; } + + public function getDns(): ?string + { + return $this->dns; + } + + public function setDns(?string $dns): static + { + $this->dns = $dns; + + return $this; + } } -- 2.40.1 From 8e21a103616a796bd0bb91e07498f500afdee0c2 Mon Sep 17 00:00:00 2001 From: Manuel Aranda Date: Fri, 28 Feb 2025 10:58:54 +0100 Subject: [PATCH 16/27] Updated groups paginator --- src/Repository/ClientRepository.php | 83 ++++++++++++++++++++++++++- src/State/Provider/ClientProvider.php | 5 +- 2 files changed, 84 insertions(+), 4 deletions(-) diff --git a/src/Repository/ClientRepository.php b/src/Repository/ClientRepository.php index 4e96160..72f8b17 100644 --- a/src/Repository/ClientRepository.php +++ b/src/Repository/ClientRepository.php @@ -20,6 +20,7 @@ class ClientRepository extends AbstractRepository { $entityManager = $this->getEntityManager(); + // Obtener el path de la unidad organizativa raíz $query = $entityManager->createQuery( 'SELECT o.path FROM App\Entity\OrganizationalUnit o @@ -33,6 +34,7 @@ class ClientRepository extends AbstractRepository $path = $result['path'] . '/%'; + // Obtener las unidades organizativas descendientes $query = $entityManager->createQuery( 'SELECT o.id FROM App\Entity\OrganizationalUnit o @@ -43,15 +45,89 @@ class ClientRepository extends AbstractRepository $unitIds = array_column($query->getArrayResult(), 'id'); + // Construcción de la consulta con filtros dinámicos $qb = $entityManager->createQueryBuilder(); $qb->select('c') - ->from(Client::class, 'c') + ->from('App\Entity\Client', 'c') ->where('c.organizationalUnit IN (:unitIds)') ->setParameter('unitIds', $unitIds); + // Aplicar dinámicamente los filtros recibidos foreach ($filters as $key => $value) { if ($key === 'order' || $key === 'page' || $key === 'itemsPerPage' || $key === 'organizationalUnit.id') { - continue; + continue; // No son filtros de búsqueda + } + + 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"); + } + } + } else { + // Si es un campo normal, añadirlo como filtro + $qb->andWhere("c.$key = :$key")->setParameter($key, $value); + } + } + + // Aplicar ordenación si existe + 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); + } + } + } + + // Aplicar paginación + 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'] . '/%'; + + // Obtener las unidades organizativas descendientes + $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'); + + // Contar clientes sin paginación + $qb = $entityManager->createQueryBuilder(); + $qb->select('COUNT(c.id)') + ->from('App\Entity\Client', 'c') + ->where('c.organizationalUnit IN (:unitIds)') + ->setParameter('unitIds', $unitIds); + + // Aplicar los mismos filtros que en `findClientsByOrganizationalUnitAndDescendants` + foreach ($filters as $key => $value) { + if ($key === 'order' || $key === 'page' || $key === 'itemsPerPage' || $key === 'organizationalUnit.id') { + continue; // No son filtros de búsqueda } if ($key === 'exists') { @@ -67,8 +143,9 @@ class ClientRepository extends AbstractRepository } } - return $qb->getQuery()->getResult(); + return (int) $qb->getQuery()->getSingleScalarResult(); } + } diff --git a/src/State/Provider/ClientProvider.php b/src/State/Provider/ClientProvider.php index a6b0fad..9e22d42 100644 --- a/src/State/Provider/ClientProvider.php +++ b/src/State/Provider/ClientProvider.php @@ -43,6 +43,8 @@ readonly class ClientProvider implements ProviderInterface if (isset($filters['organizationalUnit.id'])) { $organizationalUnitId = (int) $filters['organizationalUnit.id']; + + $totalClients = $this->clientRepository->countClientsByOrganizationalUnitAndDescendants($organizationalUnitId, $filters); $clients = $this->clientRepository->findClientsByOrganizationalUnitAndDescendants($organizationalUnitId, $filters); $items = new \ArrayObject(); @@ -50,7 +52,7 @@ readonly class ClientProvider implements ProviderInterface $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); @@ -65,6 +67,7 @@ readonly class ClientProvider implements ProviderInterface + public function provideItem(Operation $operation, array $uriVariables = [], array $context = []): object|array|null { $item = $this->itemProvider->provide($operation, $uriVariables, $context); -- 2.40.1 From 0cf373303a4642aaa9d2c92e8d83aa299c924c0f Mon Sep 17 00:00:00 2001 From: Manuel Aranda Date: Tue, 4 Mar 2025 12:53:05 +0100 Subject: [PATCH 17/27] Updated migrations --- migrations/Version20250304115209.php | 31 ++++++++++++++++++++++++++++ src/Dto/Output/UserOutput.php | 2 +- src/Entity/User.php | 4 ++-- 3 files changed, 34 insertions(+), 3 deletions(-) create mode 100644 migrations/Version20250304115209.php diff --git a/migrations/Version20250304115209.php b/migrations/Version20250304115209.php new file mode 100644 index 0000000..32f12dc --- /dev/null +++ b/migrations/Version20250304115209.php @@ -0,0 +1,31 @@ +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'); + } +} diff --git a/src/Dto/Output/UserOutput.php b/src/Dto/Output/UserOutput.php index cdab043..afa038f 100644 --- a/src/Dto/Output/UserOutput.php +++ b/src/Dto/Output/UserOutput.php @@ -24,7 +24,7 @@ final class UserOutput extends AbstractOutput #[Groups(['user:read'])] public array $userGroups; #[Groups(['user:read'])] - public string $groupsView; + public ?string $groupsView = 'card'; #[Groups(['user:read'])] public \DateTime $createdAt; diff --git a/src/Entity/User.php b/src/Entity/User.php index 984b0e9..809ea75 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -51,7 +51,7 @@ class User extends AbstractEntity implements UserInterface, PasswordAuthenticate private ?string $newPassword = null; private ?string $repeatNewPassword = null; - #[ORM\Column(length: 255)] + #[ORM\Column(length: 255, nullable: true)] private ?string $groupsView = null; @@ -254,7 +254,7 @@ class User extends AbstractEntity implements UserInterface, PasswordAuthenticate return $this->groupsView; } - public function setGroupsView(string $groupsView): static + public function setGroupsView(?string $groupsView): static { $this->groupsView = $groupsView; -- 2.40.1 From 70bf6fc5340174f2a245e55b7d54ac102a8ba575 Mon Sep 17 00:00:00 2001 From: Manuel Aranda Date: Tue, 4 Mar 2025 14:42:33 +0100 Subject: [PATCH 18/27] updated .env.prod --- .env.prod | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.env.prod b/.env.prod index d660f74..c70aa34 100644 --- a/.env.prod +++ b/.env.prod @@ -17,4 +17,10 @@ CORS_ALLOW_ORIGIN='*' 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 ### \ No newline at end of file +###< 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 ### -- 2.40.1 From a5d58fa78df1db9f7fef8dea8cfbe98fcdfda64d Mon Sep 17 00:00:00 2001 From: Manuel Aranda Date: Wed, 5 Mar 2025 09:05:49 +0100 Subject: [PATCH 19/27] Changed repoClient funcionality. New filters added --- src/Repository/ClientRepository.php | 37 +++++++++++++++++++---------- 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/src/Repository/ClientRepository.php b/src/Repository/ClientRepository.php index 72f8b17..c6f77f2 100644 --- a/src/Repository/ClientRepository.php +++ b/src/Repository/ClientRepository.php @@ -20,7 +20,6 @@ class ClientRepository extends AbstractRepository { $entityManager = $this->getEntityManager(); - // Obtener el path de la unidad organizativa raíz $query = $entityManager->createQuery( 'SELECT o.path FROM App\Entity\OrganizationalUnit o @@ -34,7 +33,6 @@ class ClientRepository extends AbstractRepository $path = $result['path'] . '/%'; - // Obtener las unidades organizativas descendientes $query = $entityManager->createQuery( 'SELECT o.id FROM App\Entity\OrganizationalUnit o @@ -45,17 +43,15 @@ class ClientRepository extends AbstractRepository $unitIds = array_column($query->getArrayResult(), 'id'); - // Construcción de la consulta con filtros dinámicos $qb = $entityManager->createQueryBuilder(); $qb->select('c') ->from('App\Entity\Client', 'c') ->where('c.organizationalUnit IN (:unitIds)') ->setParameter('unitIds', $unitIds); - // Aplicar dinámicamente los filtros recibidos foreach ($filters as $key => $value) { if ($key === 'order' || $key === 'page' || $key === 'itemsPerPage' || $key === 'organizationalUnit.id') { - continue; // No son filtros de búsqueda + continue; } if ($key === 'exists') { @@ -66,13 +62,25 @@ class ClientRepository extends AbstractRepository $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 { - // Si es un campo normal, añadirlo como filtro + // Búsqueda exacta para otros campos $qb->andWhere("c.$key = :$key")->setParameter($key, $value); } } - // Aplicar ordenación si existe + + if (isset($filters['order']) && is_array($filters['order'])) { foreach ($filters['order'] as $field => $direction) { if (in_array(strtolower($direction), ['asc', 'desc'])) { @@ -81,7 +89,6 @@ class ClientRepository extends AbstractRepository } } - // Aplicar paginación if (isset($filters['page']) && isset($filters['itemsPerPage'])) { $page = max(1, (int) $filters['page']); $itemsPerPage = max(1, (int) $filters['itemsPerPage']); @@ -108,7 +115,6 @@ class ClientRepository extends AbstractRepository $path = $result['path'] . '/%'; - // Obtener las unidades organizativas descendientes $query = $entityManager->createQuery( 'SELECT o.id FROM App\Entity\OrganizationalUnit o WHERE o.id = :id OR o.path LIKE :path' ) @@ -117,17 +123,15 @@ class ClientRepository extends AbstractRepository $unitIds = array_column($query->getArrayResult(), 'id'); - // Contar clientes sin paginación $qb = $entityManager->createQueryBuilder(); $qb->select('COUNT(c.id)') ->from('App\Entity\Client', 'c') ->where('c.organizationalUnit IN (:unitIds)') ->setParameter('unitIds', $unitIds); - // Aplicar los mismos filtros que en `findClientsByOrganizationalUnitAndDescendants` foreach ($filters as $key => $value) { if ($key === 'order' || $key === 'page' || $key === 'itemsPerPage' || $key === 'organizationalUnit.id') { - continue; // No son filtros de búsqueda + continue; } if ($key === 'exists') { @@ -138,6 +142,15 @@ class ClientRepository extends AbstractRepository $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); } -- 2.40.1 From 0ecc69e36299761193cf83d9b977974426460cd0 Mon Sep 17 00:00:00 2001 From: Manuel Aranda Date: Wed, 5 Mar 2025 09:15:19 +0100 Subject: [PATCH 20/27] Changed test --- tests/Functional/UserTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Functional/UserTest.php b/tests/Functional/UserTest.php index 6618ba0..8586c45 100644 --- a/tests/Functional/UserTest.php +++ b/tests/Functional/UserTest.php @@ -56,6 +56,7 @@ class UserTest extends AbstractTest 'username' => self::USER_CREATE, 'password' => '12345678', 'enabled' => true, + 'groupsView' => 'card' ]]); $this->assertResponseStatusCodeSame(201); -- 2.40.1 From a0e8dc22977911bc207471aaac65cd75b5339278 Mon Sep 17 00:00:00 2001 From: Manuel Aranda Date: Wed, 5 Mar 2025 09:22:59 +0100 Subject: [PATCH 21/27] Fixed phpxml --- phpunit.xml.dist | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 7b99837..6ebbd48 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,8 +1,7 @@ - - tests + tests @@ -30,10 +29,6 @@ - - - - -- 2.40.1 From 1e76956b9631e0d13953fabb92faadcb97a35759 Mon Sep 17 00:00:00 2001 From: Manuel Aranda Date: Wed, 5 Mar 2025 09:26:28 +0100 Subject: [PATCH 22/27] Fixed phpxml --- phpunit.xml.dist | 1 - 1 file changed, 1 deletion(-) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 6ebbd48..bd0dd75 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -14,7 +14,6 @@ - -- 2.40.1 From 73b5914baaf724538ce0122445f9ee17f4229320 Mon Sep 17 00:00:00 2001 From: Manuel Aranda Date: Wed, 5 Mar 2025 10:34:02 +0100 Subject: [PATCH 23/27] Updated test --- config/services_test.yaml | 3 +++ phpunit.xml.dist | 10 ++++++++-- tests/Functional/ClientTest.php | 6 ++++-- 3 files changed, 15 insertions(+), 4 deletions(-) create mode 100644 config/services_test.yaml diff --git a/config/services_test.yaml b/config/services_test.yaml new file mode 100644 index 0000000..183a1d8 --- /dev/null +++ b/config/services_test.yaml @@ -0,0 +1,3 @@ +services: + App\EventListener\ClientStatusNotifier: + tags: ~ # Esto elimina el listener en el entorno de test \ No newline at end of file diff --git a/phpunit.xml.dist b/phpunit.xml.dist index bd0dd75..7b99837 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,7 +1,8 @@ + + - tests + tests @@ -28,6 +30,10 @@ + + + + diff --git a/tests/Functional/ClientTest.php b/tests/Functional/ClientTest.php index 82d8055..c09b047 100644 --- a/tests/Functional/ClientTest.php +++ b/tests/Functional/ClientTest.php @@ -42,7 +42,9 @@ class ClientTest extends AbstractTest { 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->assertResponseStatusCodeSame(Response::HTTP_OK); @@ -51,7 +53,7 @@ class ClientTest extends AbstractTest '@context' => '/contexts/Client', '@id' => '/clients', '@type' => 'hydra:Collection', - 'hydra:totalItems' => 10, + 'hydra:totalItems' => 1, ]); } -- 2.40.1 From 629da7ca1a19c1ea8c3dc3f60f58234e3a282a88 Mon Sep 17 00:00:00 2001 From: Manuel Aranda Date: Wed, 5 Mar 2025 10:41:16 +0100 Subject: [PATCH 24/27] Updated test phunit.xml --- phpunit.xml.dist | 1 - 1 file changed, 1 deletion(-) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 7b99837..f6da34e 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -15,7 +15,6 @@ - -- 2.40.1 From e06f681464dd977e7e16d0d35d85a25eca61a881 Mon Sep 17 00:00:00 2001 From: Manuel Aranda Date: Wed, 5 Mar 2025 17:20:37 +0100 Subject: [PATCH 25/27] refs #1644. Cancel deployImage action --- config/api_platform/ImageImageRepository.yaml | 9 +++++++++ config/api_platform/Trace.yaml | 9 +++++++++ config/services/api_platform.yaml | 2 +- .../OgRepository/Image/CancelTransmissionAction.php | 8 ++++++++ src/Controller/OgRepository/Image/GetStatusAction.php | 8 ++++++++ src/Model/TraceStatus.php | 2 ++ 6 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 src/Controller/OgRepository/Image/CancelTransmissionAction.php create mode 100644 src/Controller/OgRepository/Image/GetStatusAction.php diff --git a/config/api_platform/ImageImageRepository.yaml b/config/api_platform/ImageImageRepository.yaml index 3e13b96..05fd121 100644 --- a/config/api_platform/ImageImageRepository.yaml +++ b/config/api_platform/ImageImageRepository.yaml @@ -92,6 +92,15 @@ resources: uriTemplate: /image-image-repositories/{uuid}/transfer-image 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: App\Entity\ImageImageRepository: id: diff --git a/config/api_platform/Trace.yaml b/config/api_platform/Trace.yaml index 115c1f0..65a6a32 100644 --- a/config/api_platform/Trace.yaml +++ b/config/api_platform/Trace.yaml @@ -12,6 +12,15 @@ resources: ApiPlatform\Metadata\Get: 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: createdAt: DESC diff --git a/config/services/api_platform.yaml b/config/services/api_platform.yaml index 379733d..8ec0610 100644 --- a/config/services/api_platform.yaml +++ b/config/services/api_platform.yaml @@ -20,7 +20,7 @@ services: api_platform.filter.client.search: parent: 'api_platform.doctrine.orm.search_filter' - arguments: [ { 'id': 'exact', 'name': 'partial', 'serialNumber': 'exact', 'template.id': 'exact', status: 'exact', organizationalUnit.id: 'exact', mac: 'exact', ip: 'exact', subnet.id: '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' ] api_platform.filter.client.exist: diff --git a/src/Controller/OgRepository/Image/CancelTransmissionAction.php b/src/Controller/OgRepository/Image/CancelTransmissionAction.php new file mode 100644 index 0000000..29e950d --- /dev/null +++ b/src/Controller/OgRepository/Image/CancelTransmissionAction.php @@ -0,0 +1,8 @@ + 'Pendiente', self::IN_PROGRESS => 'En progreso', self::SUCCESS => 'Finalizado con éxito', self::FAILED => 'Fallido', + self::CANCELLED => 'Cancelado', ]; public static function getStatus(): array -- 2.40.1 From 2f8b4bb32083207d4bd95fc6c07a5112cacd1c7d Mon Sep 17 00:00:00 2001 From: Manuel Aranda Date: Wed, 5 Mar 2025 17:21:57 +0100 Subject: [PATCH 26/27] Some improvements. Fixed multi select actions --- phpunit.xml.dist | 1 + src/Controller/DeployImageAction.php | 14 ++++- src/Controller/OgAgent/DeployImageAction.php | 17 ++++- src/Controller/OgAgent/PowerOffAction.php | 13 ++-- .../AbstractOgRepositoryController.php | 2 + .../Image/CancelTransmissionAction.php | 63 ++++++++++++++++++- .../OgRepository/Image/GetStatusAction.php | 37 ++++++++++- src/Controller/OgRepository/WoLAction.php | 7 ++- 8 files changed, 139 insertions(+), 15 deletions(-) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index f6da34e..7b99837 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -15,6 +15,7 @@ + diff --git a/src/Controller/DeployImageAction.php b/src/Controller/DeployImageAction.php index a586835..ae59bc3 100644 --- a/src/Controller/DeployImageAction.php +++ b/src/Controller/DeployImageAction.php @@ -60,6 +60,10 @@ class DeployImageAction extends AbstractController ]; $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); } break; @@ -85,11 +89,14 @@ class DeployImageAction extends AbstractController try { $this->deployImageOgRepositoryAction->__invoke($input, $image, $client->getEntity(), $this->httpClient); } catch (\Exception $e) { - //return new JsonResponse(data: ['error' => $e->getMessage()], status: Response::HTTP_INTERNAL_SERVER_ERROR); continue; } $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); } break; @@ -109,11 +116,14 @@ class DeployImageAction extends AbstractController try { $this->deployImageOgRepositoryAction->__invoke($input, $image, $client->getEntity(), $this->httpClient); } catch (\Exception $e) { - //return new JsonResponse(data: ['error' => $e->getMessage()], status: Response::HTTP_INTERNAL_SERVER_ERROR); continue; } $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); } diff --git a/src/Controller/OgAgent/DeployImageAction.php b/src/Controller/OgAgent/DeployImageAction.php index 1c5b1c9..eaacd9b 100644 --- a/src/Controller/OgAgent/DeployImageAction.php +++ b/src/Controller/OgAgent/DeployImageAction.php @@ -18,6 +18,7 @@ use App\Service\Trace\CreateService; use Doctrine\ORM\EntityManagerInterface; use Psr\Log\LoggerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\HttpClient\Exception\TransportException; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Response; 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) { $image = $imageImageRepository->getImage(); @@ -95,11 +102,15 @@ class DeployImageAction extends AbstractController ]); $this->logger->info('Deploying image', ['image' => $image->getId()]); - } catch (TransportExceptionInterface $e) { - $this->logger->error('Error deploying image', ['image' => $image->getId(), 'error' => $e->getMessage()]); + $jobId = json_decode($response->getContent(), true)['job_id']; + } 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); $this->entityManager->persist($client); diff --git a/src/Controller/OgAgent/PowerOffAction.php b/src/Controller/OgAgent/PowerOffAction.php index 4817ca5..39f2e5b 100644 --- a/src/Controller/OgAgent/PowerOffAction.php +++ b/src/Controller/OgAgent/PowerOffAction.php @@ -17,6 +17,7 @@ use App\Service\Trace\CreateService; use Doctrine\ORM\EntityManagerInterface; use Psr\Log\LoggerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\HttpClient\Exception\TransportException; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; @@ -67,14 +68,16 @@ class PowerOffAction extends AbstractController 'json' => $data, ]); $this->logger->info('Powering off client', ['client' => $client->getId()]); + $jobId = json_decode($response->getContent(), true)['job_id']; - } catch (TransportExceptionInterface $e) { - $this->logger->error('Error powering off client', ['client' => $client->getId(), 'error' => $e->getMessage()]); + } catch (ClientExceptionInterface | ServerExceptionInterface | TransportExceptionInterface | TransportException $e) { + $this->logger->error('Error power off client', [ + 'image' => $client->getIp(), + 'error' => $e->getMessage() + ]); continue; } - $jobId = json_decode($response->getContent(), true)['job_id']; - $client->setStatus(ClientStatus::TURNING_OFF); $this->entityManager->persist($client); $this->entityManager->flush(); @@ -82,6 +85,6 @@ class PowerOffAction extends AbstractController $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); } } diff --git a/src/Controller/OgRepository/AbstractOgRepositoryController.php b/src/Controller/OgRepository/AbstractOgRepositoryController.php index effcb5c..eb61f33 100644 --- a/src/Controller/OgRepository/AbstractOgRepositoryController.php +++ b/src/Controller/OgRepository/AbstractOgRepositoryController.php @@ -55,6 +55,7 @@ abstract class AbstractOgRepositoryController extends AbstractController $this->logger->error(sprintf('Client/Server error in request to %s: %s', $url, $e->getMessage())); return [ + 'code' => Response::HTTP_INTERNAL_SERVER_ERROR, 'error' => 'Client/Server error', '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())); return [ + 'code' => Response::HTTP_INTERNAL_SERVER_ERROR, 'error' => 'Transport error', 'details' => $e->getMessage(), ]; diff --git a/src/Controller/OgRepository/Image/CancelTransmissionAction.php b/src/Controller/OgRepository/Image/CancelTransmissionAction.php index 29e950d..eec94f2 100644 --- a/src/Controller/OgRepository/Image/CancelTransmissionAction.php +++ b/src/Controller/OgRepository/Image/CancelTransmissionAction.php @@ -2,7 +2,66 @@ namespace App\Controller\OgRepository\Image; -class CancelTransmissionAction -{ +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); + } } \ No newline at end of file diff --git a/src/Controller/OgRepository/Image/GetStatusAction.php b/src/Controller/OgRepository/Image/GetStatusAction.php index 329c53e..37d5085 100644 --- a/src/Controller/OgRepository/Image/GetStatusAction.php +++ b/src/Controller/OgRepository/Image/GetStatusAction.php @@ -2,7 +2,40 @@ namespace App\Controller\OgRepository\Image; -class GetStatusAction -{ +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); + } } \ No newline at end of file diff --git a/src/Controller/OgRepository/WoLAction.php b/src/Controller/OgRepository/WoLAction.php index e486651..814d7b9 100644 --- a/src/Controller/OgRepository/WoLAction.php +++ b/src/Controller/OgRepository/WoLAction.php @@ -58,11 +58,16 @@ class WoLAction extends AbstractOgRepositoryController $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); $this->entityManager->persist($client); $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); -- 2.40.1 From b0bf22a2eb0987b64682ae55f802b35d7e7ed687 Mon Sep 17 00:00:00 2001 From: Manuel Aranda Date: Wed, 5 Mar 2025 17:25:48 +0100 Subject: [PATCH 27/27] Modified changelog --- CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d8d2f70..f1a4fda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,15 +1,19 @@ # Changelog -## [0.8.2] - 2025-03-04 +## [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 -- 2.40.1