From 54eab1342b61153ef7427c81ee577ff98c71fd5f Mon Sep 17 00:00:00 2001 From: Nicolas Arenas Date: Fri, 17 Apr 2026 19:53:08 +0200 Subject: [PATCH] Add endpoints to remove leases from subnet and host --- .../DhcpBundle/Controller/DhcpController.php | 255 ++++++++++++++++++ api/src/DhcpBundle/Service/CurlKeaService.php | 39 +++ 2 files changed, 294 insertions(+) diff --git a/api/src/DhcpBundle/Controller/DhcpController.php b/api/src/DhcpBundle/Controller/DhcpController.php index 088eb9f..99c6329 100644 --- a/api/src/DhcpBundle/Controller/DhcpController.php +++ b/api/src/DhcpBundle/Controller/DhcpController.php @@ -2209,6 +2209,261 @@ public function restoreDhcpConfiguration(): JsonResponse } +/** + * @OA\Delete( + * path="/ogdhcp/v1/subnets/{subnetId}/leases", + * summary="Elimina todos los leases de una subred", + * @OA\Parameter( + * name="subnetId", + * in="path", + * description="ID de la subred cuyos leases se van a eliminar", + * required=true, + * @OA\Schema(type="integer") + * ), + * @OA\Response( + * response=200, + * description="Leases eliminados correctamente", + * @OA\JsonContent( + * type="object", + * @OA\Property(property="success", type="string", example="Leases de la subred eliminados correctamente"), + * @OA\Property(property="message", type="string", example="Leases deleted.") + * ) + * ), + * @OA\Response( + * response=404, + * description="No se encontraron leases en la subred", + * @OA\JsonContent( + * type="object", + * @OA\Property(property="error", type="string", example="No se encontraron leases en la subred con id '44'") + * ) + * ), + * @OA\Response( + * response=500, + * description="Error al comunicarse con el agente de control de Kea", + * @OA\JsonContent( + * type="object", + * @OA\Property(property="error", type="string", example="Error al ejecutar lease4-wipe") + * ) + * ) + * ) + * @Route("/ogdhcp/v1/subnets/{subnetId}/leases", methods={"DELETE"}) + */ +public function wipeSubnetLeases(Request $request): JsonResponse +{ + $operation = 'wipeSubnetLeases'; + $component = 'ogdhcp'; + $subnetId = (int) $request->get('subnetId'); + + try { + $this->logger->info(json_encode([ + 'severity' => 'INFO', + 'operation' => $operation, + 'component' => $component, + 'params' => ['subnet_id' => $subnetId], + 'desc' => 'Iniciando eliminación de leases de la subred.' + ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); + + $response = $this->curlKeaService->executeLeaseCommand('lease4-wipe', ['subnet-id' => $subnetId]); + + if (!$response) { + $errorText = "No se obtuvo respuesta del agente de control de Kea al ejecutar lease4-wipe."; + $this->logger->error(json_encode([ + 'severity' => 'ERROR', + 'operation' => $operation, + 'component' => $component, + 'http_code' => Response::HTTP_INTERNAL_SERVER_ERROR, + 'desc' => $errorText, + 'params' => ['subnet_id' => $subnetId] + ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); + return new JsonResponse(['error' => $errorText], Response::HTTP_INTERNAL_SERVER_ERROR); + } + + $result = $response[0]['result']; + $text = $response[0]['text'] ?? ''; + + // result 3 = empty: el comando se ejecutó pero no había leases + if ($result === 3) { + $errorText = "No se encontraron leases en la subred con id '$subnetId'"; + $this->logger->info(json_encode([ + 'severity' => 'INFO', + 'operation' => $operation, + 'component' => $component, + 'http_code' => Response::HTTP_NOT_FOUND, + 'desc' => $errorText, + 'params' => ['subnet_id' => $subnetId] + ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); + return new JsonResponse(['error' => $errorText], Response::HTTP_NOT_FOUND); + } + + if ($result !== 0) { + $errorText = "Error al eliminar los leases de la subred '$subnetId': $text"; + $this->logger->error(json_encode([ + 'severity' => 'ERROR', + 'operation' => $operation, + 'component' => $component, + 'http_code' => Response::HTTP_INTERNAL_SERVER_ERROR, + 'desc' => $errorText, + 'params' => ['subnet_id' => $subnetId, 'kea_result' => $result] + ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); + return new JsonResponse(['error' => $errorText], Response::HTTP_INTERNAL_SERVER_ERROR); + } + + $this->logger->info(json_encode([ + 'severity' => 'INFO', + 'operation' => $operation, + 'component' => $component, + 'http_code' => Response::HTTP_OK, + 'desc' => 'Leases de la subred eliminados correctamente.', + 'params' => ['subnet_id' => $subnetId] + ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); + + return new JsonResponse([ + 'success' => "Leases de la subred eliminados correctamente", + 'message' => $text + ], Response::HTTP_OK); + + } catch (Exception $e) { + $errorText = "Error al ejecutar lease4-wipe: " . $e->getMessage(); + $this->logger->error(json_encode([ + 'severity' => 'ERROR', + 'operation' => $operation, + 'component' => $component, + 'http_code' => Response::HTTP_INTERNAL_SERVER_ERROR, + 'desc' => $errorText, + 'params' => ['subnet_id' => $subnetId] + ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); + return new JsonResponse(['error' => $errorText], Response::HTTP_INTERNAL_SERVER_ERROR); + } +} + + +/** + * @OA\Delete( + * path="/ogdhcp/v1/leases/{ipAddress}", + * summary="Elimina el lease de una máquina por su dirección IP", + * @OA\Parameter( + * name="ipAddress", + * in="path", + * description="Dirección IP del lease a eliminar (ej: 192.168.1.11)", + * required=true, + * @OA\Schema(type="string") + * ), + * @OA\Response( + * response=200, + * description="Lease eliminado correctamente", + * @OA\JsonContent( + * type="object", + * @OA\Property(property="success", type="string", example="Lease eliminado correctamente"), + * @OA\Property(property="message", type="string", example="IPv4 lease deleted.") + * ) + * ), + * @OA\Response( + * response=404, + * description="No se encontró un lease para la IP indicada", + * @OA\JsonContent( + * type="object", + * @OA\Property(property="error", type="string", example="No se encontró un lease para la IP '192.168.1.11'") + * ) + * ), + * @OA\Response( + * response=500, + * description="Error al comunicarse con el agente de control de Kea", + * @OA\JsonContent( + * type="object", + * @OA\Property(property="error", type="string", example="Error al ejecutar lease4-del") + * ) + * ) + * ) + * @Route("/ogdhcp/v1/leases/{ipAddress}", methods={"DELETE"}, requirements={"ipAddress"=".+"}) + */ +public function deleteHostLease(Request $request): JsonResponse +{ + $operation = 'deleteHostLease'; + $component = 'ogdhcp'; + $ipAddress = $request->get('ipAddress'); + + try { + $this->logger->info(json_encode([ + 'severity' => 'INFO', + 'operation' => $operation, + 'component' => $component, + 'params' => ['ip_address' => $ipAddress], + 'desc' => 'Iniciando eliminación del lease del host.' + ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); + + $response = $this->curlKeaService->executeLeaseCommand('lease4-del', ['ip-address' => $ipAddress]); + + if (!$response) { + $errorText = "No se obtuvo respuesta del agente de control de Kea al ejecutar lease4-del."; + $this->logger->error(json_encode([ + 'severity' => 'ERROR', + 'operation' => $operation, + 'component' => $component, + 'http_code' => Response::HTTP_INTERNAL_SERVER_ERROR, + 'desc' => $errorText, + 'params' => ['ip_address' => $ipAddress] + ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); + return new JsonResponse(['error' => $errorText], Response::HTTP_INTERNAL_SERVER_ERROR); + } + + $result = $response[0]['result']; + $text = $response[0]['text'] ?? ''; + + // result 3 = empty: no se encontró un lease para esa IP + if ($result === 3) { + $errorText = "No se encontró un lease para la IP '$ipAddress'"; + $this->logger->info(json_encode([ + 'severity' => 'INFO', + 'operation' => $operation, + 'component' => $component, + 'http_code' => Response::HTTP_NOT_FOUND, + 'desc' => $errorText, + 'params' => ['ip_address' => $ipAddress] + ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); + return new JsonResponse(['error' => $errorText], Response::HTTP_NOT_FOUND); + } + + if ($result !== 0) { + $errorText = "Error al eliminar el lease de la IP '$ipAddress': $text"; + $this->logger->error(json_encode([ + 'severity' => 'ERROR', + 'operation' => $operation, + 'component' => $component, + 'http_code' => Response::HTTP_INTERNAL_SERVER_ERROR, + 'desc' => $errorText, + 'params' => ['ip_address' => $ipAddress, 'kea_result' => $result] + ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); + return new JsonResponse(['error' => $errorText], Response::HTTP_INTERNAL_SERVER_ERROR); + } + + $this->logger->info(json_encode([ + 'severity' => 'INFO', + 'operation' => $operation, + 'component' => $component, + 'http_code' => Response::HTTP_OK, + 'desc' => 'Lease eliminado correctamente.', + 'params' => ['ip_address' => $ipAddress] + ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); + + return new JsonResponse([ + 'success' => "Lease eliminado correctamente", + 'message' => $text + ], Response::HTTP_OK); + + } catch (Exception $e) { + $errorText = "Error al ejecutar lease4-del: " . $e->getMessage(); + $this->logger->error(json_encode([ + 'severity' => 'ERROR', + 'operation' => $operation, + 'component' => $component, + 'http_code' => Response::HTTP_INTERNAL_SERVER_ERROR, + 'desc' => $errorText, + 'params' => ['ip_address' => $ipAddress] + ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); + return new JsonResponse(['error' => $errorText], Response::HTTP_INTERNAL_SERVER_ERROR); + } +} + } diff --git a/api/src/DhcpBundle/Service/CurlKeaService.php b/api/src/DhcpBundle/Service/CurlKeaService.php index a083547..89e9d65 100644 --- a/api/src/DhcpBundle/Service/CurlKeaService.php +++ b/api/src/DhcpBundle/Service/CurlKeaService.php @@ -61,6 +61,45 @@ class CurlKeaService } } + public function executeLeaseCommand($command, $arguments) + { + $apiUrl = getenv('DHCP_IP'); + if (!$apiUrl) { + $apiUrl = 'http://localhost:8000/'; + } + + $requestData = [ + 'command' => $command, + 'service' => ['dhcp4'], + 'arguments' => $arguments, + ]; + + $jsonData = json_encode($requestData); + try { + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $apiUrl); + curl_setopt($ch, CURLOPT_POST, 1); + curl_setopt($ch, CURLOPT_POSTFIELDS, $jsonData); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json', 'Expect:']); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_TIMEOUT, 10); + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $output = json_decode($response, true); + + if ($httpCode >= 200 && $httpCode < 300) { + return $output; + } else { + $error = curl_error($ch); + throw new Exception($error); + } + } catch (Exception $e) { + throw new Exception($e->getMessage()); + } + } + public function convertMaskToCIDR($mask) { $bits = 0;