From 54eab1342b61153ef7427c81ee577ff98c71fd5f Mon Sep 17 00:00:00 2001 From: Nicolas Arenas Date: Fri, 17 Apr 2026 19:53:08 +0200 Subject: [PATCH 1/2] 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; -- 2.40.1 From ba461ead15a5c51ea01fa8db9449619d7672c381 Mon Sep 17 00:00:00 2001 From: Nicolas Arenas Date: Fri, 17 Apr 2026 20:58:32 +0200 Subject: [PATCH 2/2] Adds leases hoook to kea config --- debian/ogdhcp.postinst | 40 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/debian/ogdhcp.postinst b/debian/ogdhcp.postinst index 498604e..864b4d3 100644 --- a/debian/ogdhcp.postinst +++ b/debian/ogdhcp.postinst @@ -24,6 +24,28 @@ restore_config_if_modified() { } +# Añade el hook lease-cmds a kea-dhcp4.conf solo si no está ya presente +ensure_lease_cmds_hook() { + local config="$1" + local lib="$2" + + [ -f "$config" ] || return 0 + [ -n "$lib" ] || return 0 + + if grep -q "libdhcp_lease_cmds" "$config"; then + echo "El hook lease-cmds ya está presente en $config, no se añade de nuevo." + return 0 + fi + + echo "Añadiendo hook lease-cmds a $config..." + local tmp + tmp=$(mktemp) + jq --arg lib "$lib" \ + '.Dhcp4["hooks-libraries"] = (.Dhcp4["hooks-libraries"] // []) + [{"library": $lib}]' \ + "$config" > "$tmp" && mv "$tmp" "$config" + chown _kea:_kea "$config" +} + KEA_CTRL_AGENT_CONF="/etc/kea/kea-ctrl-agent.conf" PUBLIC_DIR=/opt/opengnsys/ogdhcp/api/public db_get opengnsys/ogdhcp_interfaces @@ -95,13 +117,23 @@ EOF echo "Configurando kea-dhcp4..." IFS=',' read -r -a INTERFACES <<< "$OGDHCP_INTERFACES" KEA_CONFIG="/etc/kea/kea-dhcp4.conf" + + # Detectar la ruta de libdhcp_lease_cmds.so (varía según arquitectura) + LEASE_CMDS_LIB=$(find /usr/lib -name "libdhcp_lease_cmds.so" 2>/dev/null | head -1) + if [ -z "$LEASE_CMDS_LIB" ]; then + echo "Advertencia: no se encontró libdhcp_lease_cmds.so. El hook lease-cmds no estará activo." + else + echo "Librería lease-cmds encontrada en: $LEASE_CMDS_LIB" + fi + if [ -e "$KEA_CONFIG" ]; then dpkg-divert --package ogdhcp --divert "$KEA_CONFIG.dpkg-dist" --rename "$KEA_CONFIG" + # El fichero se genera desde cero, no hay riesgo de duplicación cat > "$KEA_CONFIG" </dev/null | head -1) + ensure_lease_cmds_hook "/etc/kea/kea-dhcp4.conf" "$LEASE_CMDS_LIB" fi echo ">>> Configuración de ogdhcp completada." ;; -- 2.40.1