refs #614. Integration DHCP
testing/ogcore-api/pipeline/head This commit looks good Details

develop-jenkins
Manuel Aranda Rosales 2024-10-14 13:18:19 +02:00
parent 8c57e61a57
commit 4759f3e24f
24 changed files with 330 additions and 37 deletions

View File

@ -13,6 +13,7 @@ resources:
filters:
- 'api_platform.filter.client.order'
- 'api_platform.filter.client.search'
- 'api_platform.filter.client.exist'
ApiPlatform\Metadata\Get:
provider: App\State\Provider\ClientProvider

View File

@ -24,7 +24,7 @@ resources:
ApiPlatform\Metadata\Post: ~
ApiPlatform\Metadata\Delete: ~
sync:
oglives_sync:
class: ApiPlatform\Metadata\Post
method: POST
input: false

View File

@ -23,6 +23,13 @@ resources:
ApiPlatform\Metadata\Post: ~
ApiPlatform\Metadata\Delete: ~
subnet_sync:
class: ApiPlatform\Metadata\Post
method: POST
input: false
uriTemplate: /subnets/sync
controller: App\Controller\OgDhcp\Subnet\SyncAction
get_collection_subnets:
shortName: Subnet Server
description: Get collection of Subnet
@ -101,7 +108,8 @@ resources:
class: ApiPlatform\Metadata\Delete
method: DELETE
input: false
uriTemplate: /og-dhcp/server/{uuid}/delete-host
read: false
uriTemplate: /og-dhcp/server/{uuid}/delete-host/{clientUuid}
controller: App\Controller\OgDhcp\Subnet\DeleteHostAction

View File

@ -23,6 +23,11 @@ services:
arguments: [ { 'id': 'exact', 'name': 'partial', 'serialNumber': 'exact', organizationalUnit.id: 'exact', mac: 'exact', ip: 'exact' } ]
tags: [ 'api_platform.filter' ]
api_platform.filter.client.exist:
parent: 'api_platform.doctrine.orm.exists_filter'
arguments: [{'subnet': ~ }]
tags: [ 'api_platform.filter' ]
api_platform.filter.command.order:
parent: 'api_platform.doctrine.orm.order_filter'
arguments:
@ -144,6 +149,18 @@ services:
arguments: [ { 'id': 'exact', 'name': 'partial', } ]
tags: [ 'api_platform.filter' ]
api_platform.filter.subnet.order:
parent: 'api_platform.doctrine.orm.order_filter'
arguments:
$properties: { 'id': ~, 'name': ~ }
$orderParameterName: 'order'
tags: [ 'api_platform.filter' ]
api_platform.filter.subnet.search:
parent: 'api_platform.doctrine.orm.search_filter'
arguments: [ { 'id': 'exact', 'name': 'partial', ip: 'exact', nextServer: 'exact', netmask: 'exact', bootFileName: 'partial'} ]
tags: [ 'api_platform.filter' ]
api_platform.filter.trace.search:
parent: 'api_platform.doctrine.orm.search_filter'
arguments: [ { 'id': 'exact', 'command.id': 'exact', 'client.id': 'exact' } ]

View File

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20241014053130 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE subnet ADD server_id INT DEFAULT NULL, ADD synchronized TINYINT(1) DEFAULT NULL');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE subnet DROP server_id, DROP synchronized');
}
}

View File

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20241014082029 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE client DROP FOREIGN KEY FK_C7440455C9CF9478');
$this->addSql('ALTER TABLE client ADD CONSTRAINT FK_C7440455C9CF9478 FOREIGN KEY (subnet_id) REFERENCES subnet (id) ON DELETE CASCADE');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE client DROP FOREIGN KEY FK_C7440455C9CF9478');
$this->addSql('ALTER TABLE client ADD CONSTRAINT FK_C7440455C9CF9478 FOREIGN KEY (subnet_id) REFERENCES subnet (id)');
}
}

View File

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20241014102105 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE client DROP FOREIGN KEY FK_C7440455C9CF9478');
$this->addSql('ALTER TABLE client ADD CONSTRAINT FK_C7440455C9CF9478 FOREIGN KEY (subnet_id) REFERENCES subnet (id) ON DELETE SET NULL');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE client DROP FOREIGN KEY FK_C7440455C9CF9478');
$this->addSql('ALTER TABLE client ADD CONSTRAINT FK_C7440455C9CF9478 FOREIGN KEY (subnet_id) REFERENCES subnet (id) ON DELETE CASCADE');
}
}

View File

@ -4,7 +4,6 @@ declare(strict_types=1);
namespace App\Controller\OgBoot;
use App\Service\OgBoot\ConfigService;
use App\Service\OgBoot\StatusService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
@ -23,7 +22,6 @@ class OgBootController extends AbstractController
{
public function __construct(
private readonly StatusService $ogbootStatusService,
private readonly ConfigService $ogbootConfigService,
)
{
}
@ -41,19 +39,4 @@ class OgBootController extends AbstractController
return new JsonResponse( data: $data, status: Response::HTTP_OK);
}
/**
* @throws TransportExceptionInterface
* @throws ServerExceptionInterface
* @throws RedirectionExceptionInterface
* @throws ClientExceptionInterface
*/
#[Route('/config', name: 'ogboot_config', methods: ['GET'])]
public function config(): JsonResponse
{
$data = $this->ogbootConfigService->__invoke();
return new JsonResponse( data: $data, status: Response::HTTP_OK);
}
}

View File

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Controller\OgDhcp;
use App\Service\Utils\GetIpAddressAndNetmaskFromCIDRService;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
@ -21,7 +22,8 @@ abstract class AbstractOgDhcpController extends AbstractController
{
public function __construct(
protected readonly string $ogDhcpApiUrl,
protected readonly EntityManagerInterface $entityManager
protected readonly EntityManagerInterface $entityManager,
protected readonly GetIpAddressAndNetmaskFromCIDRService $getIpAddressAndNetmaskFromCIDRService
)
{
}

View File

@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace App\Controller\OgDhcp;
use App\Service\OgDhcp\StatusService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\AsController;
use Symfony\Component\Routing\Attribute\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;
#[Route('/og-dhcp')]
#[AsController]
class OgDhcpController extends AbstractController
{
public function __construct(
private readonly StatusService $ogdhcpStatusService,
)
{
}
/**
* @throws TransportExceptionInterface
* @throws ServerExceptionInterface
* @throws RedirectionExceptionInterface
* @throws ClientExceptionInterface
*/
#[Route('/status', name: 'ogdhcp_status', methods: ['GET'])]
public function status(): JsonResponse
{
$data = $this->ogdhcpStatusService->__invoke();
return new JsonResponse( data: $data, status: Response::HTTP_OK);
}
}

View File

@ -29,7 +29,7 @@ class DeleteAction extends AbstractOgDhcpController
throw new ValidatorException('Data Id is required');
}
$content = $this->createRequest($httpClient, 'DELETE', $this->ogDhcpApiUrl.'/ogdhcp/v1/subnets/'.$data->getId());
$content = $this->createRequest($httpClient, 'DELETE', $this->ogDhcpApiUrl.'/ogdhcp/v1/subnets/'.$data->getServerId());
$this->entityManager->remove($data);
$this->entityManager->flush();

View File

@ -3,6 +3,7 @@
namespace App\Controller\OgDhcp\Subnet;
use App\Controller\OgDhcp\AbstractOgDhcpController;
use App\Entity\Client;
use App\Entity\Subnet;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
@ -23,20 +24,27 @@ class DeleteHostAction extends AbstractOgDhcpController
* @throws RedirectionExceptionInterface
* @throws ClientExceptionInterface
*/
public function __invoke(Subnet $data, HttpClientInterface $httpClient): JsonResponse
public function __invoke(Subnet $data, string $clientUuid, HttpClientInterface $httpClient): JsonResponse
{
$client = $this->entityManager->getRepository(Client::class)->findOneBy(['uuid' => $clientUuid]);
if (!$client || $client->getSubnet() !== $data) {
throw new ValidatorException('Client not found');
}
if (!$data->getId()) {
throw new ValidatorException('Data URL is required');
}
$params = [
'json' => [
'host' => '',
'macAddress' => strtolower($client->getMac()),
]
];
$content = $this->createRequest($httpClient, 'POST', $this->ogDhcpApiUrl.'/ogdhcp/v1/subnets/'.$data->getId().'/hosts', $params);
$content = $this->createRequest($httpClient, 'DELETE', $this->ogDhcpApiUrl.'/ogdhcp/v1/subnets/'.$data->getServerId().'/hosts', $params);
$data->removeClient($client);
$this->entityManager->persist($data);
$this->entityManager->flush();

View File

@ -29,7 +29,7 @@ class GetAction extends AbstractOgDhcpController
throw new ValidatorException('Checksum is required');
}
$content = $this->createRequest($httpClient, 'GET', $this->ogDhcpApiUrl.'/ogdhcp/v1/subnets/'.$data->getId());
$content = $this->createRequest($httpClient, 'GET', $this->ogDhcpApiUrl.'/ogdhcp/v1/subnets/'.$data->getServerId());
return new JsonResponse(data: $content, status: Response::HTTP_OK);
}

View File

@ -29,7 +29,7 @@ class GetHostsAction extends AbstractOgDhcpController
throw new ValidatorException('Checksum is required');
}
$content = $this->createRequest($httpClient, 'GET', $this->ogDhcpApiUrl.'/ogdhcp/v1/subnets/'.$data->getId().'/hosts');
$content = $this->createRequest($httpClient, 'GET', $this->ogDhcpApiUrl.'/ogdhcp/v1/subnets/'.$data->getServerId().'/hosts');
return new JsonResponse(data: $content, status: Response::HTTP_OK);
}

View File

@ -37,6 +37,8 @@ class PostAction extends AbstractOgDhcpController
$content = $this->createRequest($httpClient, 'POST', $this->ogDhcpApiUrl.'/ogdhcp/v1/subnets' , $params);
$data->setServerId($content['message']['id']);
$data->setSynchronized(true);
$this->entityManager->persist($data);
$this->entityManager->flush();

View File

@ -44,7 +44,7 @@ class PostHostAction extends AbstractOgDhcpController
'json' => $data
];
$content = $this->createRequest($httpClient, 'POST', $this->ogDhcpApiUrl.'/ogdhcp/v1/subnets/'.$subnet->getId().'/hosts', $params);
$content = $this->createRequest($httpClient, 'POST', $this->ogDhcpApiUrl.'/ogdhcp/v1/subnets/'.$subnet->getServerId().'/hosts', $params);
$subnet->addClient($clientEntity);
$this->entityManager->persist($subnet);

View File

@ -38,7 +38,7 @@ class PutAction extends AbstractOgDhcpController
]
];
$content = $this->createRequest($httpClient, 'PUT', $this->ogDhcpApiUrl.'/ogdhcp/v1/subnets/'.$data->getId(), $params);
$content = $this->createRequest($httpClient, 'PUT', $this->ogDhcpApiUrl.'/ogdhcp/v1/subnets/'.$data->getServerId(), $params);
$this->entityManager->persist($data);
$this->entityManager->flush();

View File

@ -0,0 +1,90 @@
<?php
namespace App\Controller\OgDhcp\Subnet;
use App\Controller\OgDhcp\AbstractOgDhcpController;
use App\Entity\Client;
use App\Entity\Subnet;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\AsController;
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 SyncAction extends AbstractOgDhcpController
{
/**
* @throws TransportExceptionInterface
* @throws ServerExceptionInterface
* @throws RedirectionExceptionInterface
* @throws ClientExceptionInterface
*/
public function __invoke(HttpClientInterface $httpClient, EntityManagerInterface $entityManager): JsonResponse
{
$content = $this->createRequest($httpClient, 'GET', $this->ogDhcpApiUrl . '/ogdhcp/v1/subnets');
$arraySync = [];
foreach ($content['message'] as $subnet) {
$subnetEntity = $this->entityManager->getRepository(Subnet::class)->findOneBy(['serverId' => $subnet['id']]);
if ($subnetEntity) {
$this->extracted($subnetEntity, $subnet);
$this->entityManager->persist($subnetEntity);
} else {
$subnetEntity = new Subnet();
$this->extracted($subnetEntity, $subnet);
}
$arraySync[] = $subnet['id'];
$subnetEntity->setServerId($subnet['id']);
$subnetEntity->setSynchronized(true);
$this->entityManager->persist($subnetEntity);
}
$this->setSynchronized($arraySync);
$this->entityManager->flush();
return new JsonResponse(data: $content, status: Response::HTTP_OK);
}
/**
* @param Subnet|null $subnetEntity
* @param mixed $subnet
* @return void
*/
private function extracted(Subnet|null $subnetEntity, mixed $subnet): void
{
$getParsedData = $this->getIpAddressAndNetmaskFromCIDRService->__invoke($subnet['subnet']);
$subnetEntity->setName($subnet['boot-file-name']);
$subnetEntity->setBootFileName($subnet['boot-file-name']);
$subnetEntity->setIpAddress($getParsedData['ip']);
$subnetEntity->setNetmask($getParsedData['mask']);
$subnetEntity->setNextServer($subnet['next-server']);
foreach ($subnet['reservations'] as $host) {
$hostEntity = $this->entityManager->getRepository(Client::class)->findOneBy(['mac' => $host['hw-address']]);
if ($hostEntity){
$subnetEntity->addClient($hostEntity);
}
}
}
public function setSynchronized(array $array): void
{
$this->entityManager->createQueryBuilder()
->update(Subnet::class, 's')
->set('s.synchronized', 'false')
->where('s.serverId NOT IN (:array)')
->setParameter('array', array_values($array))
->getQuery()
->getResult();
}
}

View File

@ -52,6 +52,9 @@ final class ClientOutput extends AbstractOutput
#[ApiProperty(readableLink: true )]
public ?OgLiveOutput $ogLive = null;
#[Groups(['client:read'])]
public ?string $subnet = null;
#[Groups(['client:read'])]
public ?array $position = ['x' => 0, 'y' => 0];
@ -89,6 +92,7 @@ final class ClientOutput extends AbstractOutput
$this->menu = $client->getMenu() ? new MenuOutput($client->getMenu()) : null;
$this->position = $client->getPosition();
$this->hardwareProfile = $client->getHardwareProfile() ? new HardwareProfileOutput($client->getHardwareProfile()) : null;
$this->subnet = $client->getSubnet() ? $client->getSubnet()->getIpAddress(). '-' . $client->getSubnet()->getNetmask() : null;
$this->ogLive = $client->getOgLive() ? new OgLiveOutput($client->getOgLive()) : null;
$this->status = $client->getStatus();
$this->createdAt = $client->getCreatedAt();

View File

@ -30,6 +30,12 @@ final class SubnetOutput extends AbstractOutput
#[Groups(['subnet:read'])]
public array $clients;
#[Groups(['subnet:read'])]
public ?bool $synchronized = false;
#[Groups(['subnet:read'])]
public ?int $serverId;
#[Groups(['subnet:read'])]
public \DateTime $createdAt;
@ -45,7 +51,8 @@ final class SubnetOutput extends AbstractOutput
$this->ipAddress = $subnet->getIpAddress();
$this->nextServer = $subnet->getNextServer();
$this->bootFileName = $subnet->getBootFileName();
$this->synchronized = $subnet->isSynchronized();
$this->serverId = $subnet->getServerId();
$this->clients = $subnet->getClients()->map(
fn(Client $client) => new ClientOutput($client)

View File

@ -64,6 +64,7 @@ class Client extends AbstractEntity
private ?OgRepository $repository = null;
#[ORM\ManyToOne(inversedBy: 'clients')]
#[ORM\JoinColumn( onDelete: 'SET NULL')]
private ?Subnet $subnet = null;
#[ORM\ManyToOne(inversedBy: 'clients')]
private ?OgLive $ogLive = null;

View File

@ -11,6 +11,7 @@ use Doctrine\ORM\Mapping as ORM;
class Subnet extends AbstractEntity
{
use NameableTrait;
use SynchronizedTrait;
#[ORM\Column(length: 255)]
private ?string $netmask = null;
@ -36,6 +37,9 @@ class Subnet extends AbstractEntity
#[ORM\OneToMany(mappedBy: 'subnet', targetEntity: Client::class)]
private Collection $clients;
#[ORM\Column(nullable: true)]
private ?int $serverId = null;
public function __construct()
{
parent::__construct();
@ -164,7 +168,6 @@ class Subnet extends AbstractEntity
public function removeClient(Client $client): static
{
if ($this->clients->removeElement($client)) {
// set the owning side to null (unless already changed)
if ($client->getSubnet() === $this) {
$client->setSubnet(null);
}
@ -172,4 +175,16 @@ class Subnet extends AbstractEntity
return $this;
}
public function getServerId(): ?int
{
return $this->serverId;
}
public function setServerId(?int $serverId): static
{
$this->serverId = $serverId;
return $this;
}
}

View File

@ -1,6 +1,6 @@
<?php
namespace App\Service\OgBoot;
namespace App\Service\OgDhcp;
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\HttpFoundation\JsonResponse;
@ -9,12 +9,11 @@ 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;
readonly class ConfigService
class StatusService
{
public function __construct(
private string $ogBootApiUrl
private string $ogDhcpApiUrl
)
{
}
@ -28,12 +27,12 @@ readonly class ConfigService
public function __invoke()
{
$httpClient = HttpClient::create([
'verify_peer' => false, // Ignorar la verificación del certificado SSL
'verify_host' => false, // Ignorar la verificación del nombre del host
'verify_peer' => false,
'verify_host' => false,
]);
try {
$response = $httpClient->request('GET', $this->ogBootApiUrl.'/ogboot/v1/config', [
$response = $httpClient->request('GET', $this->ogDhcpApiUrl.'/ogdhcp/v1/status', [
'headers' => [
'accept' => 'application/json',
],

View File

@ -0,0 +1,18 @@
<?php
namespace App\Service\Utils;
class GetIpAddressAndNetmaskFromCIDRService
{
public function __invoke(string $cidr): array
{
list($ip, $prefix) = explode('/', $cidr);
$mask = long2ip((~(2 ** (32 - $prefix) - 1)) & 0xFFFFFFFF);
return [
'ip' => $ip,
'mask' => $mask
];
}
}