From 8ca3995d39cd12b11fcf6daaf427e09b0a76d730 Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Thu, 2 Jul 2026 10:56:52 +0100 Subject: [PATCH 01/19] disable dnsmasq statefulset --- components/ironic/kustomization.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/ironic/kustomization.yaml b/components/ironic/kustomization.yaml index c413942f7..4998ef477 100644 --- a/components/ironic/kustomization.yaml +++ b/components/ironic/kustomization.yaml @@ -6,7 +6,7 @@ resources: - ironic-mariadb-db.yaml - ironic-rabbitmq-queue.yaml - dnsmasq-pvc.yaml - - dnsmasq-ss.yaml +# - dnsmasq-ss.yaml - ironic-ks-user-baremetal.yaml # less than ideal addition but necessary so that we can have the ironic.conf.d loading # working due to the way the chart hardcodes the config-file parameter which then From 7f16e964f505de7b6dd8fb44f8bda291cf1ff01e Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Thu, 2 Jul 2026 11:00:00 +0100 Subject: [PATCH 02/19] feat(kea): add kea DHCP server as new site-level component Wraps the kea-dhcp chart (github.com/mglants/charts) as an ArgoCD Application, following the karma/dex external-chart pattern with chrony-style site-only scoping. Starts with upstream chart defaults. --- charts/argocd-understack/README.md | 1 + .../templates/application-kea.yaml | 46 +++++++++++++++++++ charts/argocd-understack/values.yaml | 9 ++++ components/kea/values.yaml | 1 + docs/deploy-guide/components/kea.md | 39 ++++++++++++++++ properdocs.yml | 1 + 6 files changed, 97 insertions(+) create mode 100644 charts/argocd-understack/templates/application-kea.yaml create mode 100644 components/kea/values.yaml create mode 100644 docs/deploy-guide/components/kea.md diff --git a/charts/argocd-understack/README.md b/charts/argocd-understack/README.md index 9a176f77f..624b0c234 100644 --- a/charts/argocd-understack/README.md +++ b/charts/argocd-understack/README.md @@ -177,6 +177,7 @@ Components deployed on site clusters: | argo-workflows | `site.argo_workflows` | Workflow engine | | chrony | `site.chrony` | NTP service | | envoy-configs | `site.envoy_configs` | Gateway configs | +| kea | `site.kea` | DHCP server | | openstack-exporter | `site.openstack_exporter` | Metrics exporter | | openstack-memcached | `site.openstack_memcached` | Caching | | site-workflows | `site.site_workflows` | Site workflows | diff --git a/charts/argocd-understack/templates/application-kea.yaml b/charts/argocd-understack/templates/application-kea.yaml new file mode 100644 index 000000000..3a3bb7219 --- /dev/null +++ b/charts/argocd-understack/templates/application-kea.yaml @@ -0,0 +1,46 @@ +{{- if eq (include "understack.isEnabled" (list $.Values.site "kea")) "true" }} +--- +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: {{ printf "%s-%s" $.Release.Name "kea" }} + finalizers: + - resources-finalizer.argocd.argoproj.io + annotations: + argocd.argoproj.io/compare-options: ServerSideDiff=true,IncludeMutationWebhook=true +{{- include "understack.appLabelsBlock" $ | nindent 2 }} +spec: + destination: + namespace: kea + server: {{ $.Values.cluster_server }} + project: understack + sources: + - chart: kea-dhcp + helm: + ignoreMissingValueFiles: true + releaseName: kea + valueFiles: + - $understack/components/kea/values.yaml + - $deploy/{{ include "understack.deploy_path" $ }}/kea/values.yaml + repoURL: https://mglants.github.io/charts + targetRevision: {{ $.Values.site.kea.chartVersion }} + - ref: understack + repoURL: {{ include "understack.understack_url" $ }} + targetRevision: {{ include "understack.understack_ref" $ }} + - path: {{ include "understack.deploy_path" $ }}/kea + ref: deploy + repoURL: {{ include "understack.deploy_url" $ }} + targetRevision: {{ include "understack.deploy_ref" $ }} + syncPolicy: + automated: + prune: true + selfHeal: true + managedNamespaceMetadata: + annotations: + argocd.argoproj.io/sync-options: Delete=false + syncOptions: + - CreateNamespace=true + - ServerSideApply=true + - RespectIgnoreDifferences=true + - ApplyOutOfSyncOnly=true +{{- end }} diff --git a/charts/argocd-understack/values.yaml b/charts/argocd-understack/values.yaml index abf3a677f..a801d691a 100644 --- a/charts/argocd-understack/values.yaml +++ b/charts/argocd-understack/values.yaml @@ -475,6 +475,15 @@ site: # @default -- false enabled: false + # -- Kea DHCP server + kea: + # -- Enable/disable deploying Kea DHCP + # @default -- false + enabled: false + # -- Chart version for Kea DHCP + # renovate: datasource=helm depName=kea-dhcp registryUrl=https://mglants.github.io/charts + chartVersion: "0.7.1" + # -- External DNS operator external_dns: # -- Enable/disable deploying External DNS diff --git a/components/kea/values.yaml b/components/kea/values.yaml new file mode 100644 index 000000000..d69091039 --- /dev/null +++ b/components/kea/values.yaml @@ -0,0 +1 @@ +# Default values for kea-dhcp. Using upstream chart defaults for now. diff --git a/docs/deploy-guide/components/kea.md b/docs/deploy-guide/components/kea.md new file mode 100644 index 000000000..2ffcf119b --- /dev/null +++ b/docs/deploy-guide/components/kea.md @@ -0,0 +1,39 @@ +--- +charts: +- kea-dhcp +deploy_overrides: + helm: + mode: values + kustomize: + mode: second_source +--- + +# kea + +Kea DHCP server (ISC Kea) for site network DHCP service. + +## Deployment Scope + +- Cluster scope: site +- Values key: `site.kea` +- ArgoCD Application template: `charts/argocd-understack/templates/application-kea.yaml` + +## How to Enable + +Set this component to enabled in your deployment values file: + +```yaml title="$CLUSTER_NAME/deploy.yaml" +site: + kea: + enabled: true +``` + +## How ArgoCD Builds It + +{{ component_argocd_builds() }} + +## Deployment Repo Content + +{{ secrets_disclaimer }} + +Currently deployed with upstream chart defaults; no required deployment-repo overrides yet. diff --git a/properdocs.yml b/properdocs.yml index 23b373a76..698b80f4f 100644 --- a/properdocs.yml +++ b/properdocs.yml @@ -188,6 +188,7 @@ nav: - deploy-guide/components/ironic.md - deploy-guide/components/ironic-hardware-exporter.md - deploy-guide/components/karma.md + - deploy-guide/components/kea.md - deploy-guide/components/keystone.md - deploy-guide/components/mariadb-operator.md - deploy-guide/components/monitoring.md From f6af03a0d58fdb8d77689a33033af79c2ba8eec8 Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Thu, 2 Jul 2026 11:03:37 +0100 Subject: [PATCH 03/19] chore(understack): allow deployment to kea namespace --- apps/appsets/project-understack.yaml | 68 ++++++++++++++-------------- 1 file changed, 35 insertions(+), 33 deletions(-) diff --git a/apps/appsets/project-understack.yaml b/apps/appsets/project-understack.yaml index 27e6cee7e..f9728f035 100644 --- a/apps/appsets/project-understack.yaml +++ b/apps/appsets/project-understack.yaml @@ -7,38 +7,40 @@ metadata: # they move between AppProject's spec: sourceRepos: - - '*' + - "*" destinations: - - namespace: 'argo' - server: '*' - - namespace: 'argocd' - server: '*' - - namespace: 'argo-events' - server: '*' - - namespace: 'understack-cdn' - server: '*' - - namespace: 'cert-manager' - server: '*' - - namespace: 'dex' - server: '*' - - namespace: 'nautobot' - server: '*' - - namespace: 'nautobotop' - server: '*' - - namespace: 'undersync' - server: '*' - - namespace: 'openstack' - server: '*' - - namespace: 'monitoring' - server: '*' - - namespace: 'otel-collector' - server: '*' - - namespace: 'kube-system' - server: '*' - - namespace: 'envoy-gateway' - server: '*' - - namespace: 'rook-ceph' - server: '*' + - namespace: "argo" + server: "*" + - namespace: "argocd" + server: "*" + - namespace: "argo-events" + server: "*" + - namespace: "understack-cdn" + server: "*" + - namespace: "cert-manager" + server: "*" + - namespace: "dex" + server: "*" + - namespace: "kea" + server: "*" + - namespace: "nautobot" + server: "*" + - namespace: "nautobotop" + server: "*" + - namespace: "undersync" + server: "*" + - namespace: "openstack" + server: "*" + - namespace: "monitoring" + server: "*" + - namespace: "otel-collector" + server: "*" + - namespace: "kube-system" + server: "*" + - namespace: "envoy-gateway" + server: "*" + - namespace: "rook-ceph" + server: "*" clusterResourceWhitelist: - - group: '*' - kind: '*' + - group: "*" + kind: "*" From d466e9f5f233dfc9f31b05f0a22b648d9af859d0 Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Thu, 2 Jul 2026 11:07:47 +0100 Subject: [PATCH 04/19] drop! temporarily deploy kea into openstack namespace The kea namespace can't be deployed to right now, so route the kea Application there as a stopgap. Revert this once kea namespace works. --- charts/argocd-understack/templates/application-kea.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/charts/argocd-understack/templates/application-kea.yaml b/charts/argocd-understack/templates/application-kea.yaml index 3a3bb7219..494bbd173 100644 --- a/charts/argocd-understack/templates/application-kea.yaml +++ b/charts/argocd-understack/templates/application-kea.yaml @@ -11,7 +11,8 @@ metadata: {{- include "understack.appLabelsBlock" $ | nindent 2 }} spec: destination: - namespace: kea + # TODO: revert to `kea` once that namespace is deployable again + namespace: openstack server: {{ $.Values.cluster_server }} project: understack sources: From d99caefbe4de5a186d1ebdd95a646c5781cf8cbb Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Thu, 2 Jul 2026 11:41:56 +0100 Subject: [PATCH 05/19] feat(kea): expose DHCP service as LoadBalancer DHCP needs to be reachable from the physical network, not just in-cluster, so default the kea-dhcp service to LoadBalancer for all sites. --- components/kea/values.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/components/kea/values.yaml b/components/kea/values.yaml index d69091039..5676398d6 100644 --- a/components/kea/values.yaml +++ b/components/kea/values.yaml @@ -1 +1,7 @@ # Default values for kea-dhcp. Using upstream chart defaults for now. + +service: + # DHCP servers need to be reachable from the physical network, not just + # in-cluster, so expose the DHCP service via a LoadBalancer. + dhcp: + type: LoadBalancer From 173f028c9feca610840ea0fff2fcc9b7dc14faeb Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Thu, 2 Jul 2026 12:02:06 +0100 Subject: [PATCH 06/19] fix(kea): revert chart-level LoadBalancer default The kea-dhcp chart (v0.7.1, and current main) renders invalid YAML when service.dhcp.annotations is non-empty. Sites needing an annotated LoadBalancer endpoint should add their own standalone Service in their deploy-repo overlay instead, so leave the chart's own Service at defaults (ClusterIP). --- components/kea/values.yaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/components/kea/values.yaml b/components/kea/values.yaml index 5676398d6..9df23fafc 100644 --- a/components/kea/values.yaml +++ b/components/kea/values.yaml @@ -1,7 +1,7 @@ # Default values for kea-dhcp. Using upstream chart defaults for now. - -service: - # DHCP servers need to be reachable from the physical network, not just - # in-cluster, so expose the DHCP service via a LoadBalancer. - dhcp: - type: LoadBalancer +# +# The chart's own DHCP Service is left at chart defaults (ClusterIP). External +# reachability is provided per-site via a hand-written LoadBalancer Service in +# each site's deploy-repo kea/ overlay, working around a chart bug where +# setting service.dhcp.annotations renders invalid YAML (missing newline in +# templates/service.yaml, still present on chart main as of 0.7.1). From 1d4cf349a0cfe96fa1b3349426f8e26f8e1919ea Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Thu, 2 Jul 2026 16:20:51 +0100 Subject: [PATCH 07/19] feat: enable kea control agent --- components/kea/values.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/components/kea/values.yaml b/components/kea/values.yaml index 9df23fafc..87badda9e 100644 --- a/components/kea/values.yaml +++ b/components/kea/values.yaml @@ -5,3 +5,9 @@ # each site's deploy-repo kea/ overlay, working around a chart bug where # setting service.dhcp.annotations renders invalid YAML (missing newline in # templates/service.yaml, still present on chart main as of 0.7.1). + +kea: + ctrlagent: + #Needed for HA, monitoring and stork + enabled: true + loglevel: "DEBUG" From b3592288235aa45129d3889c7a553247ea00775e Mon Sep 17 00:00:00 2001 From: Milan Fencik Date: Fri, 3 Jul 2026 16:59:55 +0100 Subject: [PATCH 08/19] add ironic kea dhcp backend handler --- .../ironic_understack/conf.py | 18 ++ .../ironic_understack/dhcp/kea.py | 167 ++++++++++++++++++ python/ironic-understack/pyproject.toml | 3 + 3 files changed, 188 insertions(+) create mode 100644 python/ironic-understack/ironic_understack/dhcp/kea.py diff --git a/python/ironic-understack/ironic_understack/conf.py b/python/ironic-understack/ironic_understack/conf.py index 459d4ff30..b7296b23f 100644 --- a/python/ironic-understack/ironic_understack/conf.py +++ b/python/ironic-understack/ironic_understack/conf.py @@ -26,6 +26,24 @@ def setup_conf(): "1d": "bmc", }, ), + cfg.StrOpt( + "kea_url", + default="http://kea-kea-dhcp-ctrl.openstack.svc.cluster.local:8000", + help="URL of the Kea DHCP server's HTTP API endpoint. " + "This endpoint is used for managing DHCP " + "configuration, reservations, leases and subnet " + "operations through Kea's HTTP API interface.", + ), + cfg.IntOpt( + "kea_request_timeout", + default=10, + help="Timeout in seconds for requests to the Kea API.", + ), + cfg.IntOpt( + "kea_max_retries", + default=3, + help="Maximum number of retry attempts for failed " "requests.", + ), ] cfg.CONF.register_group(grp) cfg.CONF.register_opts(opts, group=grp) diff --git a/python/ironic-understack/ironic_understack/dhcp/kea.py b/python/ironic-understack/ironic_understack/dhcp/kea.py new file mode 100644 index 000000000..394388a01 --- /dev/null +++ b/python/ironic-understack/ironic_understack/dhcp/kea.py @@ -0,0 +1,167 @@ +import requests +from ironic import objects +from ironic.common import exception +from ironic.dhcp import base +from oslo_log import log as logging + +from ironic_understack.conf import CONF + +LOG = logging.getLogger(__name__) + + +class DHCPConfigurationError(exception.IronicException): + """Raised when there is an error in configuring DHCP.""" + + _msg_fmt = "DHCP configuration error: %(reason)s" + + +class KeaDHCPApi(base.BaseDHCP): + def __init__(self): + super().__init__() + self.max_retries = CONF.kea_max_retries + + if not CONF.kea_url: + raise DHCPConfigurationError("Kea URL must be specified in configuration") + + def _make_request(self, command, arguments, services=None): + payload = { + "command": command, + "service": services or ["dhcp4"], + "arguments": arguments, + } + + for attempt in range(self.max_retries): + try: + response = requests.post( + CONF.kea_url, json=payload, timeout=CONF.kea_request_timeout + ) + response.raise_for_status() + return response.json() + except requests.exceptions.Timeout: + LOG.warning( + "Timeout on attempt %d/%d for command %s", + attempt + 1, + self.max_retries, + command, + ) + except requests.exceptions.RequestException as e: + if attempt == self.max_retries - 1: + LOG.error("Failed to execute command %s: %s", command, e) + raise DHCPConfigurationError( + f"Failed to execute {command}: {e}" + ) from e + LOG.warning( + "Request failed on attempt %d/%d: %s", + attempt + 1, + self.max_retries, + e, + ) + + def get_config(self): + """Retrieve current Kea configuration.""" + return self._make_request("config-get", {}) + + def set_config(self, config): + """Update Kea configuration.""" + return self._make_request("config-set", {"config": config}) + + def get_statistics(self, name=None): + """Retrieve DHCP server statistics.""" + if name: + return self._make_request("statistic-get", {"name": name}) + return self._make_request("statistic-get-all", {}) + + def _update_host_reservation(self, hw_address, options=None, remove=False): + """Modify a host reservation in the Kea config file or hosts database.""" + # TODO(cid) add support/replace with the host database configuration + # option in a central database managed by Ironic; the commands to have + # Kea manage it at runtime without restarting the server is a premium + # offering + try: + config = self.get_config() + dhcp4_config = config["arguments"]["Dhcp4"] + + reservations = dhcp4_config.get("reservations", []) + found = False + for reservation in reservations: + if reservation.get("hw-address") == hw_address: + reservation["option-data"] = options + found = True + break + + if not found: + reservations.append({"hw-address": hw_address, "option-data": options}) + dhcp4_config["reservations"] = reservations + + config["arguments"]["Dhcp4"] = dhcp4_config + self.set_config(config["arguments"]) + return True + except Exception as e: + LOG.error("Failed to update reservation for %s: %s", hw_address, e) + return False + + def update_port_dhcp_opts(self, port_id, dhcp_options, context=None): + """Update DHCP options for a specific port in Kea.""" + port = objects.Port.get(context, port_id) + + kea_options = [] + for opt in dhcp_options: + kea_opt = { + "name": opt["opt_name"], + "data": opt["opt_value"], + "always-send": True, + } + if "ip_version" in opt: + kea_opt["space"] = f'dhcp{opt["ip_version"]}' + kea_options.append(kea_opt) + return self._update_host_reservation(port.address, kea_options) + + def update_dhcp_opts(self, task, options, vifs=None): + """Update DHCP options for all ports associated with a node.""" + ports = vifs or task.ports + success = True + + for port in ports: + if not self.update_port_dhcp_opts(port.uuid, options): + success = False + LOG.error("Failed to update DHCP options for port %s", port.uuid) + return success + + def clean_dhcp_opts(self, task): + """Remove DHCP options for all ports associated with a node.""" + success = True + for port in task.ports: + if not self._update_host_reservation(port.address, remove=True): + success = False + LOG.error("Failed to clean DHCP options for port %s", port.uuid) + return success + + def get_ip_addresses(self, task): + """Retrieve IP addresses for all ports associated to a node.""" + addresses = [] + for port in task.ports: + for command, service in [("lease4-get", "dhcp4"), ("lease6-get", "dhcp6")]: + try: + response = self._make_request( + command, {"hw-address": port.address}, services=[service] + ) + leases = response.get("arguments", {}).get("leases", []) + if not leases: + LOG.warning("No leases found for port %s", port.address) + if service == "dhcp4": + addresses.extend([lease["ip-address"] for lease in leases]) + else: + for lease in leases: + addresses.extend(lease.get("ip-addresses", [])) + except DHCPConfigurationError as e: + LOG.warning( + "Failed to fetch %s addresses for port %s: %s", + service, + port.address, + e, + ) + return addresses + + def supports_ipxe_tag(self): + """Indicate whether the provider supports the 'ipxe' tag.""" + return True diff --git a/python/ironic-understack/pyproject.toml b/python/ironic-understack/pyproject.toml index 52d996a25..74d0adae6 100644 --- a/python/ironic-understack/pyproject.toml +++ b/python/ironic-understack/pyproject.toml @@ -19,6 +19,9 @@ dependencies = [ [project.entry-points."ironic.console.container"] kubernetes = "ironic.console.container.kubernetes:KubernetesConsoleContainer" +[project.entry-points."ironic.dhcp"] +kea = "ironic_understack.dhcp.kea:KeaDHCPApi" + [project.entry-points."ironic.inspection.hooks"] resource-class = "ironic_understack.resource_class:ResourceClassHook" update-baremetal-port = "ironic_understack.inspect_hook_update_baremetal_ports:InspectHookUpdateBaremetalPorts" From 5de93885335581a41024e9b78726cee7c5531eee Mon Sep 17 00:00:00 2001 From: Milan Fencik Date: Fri, 3 Jul 2026 17:14:23 +0100 Subject: [PATCH 09/19] change ironic dhcp provider to kea and adjust ironic images for testing --- components/images-openstack.yaml | 10 +++++----- components/ironic/values.yaml | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/components/images-openstack.yaml b/components/images-openstack.yaml index d63aa6968..4026d7e3a 100644 --- a/components/images-openstack.yaml +++ b/components/images-openstack.yaml @@ -22,12 +22,12 @@ images: keystone_fernet_setup: "ghcr.io/rackerlabs/understack/keystone:2026.1" # ironic - ironic_api: "ghcr.io/rackerlabs/understack/ironic:2026.1" - ironic_conductor: "ghcr.io/rackerlabs/understack/ironic:2026.1" - ironic_pxe: "ghcr.io/rackerlabs/understack/ironic:2026.1" - ironic_pxe_init: "ghcr.io/rackerlabs/understack/ironic:2026.1" + ironic_api: "ghcr.io/rackerlabs/understack/ironic:pr-2111" + ironic_conductor: "ghcr.io/rackerlabs/understack/ironic:pr-2111" + ironic_pxe: "ghcr.io/rackerlabs/understack/ironic:pr-2111" + ironic_pxe_init: "ghcr.io/rackerlabs/understack/ironic:pr-2111" ironic_pxe_http: "docker.io/nginx:1.29.8" - ironic_db_sync: "ghcr.io/rackerlabs/understack/ironic:2026.1" + ironic_db_sync: "ghcr.io/rackerlabs/understack/ironic:pr-2111" # these want curl which apparently is in the openstack-client image ironic_manage_cleaning_network: "ghcr.io/rackerlabs/understack/openstack-client:2025.2" ironic_retrive_cleaning_network: "ghcr.io/rackerlabs/understack/openstack-client:2025.2" diff --git a/components/ironic/values.yaml b/components/ironic/values.yaml index a6171ce6d..2e0ff8e60 100644 --- a/components/ironic/values.yaml +++ b/components/ironic/values.yaml @@ -90,7 +90,7 @@ conf: # https://docs.openstack.org/ironic/latest/admin/drivers/idrac.html#timeout-when-powering-off post_deploy_get_power_state_retry_interval: 18 dhcp: - dhcp_provider: dnsmasq + dhcp_provider: kea oslo_messaging_notifications: driver: messagingv2 oslo_messaging_rabbit: From 64db001609c195bc1e46dea6152e83970018207a Mon Sep 17 00:00:00 2001 From: Milan Fencik Date: Fri, 3 Jul 2026 17:38:21 +0100 Subject: [PATCH 10/19] fix kea CONF refs --- python/ironic-understack/ironic_understack/dhcp/kea.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/python/ironic-understack/ironic_understack/dhcp/kea.py b/python/ironic-understack/ironic_understack/dhcp/kea.py index 394388a01..c83427e74 100644 --- a/python/ironic-understack/ironic_understack/dhcp/kea.py +++ b/python/ironic-understack/ironic_understack/dhcp/kea.py @@ -18,9 +18,9 @@ class DHCPConfigurationError(exception.IronicException): class KeaDHCPApi(base.BaseDHCP): def __init__(self): super().__init__() - self.max_retries = CONF.kea_max_retries + self.max_retries = CONF.ironic_understack.kea_max_retries - if not CONF.kea_url: + if not CONF.ironic_understack.kea_url: raise DHCPConfigurationError("Kea URL must be specified in configuration") def _make_request(self, command, arguments, services=None): @@ -33,7 +33,9 @@ def _make_request(self, command, arguments, services=None): for attempt in range(self.max_retries): try: response = requests.post( - CONF.kea_url, json=payload, timeout=CONF.kea_request_timeout + CONF.ironic_understack.kea_url, + json=payload, + timeout=CONF.ironic_understack.kea_request_timeout, ) response.raise_for_status() return response.json() From 69ce7dacf11bae41962efe27c6ad321cff9daeee Mon Sep 17 00:00:00 2001 From: Milan Fencik Date: Fri, 3 Jul 2026 18:40:20 +0100 Subject: [PATCH 11/19] adjust kea response --- python/ironic-understack/ironic_understack/dhcp/kea.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/ironic-understack/ironic_understack/dhcp/kea.py b/python/ironic-understack/ironic_understack/dhcp/kea.py index c83427e74..0e7306a64 100644 --- a/python/ironic-understack/ironic_understack/dhcp/kea.py +++ b/python/ironic-understack/ironic_understack/dhcp/kea.py @@ -61,7 +61,7 @@ def _make_request(self, command, arguments, services=None): def get_config(self): """Retrieve current Kea configuration.""" - return self._make_request("config-get", {}) + return self._make_request("config-get", {})[0] def set_config(self, config): """Update Kea configuration.""" From 545c4c21ed0b718ca3586509a027c73b00357168 Mon Sep 17 00:00:00 2001 From: Milan Fencik Date: Fri, 3 Jul 2026 19:26:34 +0100 Subject: [PATCH 12/19] print dhcp options for tshooting --- python/ironic-understack/ironic_understack/dhcp/kea.py | 1 + 1 file changed, 1 insertion(+) diff --git a/python/ironic-understack/ironic_understack/dhcp/kea.py b/python/ironic-understack/ironic_understack/dhcp/kea.py index 0e7306a64..906d674ba 100644 --- a/python/ironic-understack/ironic_understack/dhcp/kea.py +++ b/python/ironic-understack/ironic_understack/dhcp/kea.py @@ -116,6 +116,7 @@ def update_port_dhcp_opts(self, port_id, dhcp_options, context=None): if "ip_version" in opt: kea_opt["space"] = f'dhcp{opt["ip_version"]}' kea_options.append(kea_opt) + print(kea_options) return self._update_host_reservation(port.address, kea_options) def update_dhcp_opts(self, task, options, vifs=None): From 2ffafbc392786a59687de5c1a0383b2664f5a526 Mon Sep 17 00:00:00 2001 From: Milan Fencik Date: Fri, 3 Jul 2026 21:52:19 +0100 Subject: [PATCH 13/19] disable ipxe support in kea --- python/ironic-understack/ironic_understack/dhcp/kea.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/ironic-understack/ironic_understack/dhcp/kea.py b/python/ironic-understack/ironic_understack/dhcp/kea.py index 906d674ba..63391c168 100644 --- a/python/ironic-understack/ironic_understack/dhcp/kea.py +++ b/python/ironic-understack/ironic_understack/dhcp/kea.py @@ -167,4 +167,4 @@ def get_ip_addresses(self, task): def supports_ipxe_tag(self): """Indicate whether the provider supports the 'ipxe' tag.""" - return True + return False From fcc9b9a4f4262b04755f620e6aa5df728e629df9 Mon Sep 17 00:00:00 2001 From: Milan Fencik Date: Fri, 3 Jul 2026 22:51:58 +0100 Subject: [PATCH 14/19] kea fix config-set in ironic --- python/ironic-understack/ironic_understack/dhcp/kea.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/ironic-understack/ironic_understack/dhcp/kea.py b/python/ironic-understack/ironic_understack/dhcp/kea.py index 63391c168..7baa73eb3 100644 --- a/python/ironic-understack/ironic_understack/dhcp/kea.py +++ b/python/ironic-understack/ironic_understack/dhcp/kea.py @@ -65,7 +65,7 @@ def get_config(self): def set_config(self, config): """Update Kea configuration.""" - return self._make_request("config-set", {"config": config}) + return self._make_request("config-set", config) def get_statistics(self, name=None): """Retrieve DHCP server statistics.""" From c3aeb431725acc6220cf2b665ac836588f77e21b Mon Sep 17 00:00:00 2001 From: Milan Fencik Date: Fri, 3 Jul 2026 23:03:38 +0100 Subject: [PATCH 15/19] print payload for kea in ironic for thsoot --- python/ironic-understack/ironic_understack/dhcp/kea.py | 1 + 1 file changed, 1 insertion(+) diff --git a/python/ironic-understack/ironic_understack/dhcp/kea.py b/python/ironic-understack/ironic_understack/dhcp/kea.py index 7baa73eb3..06dfab8e7 100644 --- a/python/ironic-understack/ironic_understack/dhcp/kea.py +++ b/python/ironic-understack/ironic_understack/dhcp/kea.py @@ -29,6 +29,7 @@ def _make_request(self, command, arguments, services=None): "service": services or ["dhcp4"], "arguments": arguments, } + print(f"PAYLOAD: {payload}") if command == "config-set" else 0 for attempt in range(self.max_retries): try: From 24d5b16c7668c98a8c0144dc281e607ab3317bc9 Mon Sep 17 00:00:00 2001 From: Milan Fencik Date: Fri, 3 Jul 2026 23:31:56 +0100 Subject: [PATCH 16/19] remove hash from kea config-set payload --- python/ironic-understack/ironic_understack/dhcp/kea.py | 1 + 1 file changed, 1 insertion(+) diff --git a/python/ironic-understack/ironic_understack/dhcp/kea.py b/python/ironic-understack/ironic_understack/dhcp/kea.py index 06dfab8e7..72e4c5425 100644 --- a/python/ironic-understack/ironic_understack/dhcp/kea.py +++ b/python/ironic-understack/ironic_understack/dhcp/kea.py @@ -82,6 +82,7 @@ def _update_host_reservation(self, hw_address, options=None, remove=False): # offering try: config = self.get_config() + config["arguments"].pop("hash", None) dhcp4_config = config["arguments"]["Dhcp4"] reservations = dhcp4_config.get("reservations", []) From 35f20e9737110af2ebe65af0dccc4f4eb74963c4 Mon Sep 17 00:00:00 2001 From: Milan Fencik Date: Fri, 3 Jul 2026 23:55:20 +0100 Subject: [PATCH 17/19] exclude pxe options from kea in ironic --- .../ironic_understack/dhcp/kea.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/python/ironic-understack/ironic_understack/dhcp/kea.py b/python/ironic-understack/ironic_understack/dhcp/kea.py index 72e4c5425..f6958c003 100644 --- a/python/ironic-understack/ironic_understack/dhcp/kea.py +++ b/python/ironic-understack/ironic_understack/dhcp/kea.py @@ -110,14 +110,15 @@ def update_port_dhcp_opts(self, port_id, dhcp_options, context=None): kea_options = [] for opt in dhcp_options: - kea_opt = { - "name": opt["opt_name"], - "data": opt["opt_value"], - "always-send": True, - } - if "ip_version" in opt: - kea_opt["space"] = f'dhcp{opt["ip_version"]}' - kea_options.append(kea_opt) + if not opt["opt_name"].startswith("!"): + kea_opt = { + "name": opt["opt_name"], + "data": opt["opt_value"], + "always-send": True, + } + if "ip_version" in opt: + kea_opt["space"] = f'dhcp{opt["ip_version"]}' + kea_options.append(kea_opt) print(kea_options) return self._update_host_reservation(port.address, kea_options) From f62420e4bd6c2a8261ea8b07348d13ebcc57fcfa Mon Sep 17 00:00:00 2001 From: Milan Fencik Date: Sat, 4 Jul 2026 00:23:54 +0100 Subject: [PATCH 18/19] swap kea options to boot file name only in ironic --- .../ironic_understack/dhcp/kea.py | 23 +++++++------------ 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/python/ironic-understack/ironic_understack/dhcp/kea.py b/python/ironic-understack/ironic_understack/dhcp/kea.py index f6958c003..6adda7435 100644 --- a/python/ironic-understack/ironic_understack/dhcp/kea.py +++ b/python/ironic-understack/ironic_understack/dhcp/kea.py @@ -74,7 +74,7 @@ def get_statistics(self, name=None): return self._make_request("statistic-get", {"name": name}) return self._make_request("statistic-get-all", {}) - def _update_host_reservation(self, hw_address, options=None, remove=False): + def _update_host_reservation(self, hw_address, boot_file_name=None, remove=False): """Modify a host reservation in the Kea config file or hosts database.""" # TODO(cid) add support/replace with the host database configuration # option in a central database managed by Ironic; the commands to have @@ -89,12 +89,12 @@ def _update_host_reservation(self, hw_address, options=None, remove=False): found = False for reservation in reservations: if reservation.get("hw-address") == hw_address: - reservation["option-data"] = options + reservation["boot-file-name"] = boot_file_name found = True break if not found: - reservations.append({"hw-address": hw_address, "option-data": options}) + reservations.append({"hw-address": hw_address, "boot-file-name": boot_file_name}) dhcp4_config["reservations"] = reservations config["arguments"]["Dhcp4"] = dhcp4_config @@ -108,19 +108,12 @@ def update_port_dhcp_opts(self, port_id, dhcp_options, context=None): """Update DHCP options for a specific port in Kea.""" port = objects.Port.get(context, port_id) - kea_options = [] + boot_file_name = "" for opt in dhcp_options: - if not opt["opt_name"].startswith("!"): - kea_opt = { - "name": opt["opt_name"], - "data": opt["opt_value"], - "always-send": True, - } - if "ip_version" in opt: - kea_opt["space"] = f'dhcp{opt["ip_version"]}' - kea_options.append(kea_opt) - print(kea_options) - return self._update_host_reservation(port.address, kea_options) + if opt["opt_name"].startswith("!"): + boot_file_name = opt["opt_value"] + break + return self._update_host_reservation(port.address, boot_file_name) def update_dhcp_opts(self, task, options, vifs=None): """Update DHCP options for all ports associated with a node.""" From 5b88700e9dbdaf0214f633853dcb20ba3c4890a6 Mon Sep 17 00:00:00 2001 From: Milan Fencik Date: Sat, 4 Jul 2026 01:06:53 +0100 Subject: [PATCH 19/19] add next-server to kea in ironic --- .../ironic-understack/ironic_understack/dhcp/kea.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/python/ironic-understack/ironic_understack/dhcp/kea.py b/python/ironic-understack/ironic_understack/dhcp/kea.py index 6adda7435..1bfb8647b 100644 --- a/python/ironic-understack/ironic_understack/dhcp/kea.py +++ b/python/ironic-understack/ironic_understack/dhcp/kea.py @@ -3,6 +3,7 @@ from ironic.common import exception from ironic.dhcp import base from oslo_log import log as logging +from urllib.parse import urlparse from ironic_understack.conf import CONF @@ -84,17 +85,25 @@ def _update_host_reservation(self, hw_address, boot_file_name=None, remove=False config = self.get_config() config["arguments"].pop("hash", None) dhcp4_config = config["arguments"]["Dhcp4"] + next_server = urlparse(boot_file_name).hostname reservations = dhcp4_config.get("reservations", []) found = False for reservation in reservations: if reservation.get("hw-address") == hw_address: reservation["boot-file-name"] = boot_file_name + reservation["next-server"] = next_server found = True break if not found: - reservations.append({"hw-address": hw_address, "boot-file-name": boot_file_name}) + reservations.append( + { + "hw-address": hw_address, + "boot-file-name": boot_file_name, + "next-server": next_server, + } + ) dhcp4_config["reservations"] = reservations config["arguments"]["Dhcp4"] = dhcp4_config