From ff3086d00f6e6b8d8c311c495834295fdf86a944 Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Thu, 2 Jul 2026 10:56:52 +0100 Subject: [PATCH 01/34] 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 c590ade78ea57450122e916adf712396b7d42997 Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Thu, 2 Jul 2026 11:00:00 +0100 Subject: [PATCH 02/34] 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 0f88e9ccd9bc5de4f5e1e9ea15e36b87c33bd550 Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Thu, 2 Jul 2026 11:03:37 +0100 Subject: [PATCH 03/34] 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 1ad5e896476718ae62bdc4944ab7ecb79e3f8edc Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Thu, 2 Jul 2026 11:07:47 +0100 Subject: [PATCH 04/34] 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 a0c8f7bbcf9f08ee7f3936da72d2787efef010ed Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Thu, 2 Jul 2026 11:41:56 +0100 Subject: [PATCH 05/34] 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 bb9b55a1e1fc1a1493434feef594330a2287d29b Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Thu, 2 Jul 2026 12:02:06 +0100 Subject: [PATCH 06/34] 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 3f7322e7a3261451a5a6232444c63159e95677e2 Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Thu, 2 Jul 2026 16:20:51 +0100 Subject: [PATCH 07/34] 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 87b2f6cb4491e3e4b7d0ee91953775551becead9 Mon Sep 17 00:00:00 2001 From: Milan Fencik Date: Fri, 3 Jul 2026 16:59:55 +0100 Subject: [PATCH 08/34] 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 06a3cdf0961ee9678f9ba7a3da0f2e29ae5a0c98 Mon Sep 17 00:00:00 2001 From: Milan Fencik Date: Fri, 3 Jul 2026 17:14:23 +0100 Subject: [PATCH 09/34] 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 799d40280ef0c4013b3f6266c96dab38064776d1 Mon Sep 17 00:00:00 2001 From: Milan Fencik Date: Fri, 3 Jul 2026 17:38:21 +0100 Subject: [PATCH 10/34] 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 355bf8874c81f73866fd89bc6e2e1026041b92bf Mon Sep 17 00:00:00 2001 From: Milan Fencik Date: Fri, 3 Jul 2026 18:40:20 +0100 Subject: [PATCH 11/34] 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 2eac4ff3a60de7645e001cfb47b78eb1f0d2f65f Mon Sep 17 00:00:00 2001 From: Milan Fencik Date: Fri, 3 Jul 2026 19:26:34 +0100 Subject: [PATCH 12/34] 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 6e03844e2551067f6c6aefa55fbb57df7289e2e9 Mon Sep 17 00:00:00 2001 From: Milan Fencik Date: Fri, 3 Jul 2026 21:52:19 +0100 Subject: [PATCH 13/34] 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 97542ae8b25bf789a7d57e80a151828f29588d6e Mon Sep 17 00:00:00 2001 From: Milan Fencik Date: Fri, 3 Jul 2026 22:51:58 +0100 Subject: [PATCH 14/34] 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 110cd0bdf0ae5c62f0147a0cd30dbbe392ea240f Mon Sep 17 00:00:00 2001 From: Milan Fencik Date: Fri, 3 Jul 2026 23:03:38 +0100 Subject: [PATCH 15/34] 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 3e78f2a34a188f3f6fddd9a967ed6db51eb48669 Mon Sep 17 00:00:00 2001 From: Milan Fencik Date: Fri, 3 Jul 2026 23:31:56 +0100 Subject: [PATCH 16/34] 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 17d1eab48c3a746c2f13e108e1b223897bf96966 Mon Sep 17 00:00:00 2001 From: Milan Fencik Date: Fri, 3 Jul 2026 23:55:20 +0100 Subject: [PATCH 17/34] 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 871e1975f1f4be5f1e361245a7fc6067b264b87e Mon Sep 17 00:00:00 2001 From: Milan Fencik Date: Sat, 4 Jul 2026 00:23:54 +0100 Subject: [PATCH 18/34] 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 7160943defc41a3cea619c454d81fb7647dd3063 Mon Sep 17 00:00:00 2001 From: Milan Fencik Date: Sat, 4 Jul 2026 01:06:53 +0100 Subject: [PATCH 19/34] 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 From 42e10d91de00be84ebdc19cc4043be03ec2b3c58 Mon Sep 17 00:00:00 2001 From: Milan Fencik Date: Mon, 6 Jul 2026 16:19:13 +0100 Subject: [PATCH 20/34] print dhcp options for 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 1bfb8647b..073514b4f 100644 --- a/python/ironic-understack/ironic_understack/dhcp/kea.py +++ b/python/ironic-understack/ironic_understack/dhcp/kea.py @@ -30,7 +30,6 @@ 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: @@ -118,6 +117,7 @@ def update_port_dhcp_opts(self, port_id, dhcp_options, context=None): port = objects.Port.get(context, port_id) boot_file_name = "" + print(f"DHCP_OPTIONS: {dhcp_options}") for opt in dhcp_options: if opt["opt_name"].startswith("!"): boot_file_name = opt["opt_value"] From 8d649110768a02985cd35a2ae439bb099e2ebd32 Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Mon, 6 Jul 2026 16:21:20 +0100 Subject: [PATCH 21/34] feat: add kea chart --- .yamllint.yaml | 1 + charts/kea-dhcp/.helmignore | 23 ++ charts/kea-dhcp/Chart.yaml | 33 +++ charts/kea-dhcp/LICENSE | 21 ++ charts/kea-dhcp/README.md | 7 + charts/kea-dhcp/README.md.gotmpl | 161 ++++++++++ charts/kea-dhcp/README.orig.md | 156 ++++++++++ charts/kea-dhcp/templates/_helpers.tpl | 58 ++++ charts/kea-dhcp/templates/configmap.yaml | 280 ++++++++++++++++++ .../kea-dhcp/templates/metrics-service.yaml | 30 ++ charts/kea-dhcp/templates/role.yaml | 16 + charts/kea-dhcp/templates/rolebinding.yaml | 19 ++ charts/kea-dhcp/templates/service.yaml | 112 +++++++ charts/kea-dhcp/templates/serviceaccount.yaml | 10 + charts/kea-dhcp/templates/servicemonitor.yaml | 15 + charts/kea-dhcp/templates/statefulset.yaml | 201 +++++++++++++ charts/kea-dhcp/values.schema.json | 10 + charts/kea-dhcp/values.yaml | 215 ++++++++++++++ 18 files changed, 1368 insertions(+) create mode 100644 charts/kea-dhcp/.helmignore create mode 100644 charts/kea-dhcp/Chart.yaml create mode 100644 charts/kea-dhcp/LICENSE create mode 100644 charts/kea-dhcp/README.md create mode 100644 charts/kea-dhcp/README.md.gotmpl create mode 100644 charts/kea-dhcp/README.orig.md create mode 100644 charts/kea-dhcp/templates/_helpers.tpl create mode 100644 charts/kea-dhcp/templates/configmap.yaml create mode 100644 charts/kea-dhcp/templates/metrics-service.yaml create mode 100644 charts/kea-dhcp/templates/role.yaml create mode 100644 charts/kea-dhcp/templates/rolebinding.yaml create mode 100644 charts/kea-dhcp/templates/service.yaml create mode 100644 charts/kea-dhcp/templates/serviceaccount.yaml create mode 100644 charts/kea-dhcp/templates/servicemonitor.yaml create mode 100644 charts/kea-dhcp/templates/statefulset.yaml create mode 100644 charts/kea-dhcp/values.schema.json create mode 100644 charts/kea-dhcp/values.yaml diff --git a/.yamllint.yaml b/.yamllint.yaml index f38b70c63..02d08cc73 100644 --- a/.yamllint.yaml +++ b/.yamllint.yaml @@ -3,6 +3,7 @@ extends: default ignore: - components/etcdbackup/templates/ - charts/argocd-understack/templates/ + - charts/kea-dhcp/templates/ - charts/nautobot-api-tokens/templates/ - charts/nautobot-job-queues/templates/ - charts/site-workflows/templates/ diff --git a/charts/kea-dhcp/.helmignore b/charts/kea-dhcp/.helmignore new file mode 100644 index 000000000..0e8a0eb36 --- /dev/null +++ b/charts/kea-dhcp/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/charts/kea-dhcp/Chart.yaml b/charts/kea-dhcp/Chart.yaml new file mode 100644 index 000000000..68cfcda65 --- /dev/null +++ b/charts/kea-dhcp/Chart.yaml @@ -0,0 +1,33 @@ +apiVersion: v2 +name: kea-dhcp +description: Helm chart for kea-dhcp + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.8.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "2.5.8" +home: https://www.isc.org/kea/ +icon: https://raw.githubusercontent.com/isc-projects/kea/3eee114614162b087e415e0a6ce1057d422c8174/doc/images/kea-logo-100x70.png +maintainers: + - name: Mark Glants + email: m@gearage.ru +annotations: + artifacthub.io/changes: | + - kind: changed + description: allow to use custom interface diff --git a/charts/kea-dhcp/LICENSE b/charts/kea-dhcp/LICENSE new file mode 100644 index 000000000..683c9d50b --- /dev/null +++ b/charts/kea-dhcp/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Jack Maloney + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/charts/kea-dhcp/README.md b/charts/kea-dhcp/README.md new file mode 100644 index 000000000..b39c28af9 --- /dev/null +++ b/charts/kea-dhcp/README.md @@ -0,0 +1,7 @@ +# kea-dhcp + +This is a Helm chart adapted from https://github.com/Turgon37/mglants-charts/tree/main + +Credits: +- original chart https://github.com/mglants/charts +- Turgon37's fixes - https://github.com/Turgon37/mglants-charts/tree/main diff --git a/charts/kea-dhcp/README.md.gotmpl b/charts/kea-dhcp/README.md.gotmpl new file mode 100644 index 000000000..25992625f --- /dev/null +++ b/charts/kea-dhcp/README.md.gotmpl @@ -0,0 +1,161 @@ +{{- define "custom.repository.organization" -}} +mglants +{{- end -}} + +{{- define "custom.repository.url" -}} +https://github.com/MGlants/charts +{{- end -}} + +{{- define "custom.helm.url" -}} +http://charts.glants.xyz +{{- end -}} + +{{- define "custom.helm.path" -}} +{{ template "custom.repository.organization" . }}/{{ template "chart.name" . }} +{{- end -}} + +{{- define "custom.notes" -}} +**This chart is not maintained by the upstream project and any issues with the chart should be raised [here](https://github.com/MGlants/charts/issues/new/choose)** +**More than 1 replicas is not supported by now** +{{- end -}} + +{{- define "custom.requirements" -}} +## Requirements + +{{ template "chart.kubeVersionLine" . }} +{{- end -}} + +{{- define "custom.dependencies" -}} +## Dependencies + +{{ template "chart.requirementsTable" . }} +{{- end -}} + +{{- define "custom.install.tldr" -}} +## TL;DR + +```console +helm repo add {{ template "custom.repository.organization" . }} {{ template "custom.helm.url" . }} +helm repo update +helm install {{ template "chart.name" . }} {{ template "custom.helm.path" . }} +``` +{{- end -}} + +{{- define "custom.install" -}} +## Installing the Chart + +To install the chart with the release name `{{ template "chart.name" . }}` + +```console +helm install {{ template "chart.name" . }} {{ template "custom.helm.path" . }} +``` +{{- end -}} + +{{- define "custom.uninstall" -}} +## Uninstalling the Chart + +To uninstall the `{{ template "chart.name" . }}` deployment + +```console +helm uninstall {{ template "chart.name" . }} +``` + +The command removes all the Kubernetes components associated with the chart **including persistent volumes** and deletes the release. +{{- end -}} + +{{- define "custom.configuration.header" -}} +## Configuration +{{- end -}} + +{{- define "custom.configuration.readValues" -}} +Read through the [values.yaml](./values.yaml) file. It has several commented out suggested values. +{{- end -}} + +{{- define "custom.configuration.example.set" -}} +Specify each parameter using the `--set key=value[,key=value]` argument to `helm install`. + +```console +helm install {{ template "chart.name" . }} \ + --set env.TZ="America/New York" \ + {{ template "custom.helm.path" . }} +``` +{{- end -}} + +{{- define "custom.configuration.example.file" -}} +Alternatively, a YAML file that specifies the values for the above parameters can be provided while installing the chart. + +```console +helm install {{ template "chart.name" . }} {{ template "custom.helm.path" . }} -f values.yaml +``` +{{- end -}} + +{{- define "custom.valuesSection" -}} +## Values + +{{ template "chart.valuesTable" . }} +{{- end -}} + +{{- define "custom.changelog" -}} +{{ $changeTypes := list "added" "changed" "fixed" }} +## Changelog + +### Version {{ template "chart.version" . }} + +{{ range $changeType := $changeTypes }} + {{- with (index $.Annotations "artifacthub.io/changes") }} + {{- print "#### " ($changeType | title) | nindent 0 }} + {{- print "" | nindent 0 }} + {{- $changesFound := false }} + {{- range (print "changes:\n" . | fromYaml).changes }} + {{- if eq .kind $changeType }} + {{- print "* " .description | nindent 0 }} + {{- $changesFound = true }} + {{- end }} + {{- end }} + {{- if not $changesFound }} + {{- print "N/A" | nindent 0 }} + {{- end }} + {{- print "" | nindent 0 }} + {{- end }} +{{- end }} + +### Older versions + +A historical overview of changes can be found on [ArtifactHUB](https://artifacthub.io/packages/helm/mglants/{{- template "chart.name" . }}?modal=changelog) + +{{- end -}} + +{{ template "chart.header" . }} + +{{ template "chart.versionBadge" . }}{{ template "chart.typeBadge" . }}{{ template "chart.appVersionBadge" . }} + +{{ template "chart.description" . }} + +{{ template "custom.notes" . }} + +{{ template "chart.sourcesSection" . }} + +{{ template "custom.requirements" . }} + +{{ template "custom.dependencies" . }} + +{{ template "custom.install.tldr" . }} + +{{ template "custom.install" . }} + +{{ template "custom.uninstall" . }} + +{{ template "custom.configuration.header" . }} + +{{ template "custom.configuration.readValues" . }} + +{{ template "custom.configuration.example.set" . }} + +{{ template "custom.configuration.example.file" . }} + +{{ template "custom.valuesSection" . }} + +{{ template "custom.changelog" . }} + +{{ template "helm-docs.versionFooter" . }} +{{ "" }} diff --git a/charts/kea-dhcp/README.orig.md b/charts/kea-dhcp/README.orig.md new file mode 100644 index 000000000..90db2b707 --- /dev/null +++ b/charts/kea-dhcp/README.orig.md @@ -0,0 +1,156 @@ +# kea-dhcp + +![Version: 0.6.1](https://img.shields.io/badge/Version-0.6.1-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 2.1.2](https://img.shields.io/badge/AppVersion-2.1.2-informational?style=flat-square) + +Helm chart for kea-dhcp + +**This chart is not maintained by the upstream project and any issues with the chart should be raised [here](https://github.com/MGlants/charts/issues/new/choose)** +**More than 1 replicas is not supported by now** + +## Requirements + +## Dependencies + +| Repository | Name | Version | +|------------|------|---------| + +## TL;DR + +```console +helm repo add mglants http://charts.glants.xyz +helm repo update +helm install kea-dhcp mglants/kea-dhcp +``` + +## Installing the Chart + +To install the chart with the release name `kea-dhcp` + +```console +helm install kea-dhcp mglants/kea-dhcp +``` + +## Uninstalling the Chart + +To uninstall the `kea-dhcp` deployment + +```console +helm uninstall kea-dhcp +``` + +The command removes all the Kubernetes components associated with the chart **including persistent volumes** and deletes the release. + +## Configuration + +Read through the [values.yaml](./values.yaml) file. It has several commented out suggested values. + +Specify each parameter using the `--set key=value[,key=value]` argument to `helm install`. + +```console +helm install kea-dhcp \ + --set env.TZ="America/New York" \ + mglants/kea-dhcp +``` + +Alternatively, a YAML file that specifies the values for the above parameters can be provided while installing the chart. + +```console +helm install kea-dhcp mglants/kea-dhcp -f values.yaml +``` + +## Values + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| affinity | object | `{}` | | +| clusterDomain | string | `"cluster.local"` | | +| fullnameOverride | string | `""` | | +| image.pullPolicy | string | `"IfNotPresent"` | | +| image.repository | string | `"smailkoz/kea-dhcp"` | | +| image.tag | string | `""` | | +| imagePullSecrets | list | `[]` | | +| kea.ctrlagent.enabled | bool | `false` | | +| kea.ctrlagent.loglevel | string | `"DEBUG"` | | +| kea.ddns.enabled | bool | `false` | | +| kea.ddns.forward | object | `{}` | | +| kea.ddns.loglevel | string | `"DEBUG"` | | +| kea.ddns.prefix | string | `"myhost"` | | +| kea.ddns.reverse | object | `{}` | - name: home.arpa. key-name: '' dns-servers: - ip-address: 172.16.32.2 port: 53 | +| kea.ddns.suffix | string | `"home.arpa."` | | +| kea.dhcp4.enabled | bool | `true` | | +| kea.dhcp4.loglevel | string | `"DEBUG"` | | +| kea.dhcp4.options | object | `{}` | | +| kea.dhcp4.rebindtimer | string | `"2000"` | | +| kea.dhcp4.renewtimer | string | `"1000"` | | +| kea.dhcp4.reservations | object | `{}` | data: 192.168.1.2 - name: domain-name data: local - name: domain-search data: local - code: 66 data: 192.168.1.2 name: tftp-server-name | +| kea.dhcp4.subnets | object | `{}` | hw-address: "aa:aa:aa:aa:aa:aa" hostname: "hostname" option-data: - name: "tftp-servers" data: "10.1.1.202,10.1.1.203" | +| kea.dhcp4.validlifetime | string | `"4000"` | | +| kea.dhcp6.enabled | bool | `false` | | +| kea.dhcp6.loglevel | string | `"DEBUG"` | | +| kea.dhcp6.options | object | `{}` | | +| kea.dhcp6.rebindtimer | string | `"2000"` | | +| kea.dhcp6.renewtimer | string | `"1000"` | | +| kea.dhcp6.reservations | object | `{}` | | +| kea.dhcp6.subnets | object | `{}` | duid: "aa:aa:aa:aa:aa:aa" hostname: "hostname" option-data: - name: "dns-servers" data: "2001:db8:2::45, 2001:db8:2::100" | +| kea.dhcp6.validlifetime | string | `"4000"` | | +| livenessProbe.enabled | bool | `true` | | +| metrics.enabled | bool | `false` | | +| metrics.service.annotations | object | `{"prometheus.io/port":"9547","prometheus.io/scrape":"true"}` | loadBalancerIP: | +| metrics.service.labels | object | `{}` | | +| metrics.service.type | string | `"ClusterIP"` | | +| nameOverride | string | `""` | | +| nodeSelector | object | `{}` | | +| persistence.enabled | bool | `false` | | +| persistence.size | string | `"100Mi"` | | +| podAnnotations | object | `{}` | | +| podSecurityContext | object | `{}` | | +| readinessProbe.enabled | bool | `true` | | +| replicaCount | int | `1` | | +| resources.limits.cpu | string | `"200m"` | | +| resources.limits.memory | string | `"256Mi"` | | +| resources.requests.cpu | string | `"100m"` | | +| resources.requests.memory | string | `"128Mi"` | | +| role.annotations | object | `{}` | | +| roleBinding.annotations | object | `{}` | | +| securityContext | object | `{}` | | +| service.ctrl.annotations | object | `{}` | nodePort: | +| service.ctrl.loadBalancerIP | string | `nil` | | +| service.ctrl.port | int | `8000` | | +| service.ctrl.type | string | `"ClusterIP"` | | +| service.dhcp.annotations | object | `{}` | nodePort: | +| service.dhcp.loadBalancerIP | string | `nil` | | +| service.dhcp.port | int | `67` | | +| service.dhcp.type | string | `"ClusterIP"` | | +| serviceAccount.annotations | object | `{}` | | +| serviceAccount.name | string | `""` | If not set and create is true, a name is generated using the fullname template | +| sidecar.image.pullPolicy | string | `"IfNotPresent"` | | +| sidecar.image.repository | string | `"smailkoz/kea-dhcp-sidecar"` | | +| sidecar.image.tag | string | `""` | | +| sidecar.resources.limits.cpu | string | `"50m"` | | +| sidecar.resources.limits.memory | string | `"128Mi"` | | +| sidecar.resources.requests.cpu | string | `"10m"` | | +| sidecar.resources.requests.memory | string | `"32Mi"` | | +| tolerations | list | `[]` | | + +## Changelog + +### Version 0.6.1 + +#### Added + +N/A + +#### Changed + +N/A + +#### Fixed + +* probes values fixed + +### Older versions + +A historical overview of changes can be found on [ArtifactHUB](https://artifacthub.io/packages/helm/mglants/kea-dhcp?modal=changelog) + +---------------------------------------------- +Autogenerated from chart metadata using [helm-docs v0.1.1](https://github.com/k8s-at-home/helm-docs/releases/v0.1.1) diff --git a/charts/kea-dhcp/templates/_helpers.tpl b/charts/kea-dhcp/templates/_helpers.tpl new file mode 100644 index 000000000..e7e908b0a --- /dev/null +++ b/charts/kea-dhcp/templates/_helpers.tpl @@ -0,0 +1,58 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "kea-dhcp.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "kea-dhcp.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "kea-dhcp.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "kea-dhcp.labels" -}} +helm.sh/chart: {{ include "kea-dhcp.chart" . }} +{{ include "kea-dhcp.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "kea-dhcp.selectorLabels" -}} +app.kubernetes.io/name: {{ include "kea-dhcp.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "kea-dhcp.serviceAccountName" -}} +{{- default (include "kea-dhcp.fullname" .) .Values.serviceAccount.name }} +{{- end }} diff --git a/charts/kea-dhcp/templates/configmap.yaml b/charts/kea-dhcp/templates/configmap.yaml new file mode 100644 index 000000000..abd0f9efd --- /dev/null +++ b/charts/kea-dhcp/templates/configmap.yaml @@ -0,0 +1,280 @@ +kind: ConfigMap +apiVersion: v1 +metadata: + name: {{ include "kea-dhcp.fullname" . }} +data: + kea-dhcp4.conf: | + { + {{- if .Values.kea.dhcp4.enabled }} + "Dhcp4": { + "lease-database": { + "type": "memfile", + "persist": true, + "name": "/data/dhcp4.leases" + }, + "interfaces-config": { + "interfaces": [ "INTERFACE" ], + "service-sockets-require-all": true, + "dhcp-socket-type": "{{ .Values.kea.dhcp4.dhcpSocketType }}" + }, + "valid-lifetime": {{ .Values.kea.dhcp4.validlifetime }}, + "renew-timer": {{ .Values.kea.dhcp4.renewtimer }}, + "rebind-timer": {{ .Values.kea.dhcp4.rebindtimer }}, + "reservations-global": true, + "reservations-in-subnet": false, + "control-socket": { + "socket-type": "unix", + "socket-name": "/run/kea/kea-dhcp4-ctrl.sock" + }, + {{- if .Values.kea.ddns.enabled }} + "dhcp-ddns" : { + "enable-updates" : true, + "server-ip" : "127.0.0.1", + "server-port" : 53001, + "max-queue-size" : 2048, + "ncr-protocol" : "UDP", + "ncr-format" : "JSON" + }, + "ddns-send-updates" : true, + "ddns-override-no-update" : true, + "ddns-override-client-update" : true, + "ddns-replace-client-name" : "when-not-present", + "ddns-generated-prefix": {{ .Values.kea.ddns.prefix | quote }}, + "ddns-qualifying-suffix" : {{ .Values.kea.ddns.suffix | quote }}, + "ddns-update-on-renew" : false, + "ddns-use-conflict-resolution" : true, + "hostname-char-set": "[^A-Za-z0-9.-]", + "hostname-char-replacement": "", + {{- end }} + {{- if .Values.kea.dhcp4.options }} + "option-data": {{ .Values.kea.dhcp4.options | toJson }}, + {{- end }} + {{- if .Values.kea.dhcp4.reservations }} + "reservations": {{ .Values.kea.dhcp4.reservations | toJson }}, + {{- end }} + {{- if .Values.kea.dhcp4.subnets }} + "subnet4": {{ .Values.kea.dhcp4.subnets | toJson }}, + {{- end }} + {{- if .Values.kea.dhcp4.sharedNetworks }} + "shared-networks": {{ .Values.kea.dhcp4.sharedNetworks | toJson }}, + {{- end }} + {{- if .Values.kea.dhcp4.clientClasses }} + "client-classes": {{ .Values.kea.dhcp4.clientClasses | toJson }}, + {{- end }} + {{ if gt .Values.replicaCount 1.0 }} + + {{- end }} + "loggers": [{ + "name": "kea-dhcp4", + "output_options": [ + { + "output": "stdout" + } + ], + {{- if eq .Values.kea.dhcp4.loglevel "DEBUG" }} + "debuglevel": {{ .Values.kea.dhcp4.debugLevel }}, + {{- end }} + "severity": "{{ .Values.kea.dhcp4.loglevel }}" + }] + } + {{- end }} + } + kea-dhcp6.conf: | + { + {{- if .Values.kea.dhcp6.enabled }} + "Dhcp6": { + "lease-database": { + "type": "memfile", + "persist": true, + "name": "/data/dhcp6.leases" + }, + "interfaces-config": { + "interfaces": [ "INTERFACE" ], + "service-sockets-require-all": true, + "dhcp-socket-type": "{{ .Values.kea.dhcp6.dhcpSocketType }}" + }, + "valid-lifetime": {{ .Values.kea.dhcp6.validlifetime }}, + "renew-timer": {{ .Values.kea.dhcp6.renewtimer }}, + "rebind-timer": {{ .Values.kea.dhcp6.rebindtimer }}, + "reservations-global": true, + "reservations-in-subnet": false, + "control-socket": { + "socket-type": "unix", + "socket-name": "/run/kea/kea-dhcp6-ctrl.sock" + }, + {{- if .Values.kea.ddns.enabled }} + "dhcp-ddns" : { + "enable-updates" : true, + "server-ip" : "::1", + "server-port" : 53001, + "max-queue-size" : 2048, + "ncr-protocol" : "UDP", + "ncr-format" : "JSON" + }, + "ddns-send-updates" : true, + "ddns-override-no-update" : true, + "ddns-override-client-update" : true, + "ddns-replace-client-name" : "when-not-present", + "ddns-generated-prefix": {{ .Values.kea.ddns.prefix | quote }}, + "ddns-qualifying-suffix" : {{ .Values.kea.ddns.suffix | quote }}, + "ddns-update-on-renew" : false, + "ddns-use-conflict-resolution" : true, + "hostname-char-set": "[^A-Za-z0-9.-]", + "hostname-char-replacement": "", + {{- end }} + {{- if .Values.kea.dhcp6.options }} + "option-data": {{ .Values.kea.dhcp6.options | toJson }}, + {{- end }} + {{- if .Values.kea.dhcp6.reservations }} + "reservations": {{ .Values.kea.dhcp6.reservations | toJson }}, + {{- end }} + {{- if .Values.kea.dhcp6.subnets }} + "subnet6": {{ .Values.kea.dhcp6.subnets | toJson }}, + {{- end }} + {{- if .Values.kea.dhcp6.clientClasses }} + "client-classes": {{ .Values.kea.dhcp6.clientClasses | toJson }}, + {{- end }} + {{ if gt .Values.replicaCount 1.0 }} + + {{- end }} + "loggers": [{ + "name": "kea-dhcp6", + "output_options": [ + { + "output": "stdout" + } + ], + {{- if eq .Values.kea.dhcp6.loglevel "DEBUG" }} + "debuglevel": {{ .Values.kea.dhcp6.debugLevel }}, + {{- end }} + "severity": "{{ .Values.kea.dhcp6.loglevel }}" + }] + } + {{- end }} + } + kea-dhcp-ddns.conf: | + { + {{- if .Values.kea.ddns.enabled }} + "DhcpDdns": { + "forward-ddns": {{ .Values.kea.ddns.forward | toJson }}, + "reverse-ddns": {{ .Values.kea.ddns.reverse | toJson }}, + "control-socket": { + "socket-type": "unix", + "socket-name": "/run/kea/kea-dhcp-ddns-ctrl.sock" + }, + "loggers": [ + { + "name": "kea-dhcp-ddns", + "output_options": [ + { + "output": "stdout" + } + ], + "severity": "{{ .Values.kea.ddns.loglevel }}" + } + ] + } + {{- end }} + } + kea-ctrl-agent.conf: | + { + {{- if .Values.kea.ctrlagent.enabled }} + "Control-agent": { + "http-host": "0.0.0.0", + "http-port": 8000, + "control-sockets": { + "dhcp4": { + "socket-type": "unix", + "socket-name": "/run/kea/kea-dhcp4-ctrl.sock" + }, + "dhcp6": { + "socket-type": "unix", + "socket-name": "/run/kea/kea-dhcp6-ctrl.sock" + }, + "d2": { + "socket-type": "unix", + "socket-name": "/run/kea/kea-dhcp-ddns-ctrl.sock" + } + }, + "loggers": [ + { + "name": "kea-ctrl-agent", + "output_options": [ + { + "output": "stdout" + } + ], + "severity": "{{ .Values.kea.ctrlagent.loglevel }}" + } + ] + } + {{- end}} + } + keactrl.conf: | + #!/bin/sh + + # This is a configuration file for keactrl script which controls + # the startup, shutdown, reconfiguration and gathering the status + # of the Kea's processes. + + # Note that control agent must be launched after servers and netconf last. + + # shellcheck disable=SC2034 + # SC2034: ... appears unused. Verify use (or export if used externally). + + # prefix holds the location where the Kea is installed. + prefix="/usr/local" + + # Location of Kea configuration files. + kea_dhcp4_config_file="/config/kea/kea-dhcp4.conf" + kea_dhcp6_config_file="/config/kea/kea-dhcp6.conf" + kea_dhcp_ddns_config_file="/config/kea/kea-dhcp-ddns.conf" + kea_ctrl_agent_config_file="/config/kea/kea-ctrl-agent.conf" + kea_netconf_config_file="/config/kea/kea-netconf.conf" + + # Location of Kea binaries. + exec_prefix="${prefix}" + dhcp4_srv="${exec_prefix}/sbin/kea-dhcp4" + dhcp6_srv="${exec_prefix}/sbin/kea-dhcp6" + dhcp_ddns_srv="${exec_prefix}/sbin/kea-dhcp-ddns" + ctrl_agent_srv="${exec_prefix}/sbin/kea-ctrl-agent" + netconf_srv="${exec_prefix}/sbin/kea-netconf" + + # Start DHCPv4 server? + dhcp4={{- if .Values.kea.dhcp4.enabled }}yes{{- else }}no{{- end }} + + # Start DHCPv6 server? + dhcp6={{- if .Values.kea.dhcp6.enabled }}yes{{- else }}no{{- end }} + + # Start DHCP DDNS server? + dhcp_ddns={{- if .Values.kea.ddns.enabled }}yes{{- else }}no{{- end }} + + # Start Control Agent? + ctrl_agent={{- if .Values.kea.ctrlagent.enabled }}yes{{- else }}no{{- end }} + + # Start Netconf? + netconf=no + + # Be verbose? + kea_verbose=no + {{ if gt .Values.replicaCount 1.0 }} + ha-init.json: | + + "hooks-libraries": [{ + "library": "/usr/local/lib/kea/hooks/libdhcp_lease_cmds.so", + "parameters": { } + }, { + "library": "/usr/local/lib/kea/hooks/libdhcp_ha.so", + "parameters": { + "high-availability": [{ + "this-server-name": "HOSTNAME", + "mode": "load-balancing", + "heartbeat-delay": 10000, + "max-response-delay": 10000, + "max-ack-delay": 5000, + "max-unacked-clients": 5, + + }] + } + }], + {{- end }} diff --git a/charts/kea-dhcp/templates/metrics-service.yaml b/charts/kea-dhcp/templates/metrics-service.yaml new file mode 100644 index 000000000..a67122886 --- /dev/null +++ b/charts/kea-dhcp/templates/metrics-service.yaml @@ -0,0 +1,30 @@ +{{- if .Values.metrics.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ template "kea-dhcp.fullname" . }}-metrics + labels: + app.kubernetes.io/name: {{ include "kea-dhcp.name" . }} + helm.sh/chart: {{ include "kea-dhcp.chart" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + {{- if .Values.metrics.service.labels -}} + {{ toYaml .Values.metrics.service.labels | nindent 4 }} + {{- end -}} + {{- if .Values.metrics.service.annotations }} + annotations: {{ toYaml .Values.metrics.service.annotations | nindent 4 }} + {{- end }} +spec: + type: {{ .Values.metrics.service.type }} + {{ if eq .Values.metrics.service.type "LoadBalancer" -}} {{ if .Values.metrics.service.loadBalancerIP }} + loadBalancerIP: {{ .Values.metrics.service.loadBalancerIP }} + {{ end -}} + {{- end -}} + ports: + - name: metrics + port: 9547 + targetPort: metrics + selector: + app.kubernetes.io/name: {{ include "kea-dhcp.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} diff --git a/charts/kea-dhcp/templates/role.yaml b/charts/kea-dhcp/templates/role.yaml new file mode 100644 index 000000000..5c89292f0 --- /dev/null +++ b/charts/kea-dhcp/templates/role.yaml @@ -0,0 +1,16 @@ +{{ if gt .Values.replicaCount 1.0 }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: {{ include "kea-dhcp.fullname" . }} + labels: + {{- include "kea-dhcp.labels" . | nindent 4 }} + {{- with .Values.role.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +rules: +- apiGroups: [""] + resources: ["pods", "services"] + verbs: ["get", "list", "watch"] +{{- end }} diff --git a/charts/kea-dhcp/templates/rolebinding.yaml b/charts/kea-dhcp/templates/rolebinding.yaml new file mode 100644 index 000000000..93b8b88e0 --- /dev/null +++ b/charts/kea-dhcp/templates/rolebinding.yaml @@ -0,0 +1,19 @@ +{{ if gt .Values.replicaCount 1.0 }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ include "kea-dhcp.fullname" . }} + labels: + {{- include "kea-dhcp.labels" . | nindent 4 }} + {{- with .Values.roleBinding.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +subjects: +- kind: ServiceAccount + name: {{ include "kea-dhcp.serviceAccountName" . }} +roleRef: + kind: Role + name: {{ include "kea-dhcp.fullname" . }} + apiGroup: rbac.authorization.k8s.io +{{- end }} diff --git a/charts/kea-dhcp/templates/service.yaml b/charts/kea-dhcp/templates/service.yaml new file mode 100644 index 000000000..3b16d482c --- /dev/null +++ b/charts/kea-dhcp/templates/service.yaml @@ -0,0 +1,112 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "kea-dhcp.fullname" . }}-headless + labels: + {{- include "kea-dhcp.labels" . | nindent 4 }} +spec: + type: ClusterIP + clusterIP: None + ports: + - port: 67 + targetPort: dhcp + protocol: UDP + name: dhcp + - port: 8080 + targetPort: control-agent + protocol: TCP + name: control-agent + selector: + {{- include "kea-dhcp.selectorLabels" . | nindent 4 }} +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ include "kea-dhcp.fullname" . }} + labels: + {{- include "kea-dhcp.labels" . | nindent 4 }} + {{- if .Values.service.dhcp.annotations }} + {{- with .Values.service.dhcp.annotations }} + annotations: + {{- toYaml . | indent 4 }} + {{- end }} + {{- end }} +spec: +{{- if (or (eq .Values.service.dhcp.type "ClusterIP") (empty .Values.service.dhcp.type)) }} + type: ClusterIP + {{- if .Values.service.dhcp.clusterIP }} + clusterIP: {{ .Values.service.dhcp.clusterIP }} + {{end}} +{{- else if eq .Values.service.dhcp.type "LoadBalancer" }} + type: {{ .Values.service.dhcp.type }} + {{- if .Values.service.dhcp.loadBalancerIP }} + loadBalancerIP: {{ .Values.service.dhcp.loadBalancerIP }} + {{- end }} + {{- if .Values.service.dhcp.loadBalancerSourceRanges }} + loadBalancerSourceRanges: +{{ toYaml .Values.service.dhcp.loadBalancerSourceRanges | indent 4 }} + {{- end -}} +{{- else }} + type: {{ .Values.service.dhcp.type }} +{{- end }} +{{- if .Values.service.dhcp.externalIPs }} + externalIPs: +{{ toYaml .Values.service.dhcp.externalIPs | indent 4 }} +{{- end }} + {{- if .Values.service.dhcp.externalTrafficPolicy }} + externalTrafficPolicy: {{ .Values.service.dhcp.externalTrafficPolicy }} + {{- end }} + ports: + - port: {{ .Values.service.dhcp.port }} + targetPort: dhcp + protocol: UDP + name: dhcp + selector: + {{- include "kea-dhcp.selectorLabels" . | nindent 4 }} +--- +{{- if .Values.kea.ctrlagent.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "kea-dhcp.fullname" . }}-ctrl + labels: + {{- include "kea-dhcp.labels" . | nindent 4 }} + {{- if .Values.service.ctrl.annotations }} + {{- with .Values.service.ctrl.annotations }} + annotations: + {{- toYaml . | indent 4 }} + {{- end }} + {{- end }} +spec: +{{- if (or (eq .Values.service.ctrl.type "ClusterIP") (empty .Values.service.ctrl.type)) }} + type: ClusterIP + {{- if .Values.service.ctrl.clusterIP }} + clusterIP: {{ .Values.service.ctrl.clusterIP }} + {{end}} +{{- else if eq .Values.service.ctrl.type "LoadBalancer" }} + type: {{ .Values.service.ctrl.type }} + {{- if .Values.service.ctrl.loadBalancerIP }} + loadBalancerIP: {{ .Values.service.ctrl.loadBalancerIP }} + {{- end }} + {{- if .Values.service.ctrl.loadBalancerSourceRanges }} + loadBalancerSourceRanges: +{{ toYaml .Values.service.ctrl.loadBalancerSourceRanges | indent 4 }} + {{- end -}} +{{- else }} + type: {{ .Values.service.ctrl.type }} +{{- end }} +{{- if .Values.service.ctrl.externalIPs }} + externalIPs: +{{ toYaml .Values.service.ctrl.externalIPs | indent 4 }} +{{- end }} + {{- if .Values.service.ctrl.externalTrafficPolicy }} + externalTrafficPolicy: {{ .Values.service.ctrl.externalTrafficPolicy }} + {{- end }} + ports: + - port: {{ .Values.service.ctrl.port }} + targetPort: control-agent + protocol: TCP + name: control-agent + selector: + {{- include "kea-dhcp.selectorLabels" . | nindent 4 }} +{{- end }} diff --git a/charts/kea-dhcp/templates/serviceaccount.yaml b/charts/kea-dhcp/templates/serviceaccount.yaml new file mode 100644 index 000000000..2f8e9016c --- /dev/null +++ b/charts/kea-dhcp/templates/serviceaccount.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "kea-dhcp.serviceAccountName" . }} + labels: + {{- include "kea-dhcp.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} diff --git a/charts/kea-dhcp/templates/servicemonitor.yaml b/charts/kea-dhcp/templates/servicemonitor.yaml new file mode 100644 index 000000000..31389052e --- /dev/null +++ b/charts/kea-dhcp/templates/servicemonitor.yaml @@ -0,0 +1,15 @@ +{{- if .Values.metrics.enabled }} +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: {{ include "kea-dhcp.fullname" . }} + labels: + {{- include "kea-dhcp.labels" . | nindent 4 }} +spec: + selector: + matchLabels: + app.kubernetes.io/name: {{ include "kea-dhcp.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + endpoints: + - port: metrics +{{- end }} diff --git a/charts/kea-dhcp/templates/statefulset.yaml b/charts/kea-dhcp/templates/statefulset.yaml new file mode 100644 index 000000000..dd3bc6c7d --- /dev/null +++ b/charts/kea-dhcp/templates/statefulset.yaml @@ -0,0 +1,201 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: {{ include "kea-dhcp.fullname" . }} + labels: + {{- include "kea-dhcp.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.replicaCount }} + serviceName: {{ template "kea-dhcp.fullname" . }}-headless + selector: + matchLabels: + {{- include "kea-dhcp.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "kea-dhcp.selectorLabels" . | nindent 8 }} + checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum | trunc 63}} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "kea-dhcp.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + initContainers: + - name: init-kea + securityContext: + {{- toYaml .Values.securityContext | nindent 10 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + command: + - sh + - "-c" + - | + set -ex + cp /mnt/config-map/* /mnt/kea/ + {{- if .Values.kea.interface }} + interface={{ .Values.kea.interface }} + {{- else }} + echo "Finding interface..." + interface=$(route | grep '^default' | grep -o '[^ ]*$') + {{- end }} + echo "Selected interface $interface" + sed -i "s/INTERFACE/${interface}/g" /mnt/kea/kea-dhcp4.conf + sed -i "s/INTERFACE/${interface}/g" /mnt/kea/kea-dhcp6.conf + sed -i "s/HOSTNAME/${HOSTNAME}/g" /mnt/kea/ha-init.json || true + touch /mnt/kea/ha.json + volumeMounts: + - name: kea-config + mountPath: /mnt/kea + - name: kea-configmap + mountPath: /mnt/config-map + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: dhcp + containerPort: 67 + protocol: UDP + - name: control-agent + containerPort: 8000 + protocol: TCP + {{- if .Values.metrics.enabled }} + - name: metrics + containerPort: 9547 + protocol: TCP + {{- end }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- if .Values.readinessProbe.enabled }} + lifecycle: + preStop: + exec: + command: ["/usr/bin/env", "keactrl", "stop", "-c", "/config/kea/keactrl.conf"] + readinessProbe: + exec: + command: + - /bin/sh + - -c + - > + {{- if .Values.kea.dhcp4.enabled }} + if keactrl status | grep -q 'DHCPv4 server: active'; then + exit 0; + else + exit 1; + fi + {{- end }} + {{- if .Values.kea.dhcp6.enabled }} + if keactrl status | grep -q 'DHCPv6 server: active'; then + exit 0; + else + exit 1; + fi + {{- end }} + initialDelaySeconds: 10 + periodSeconds: 10 + {{- end }} + {{- if .Values.livenessProbe.enabled }} + livenessProbe: + exec: + command: + - /bin/sh + - -c + - > + {{- if .Values.kea.dhcp4.enabled }} + if keactrl status | grep -q 'DHCPv4 server: active'; then + exit 0; + else + exit 1; + fi + {{- end }} + {{- if .Values.kea.dhcp6.enabled }} + if keactrl status | grep -q 'DHCPv6 server: active'; then + exit 0; + else + exit 1; + fi + {{- end }} + initialDelaySeconds: 60 + periodSeconds: 20 + {{- end }} + volumeMounts: + - name: data + mountPath: /data + - name: kea-run + mountPath: /run/kea + # Prevent error Unable to use interprocess sync lockfile (No such file or directory): /data/run/kea/logger_lockfile + - name: kea-run + mountPath: /data/run/kea + - name: kea-config + mountPath: /config/kea + {{ if gt .Values.replicaCount 1.0 }} + - name: {{ .Chart.Name }}-sidecar + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.sidecar.image.repository }}:{{ .Values.sidecar.image.tag | default "latest" }}" + imagePullPolicy: {{ .Values.sidecar.image.pullPolicy }} + resources: + {{- toYaml .Values.sidecar.resources | nindent 12 }} + env: + - name: STATEFULSET_LABEL + valueFrom: + fieldRef: + fieldPath: metadata.labels['app.kubernetes.io/instance'] + volumeMounts: + - name: kea-config + mountPath: /config/kea + {{- end }} + volumes: + - name: kea-config + emptyDir: {} + - name: kea-run + emptyDir: {} + - name: kea-configmap + configMap: + name: "{{ include "kea-dhcp.fullname" . }}" +{{- if .Values.persistence }} +{{- if .Values.persistence.enabled }} + volumeClaimTemplates: + - metadata: + name: data + annotations: + {{- range $key, $value := .Values.persistence.annotations }} + {{ $key }}: {{ $value }} + {{- end }} + spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: {{ .Values.persistence.size | quote }} + {{- if .Values.persistence.storageClass }} + {{- if (eq "-" .Values.persistence.storageClass) }} + storageClassName: "" + {{- else }} + storageClassName: "{{ .Values.persistence.storageClass }}" + {{- end }} + {{- end }} +{{- else }} + - name: data + emptyDir: {} +{{- end }} +{{- end }} diff --git a/charts/kea-dhcp/values.schema.json b/charts/kea-dhcp/values.schema.json new file mode 100644 index 000000000..e160142aa --- /dev/null +++ b/charts/kea-dhcp/values.schema.json @@ -0,0 +1,10 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "replicaCount": { + "type": "integer", + "maximum": 2 + } + } +} diff --git a/charts/kea-dhcp/values.yaml b/charts/kea-dhcp/values.yaml new file mode 100644 index 000000000..3e5fe17e6 --- /dev/null +++ b/charts/kea-dhcp/values.yaml @@ -0,0 +1,215 @@ +# Default values for kea-dhcp. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +# More replicas is not available for now due Kea implementation of peers url should be ipv4 or ipv6 addresses instead of dnsname +replicaCount: 1 + +image: + repository: ghcr.io/mglants/kea-dhcp + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: "" +sidecar: + image: + repository: ghcr.io/mglants/kea-dhcp-sidecar + pullPolicy: IfNotPresent + # Overrides the image tag whose default is latest + tag: v0.0.2 + resources: + limits: + cpu: 50m + memory: 128Mi + requests: + cpu: 10m + memory: 32Mi +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" +clusterDomain: "cluster.local" +kea: + # interface: eth1 - specify interface if needed for multus + dhcp4: + enabled: true + dhcpSocketType: raw + validlifetime: "4000" + renewtimer: "1000" + rebindtimer: "2000" + loglevel: "DEBUG" + debugLevel: 0 + options: [] + # - name: domain-name-servers + # data: 192.168.1.2 + # - name: domain-name + # data: local + # - name: domain-search + # data: local + # - code: 66 + # data: 192.168.1.2 + # name: tftp-server-name + reservations: [] + # - ip-address: "192.168.1.2" + # hw-address: "aa:aa:aa:aa:aa:aa" + # hostname: "hostname" + # option-data: + # - name: "tftp-servers" + # data: "10.1.1.202,10.1.1.203" + subnets: [] + # - subnet: "192.168.1.0/24" + # pools: + # - pool: "192.168.1.10-192.168.1.254" + # option-data: + # - name: "routers" + # data: "172.16.10.1" + dhcp6: + enabled: false + dhcpSocketType: raw + validlifetime: "4000" + renewtimer: "1000" + rebindtimer: "2000" + loglevel: "DEBUG" + debugLevel: 0 + options: [] + reservations: [] + # - ip-address: "2001:db8:1::100" + # duid: "aa:aa:aa:aa:aa:aa" + # hostname: "hostname" + # option-data: + # - name: "dns-servers" + # data: "2001:db8:2::45, 2001:db8:2::100" + subnets: [] + # - subnet: "2001:db8:1::/48" + # pools: + # - pool: "2001:db8:1::/80" + ddns: + enabled: false + loglevel: "DEBUG" + prefix: "myhost" + suffix: "home.arpa." + forward: {} + # ddns-domains: + # - name: home.arpa. + # key-name: '' + # dns-servers: + # - ip-address: 172.16.32.2 + # port: 53 + reverse: {} + # ddns-domains: + # - name: 168.192.in-addr.arpa. + # key-name: '' + # dns-servers: + # - ip-address: 172.16.32.2 + # port: 53 + ctrlagent: + # Needed for HA, monitoring and stork + enabled: false + loglevel: "DEBUG" +metrics: + enabled: false + service: + type: ClusterIP + ## Use serviceLoadBalancerIP to request a specific static IP, + ## otherwise leave blank + # loadBalancerIP: + annotations: + prometheus.io/scrape: "true" + prometheus.io/port: "9547" + labels: {} +serviceAccount: + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +role: + # Annotations to add to the Role + annotations: {} + +roleBinding: + # Annotations to add to the RoleBinding + annotations: {} + +podAnnotations: {} + +podSecurityContext: {} +# fsGroup: 2000 + +securityContext: {} +# capabilities: +# drop: +# - ALL +# readOnlyRootFilesystem: true +# runAsNonRoot: true +# runAsUser: 1000 + +service: + dhcp: + type: ClusterIP + port: 67 + ## Specify the nodePort value for the LoadBalancer and NodePort service types. + ## ref: https://kubernetes.io/docs/concepts/services-networking/service/#type-nodeport + ## + # nodePort: + ## Provide any additional annotations which may be required. This can be used to + ## set the LoadBalancer service type to internal only. + ## ref: https://kubernetes.io/docs/concepts/services-networking/service/#internal-load-balancer + ## + annotations: {} + ## Use loadBalancerIP to request a specific static IP, + ## otherwise leave blank + ## + loadBalancerIP: + # loadBalancerSourceRanges: [] + ## Set the externalTrafficPolicy in the Service to either Cluster or Local + # externalTrafficPolicy: Cluster + + ctrl: + type: ClusterIP + port: 8000 + ## Specify the nodePort value for the LoadBalancer and NodePort service types. + ## ref: https://kubernetes.io/docs/concepts/services-networking/service/#type-nodeport + ## + # nodePort: + ## Provide any additional annotations which may be required. This can be used to + ## set the LoadBalancer service type to internal only. + ## ref: https://kubernetes.io/docs/concepts/services-networking/service/#internal-load-balancer + ## + annotations: {} + ## Use loadBalancerIP to request a specific static IP, + ## otherwise leave blank + ## + loadBalancerIP: + # loadBalancerSourceRanges: [] + ## Set the externalTrafficPolicy in the Service to either Cluster or Local + # externalTrafficPolicy: Cluster +resources: + limits: + cpu: 200m + memory: 256Mi + requests: + cpu: 100m + memory: 128Mi +readinessProbe: + enabled: true +livenessProbe: + enabled: true +nodeSelector: {} + +tolerations: [] + +affinity: {} +# affinity: +# podAntiAffinity: +# requiredDuringSchedulingIgnoredDuringExecution: +# - labelSelector: +# matchExpressions: +# - key: app.kubernetes.io/name +# operator: In +# values: +# - kea-dhcp +# topologyKey: kubernetes.io/hostname + +persistence: + enabled: false + size: 100Mi From 26ca532a2454393b925441214399793c08500b55 Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Mon, 6 Jul 2026 16:25:51 +0100 Subject: [PATCH 22/34] feat: use local kea-dhcp chart fork instead of upstream repo --- .../argocd-understack/templates/application-kea.yaml | 10 ++++------ charts/argocd-understack/values.yaml | 3 --- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/charts/argocd-understack/templates/application-kea.yaml b/charts/argocd-understack/templates/application-kea.yaml index 494bbd173..832c2442d 100644 --- a/charts/argocd-understack/templates/application-kea.yaml +++ b/charts/argocd-understack/templates/application-kea.yaml @@ -16,18 +16,16 @@ spec: server: {{ $.Values.cluster_server }} project: understack sources: - - chart: kea-dhcp + - ref: understack + path: 'charts/kea-dhcp' + repoURL: {{ include "understack.understack_url" $ }} + targetRevision: {{ include "understack.understack_ref" $ }} 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" $ }} diff --git a/charts/argocd-understack/values.yaml b/charts/argocd-understack/values.yaml index a801d691a..858fa008a 100644 --- a/charts/argocd-understack/values.yaml +++ b/charts/argocd-understack/values.yaml @@ -480,9 +480,6 @@ site: # -- 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: From d041e5b7dfa5b5a7b6d5910808c125d7ff37b172 Mon Sep 17 00:00:00 2001 From: Milan Fencik Date: Mon, 6 Jul 2026 18:10:49 +0100 Subject: [PATCH 23/34] change reservations in kea to include client classes --- .../ironic_understack/dhcp/kea.py | 51 ++++++++++++++----- 1 file changed, 37 insertions(+), 14 deletions(-) diff --git a/python/ironic-understack/ironic_understack/dhcp/kea.py b/python/ironic-understack/ironic_understack/dhcp/kea.py index 073514b4f..07c0a3976 100644 --- a/python/ironic-understack/ironic_understack/dhcp/kea.py +++ b/python/ironic-understack/ironic_understack/dhcp/kea.py @@ -1,9 +1,10 @@ +from urllib.parse import urlparse + import requests from ironic import objects 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 @@ -74,7 +75,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, boot_file_name=None, remove=False): + def _update_host_reservation(self, hw_address, kea_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 @@ -84,23 +85,38 @@ 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 + if "httpclient" in reservation.get("client-classes", []): + boot_file_name = kea_options["http"]["boot_file_name"] + reservation["boot-file-name"] = boot_file_name + next_server = urlparse(boot_file_name).hostname + reservation["next-server"] = next_server + elif "pxeclient" in reservation.get("client-classes", []): + boot_file_name = kea_options["pxe"]["boot_file_name"] + reservation["boot-file-name"] = boot_file_name + next_server = urlparse(boot_file_name).hostname + reservation["next-server"] = next_server found = True - break - if not found: + boot_file_name = kea_options["http"]["boot_file_name"] + reservations.append( + { + "hw-address": hw_address, + "boot-file-name": kea_options["http"]["boot_file_name"], + "next-server": urlparse(boot_file_name).hostname, + "client-classes": ["httpclient"], + } + ) reservations.append( { "hw-address": hw_address, - "boot-file-name": boot_file_name, - "next-server": next_server, + "boot-file-name": kea_options["pxe"]["boot_file_name"], + "next-server": urlparse(boot_file_name).hostname, + "client-classes": ["pxeclient"], } ) dhcp4_config["reservations"] = reservations @@ -116,13 +132,20 @@ 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) - boot_file_name = "" + kea_options = {} print(f"DHCP_OPTIONS: {dhcp_options}") for opt in dhcp_options: - if opt["opt_name"].startswith("!"): - boot_file_name = opt["opt_value"] - break - return self._update_host_reservation(port.address, boot_file_name) + if opt["opt_name"] == "!175,67": + kea_options["http"] = { + "boot_file_name": opt["opt_value"], + "client_class": "httpclient", + } + elif opt["opt_name"] == "67": + kea_options["pxe"] = { + "boot_file_name": opt["opt_value"], + "client_class": "pxeclient", + } + 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.""" From 82eaddc4ec74f70b0ec7201feae8447415023d60 Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Tue, 7 Jul 2026 11:55:20 +0100 Subject: [PATCH 24/34] feat(kea-dhcp): add support for optionDef --- charts/kea-dhcp/templates/configmap.yaml | 3 +++ charts/kea-dhcp/values.yaml | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/charts/kea-dhcp/templates/configmap.yaml b/charts/kea-dhcp/templates/configmap.yaml index abd0f9efd..be9f33d5a 100644 --- a/charts/kea-dhcp/templates/configmap.yaml +++ b/charts/kea-dhcp/templates/configmap.yaml @@ -49,6 +49,9 @@ data: {{- if .Values.kea.dhcp4.options }} "option-data": {{ .Values.kea.dhcp4.options | toJson }}, {{- end }} + {{- if .Values.kea.dhcp4.optionDef }} + "option-def": {{ .Values.kea.dhcp4.optionDef | toJson }}, + {{- end }} {{- if .Values.kea.dhcp4.reservations }} "reservations": {{ .Values.kea.dhcp4.reservations | toJson }}, {{- end }} diff --git a/charts/kea-dhcp/values.yaml b/charts/kea-dhcp/values.yaml index 3e5fe17e6..63f6e1da6 100644 --- a/charts/kea-dhcp/values.yaml +++ b/charts/kea-dhcp/values.yaml @@ -47,6 +47,11 @@ kea: # - code: 66 # data: 192.168.1.2 # name: tftp-server-name + optionDef: [] + # - name: "boot-file-base" + # code: "234" + # type: "string" + # space: "dhcp4" reservations: [] # - ip-address: "192.168.1.2" # hw-address: "aa:aa:aa:aa:aa:aa" From 2aeac6e197e8f56a137999e9ced641f93ba4efce Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Tue, 7 Jul 2026 12:06:39 +0100 Subject: [PATCH 25/34] feat(kea-dhcp): add support for hooks libraries --- charts/kea-dhcp/templates/configmap.yaml | 3 +++ charts/kea-dhcp/values.yaml | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/charts/kea-dhcp/templates/configmap.yaml b/charts/kea-dhcp/templates/configmap.yaml index be9f33d5a..954b2cf63 100644 --- a/charts/kea-dhcp/templates/configmap.yaml +++ b/charts/kea-dhcp/templates/configmap.yaml @@ -64,6 +64,9 @@ data: {{- if .Values.kea.dhcp4.clientClasses }} "client-classes": {{ .Values.kea.dhcp4.clientClasses | toJson }}, {{- end }} + {{- if .Values.kea.dhcp4.hooksLibraries }} + "hooks-libraries": {{ .Values.kea.dhcp4.hooksLibraries | toJson }}, + {{- end }} {{ if gt .Values.replicaCount 1.0 }} {{- end }} diff --git a/charts/kea-dhcp/values.yaml b/charts/kea-dhcp/values.yaml index 63f6e1da6..1eab293e7 100644 --- a/charts/kea-dhcp/values.yaml +++ b/charts/kea-dhcp/values.yaml @@ -52,6 +52,14 @@ kea: # code: "234" # type: "string" # space: "dhcp4" + hooksLibraries: [] + # - library: /usr/lib/x86_64-linux-gnu/kea/hooks/libdhcp_flex_option.so + # parameters: + # options: + # - code: 67 + # space: dhcp4 + # csv-format: true + # supersede: myvalue reservations: [] # - ip-address: "192.168.1.2" # hw-address: "aa:aa:aa:aa:aa:aa" From 13556f10df608f36edbd5af39e3c358a9b143f8f Mon Sep 17 00:00:00 2001 From: Milan Fencik Date: Tue, 7 Jul 2026 12:42:10 +0100 Subject: [PATCH 26/34] adjust kea reservations --- .../ironic_understack/dhcp/kea.py | 53 ++++++------------- 1 file changed, 15 insertions(+), 38 deletions(-) diff --git a/python/ironic-understack/ironic_understack/dhcp/kea.py b/python/ironic-understack/ironic_understack/dhcp/kea.py index 07c0a3976..cf3ba9e3a 100644 --- a/python/ironic-understack/ironic_understack/dhcp/kea.py +++ b/python/ironic-understack/ironic_understack/dhcp/kea.py @@ -75,7 +75,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, kea_options=None, remove=False): + def _update_host_reservation(self, hw_address, boot_file_prefix=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 @@ -84,44 +84,30 @@ def _update_host_reservation(self, hw_address, kea_options=None, remove=False): try: config = self.get_config() config["arguments"].pop("hash", None) - dhcp4_config = config["arguments"]["Dhcp4"] + dhcp4_config = config["arguments"]["Dhcp4"]["subnet4"][0] reservations = dhcp4_config.get("reservations", []) found = False for reservation in reservations: if reservation.get("hw-address") == hw_address: - if "httpclient" in reservation.get("client-classes", []): - boot_file_name = kea_options["http"]["boot_file_name"] - reservation["boot-file-name"] = boot_file_name - next_server = urlparse(boot_file_name).hostname - reservation["next-server"] = next_server - elif "pxeclient" in reservation.get("client-classes", []): - boot_file_name = kea_options["pxe"]["boot_file_name"] - reservation["boot-file-name"] = boot_file_name - next_server = urlparse(boot_file_name).hostname - reservation["next-server"] = next_server + reservation["option-data"] = [ + {"name": "boot-file-base", "data": boot_file_prefix} + ] found = True + break if not found: - boot_file_name = kea_options["http"]["boot_file_name"] reservations.append( { "hw-address": hw_address, - "boot-file-name": kea_options["http"]["boot_file_name"], - "next-server": urlparse(boot_file_name).hostname, - "client-classes": ["httpclient"], - } - ) - reservations.append( - { - "hw-address": hw_address, - "boot-file-name": kea_options["pxe"]["boot_file_name"], - "next-server": urlparse(boot_file_name).hostname, - "client-classes": ["pxeclient"], + "option-data": [ + {"name": "boot-file-base", "data": boot_file_prefix}, + ], } ) dhcp4_config["reservations"] = reservations - config["arguments"]["Dhcp4"] = dhcp4_config + print(dhcp4_config) + config["arguments"]["Dhcp4"]["subnet4"][0] = dhcp4_config self.set_config(config["arguments"]) return True except Exception as e: @@ -132,20 +118,11 @@ 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 = {} - print(f"DHCP_OPTIONS: {dhcp_options}") for opt in dhcp_options: - if opt["opt_name"] == "!175,67": - kea_options["http"] = { - "boot_file_name": opt["opt_value"], - "client_class": "httpclient", - } - elif opt["opt_name"] == "67": - kea_options["pxe"] = { - "boot_file_name": opt["opt_value"], - "client_class": "pxeclient", - } - return self._update_host_reservation(port.address, kea_options) + if opt["opt_name"] == "67": + parsed_url = urlparse(opt["opt_value"]) + boot_file_prefix = f"{parsed_url.scheme}://{parsed_url.netloc}/" + return self._update_host_reservation(port.address, boot_file_prefix) def update_dhcp_opts(self, task, options, vifs=None): """Update DHCP options for all ports associated with a node.""" From 55142c0e1c91ac2028cab80befdf59f830fee4f2 Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Tue, 7 Jul 2026 15:28:11 +0100 Subject: [PATCH 27/34] feat(kea-dhcp): add support for overriding loggers --- charts/kea-dhcp/templates/configmap.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/charts/kea-dhcp/templates/configmap.yaml b/charts/kea-dhcp/templates/configmap.yaml index 954b2cf63..cb8c783ce 100644 --- a/charts/kea-dhcp/templates/configmap.yaml +++ b/charts/kea-dhcp/templates/configmap.yaml @@ -70,6 +70,9 @@ data: {{ if gt .Values.replicaCount 1.0 }} {{- end }} + {{- if .Values.kea.dhcp4.loggers }} + "loggers": {{ .Values.kea.dhcp4.loggers | toJson }} + {{- else}} "loggers": [{ "name": "kea-dhcp4", "output_options": [ @@ -82,6 +85,7 @@ data: {{- end }} "severity": "{{ .Values.kea.dhcp4.loglevel }}" }] + {{- end }} } {{- end }} } From f96159b1228caab0bd44c40b5721556f45bcf4d1 Mon Sep 17 00:00:00 2001 From: Milan Fencik Date: Tue, 7 Jul 2026 16:00:03 +0100 Subject: [PATCH 28/34] add always-send: true to kea options --- .../ironic-understack/ironic_understack/dhcp/kea.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/python/ironic-understack/ironic_understack/dhcp/kea.py b/python/ironic-understack/ironic_understack/dhcp/kea.py index cf3ba9e3a..e8c9bb09e 100644 --- a/python/ironic-understack/ironic_understack/dhcp/kea.py +++ b/python/ironic-understack/ironic_understack/dhcp/kea.py @@ -91,7 +91,11 @@ def _update_host_reservation(self, hw_address, boot_file_prefix=None, remove=Fal for reservation in reservations: if reservation.get("hw-address") == hw_address: reservation["option-data"] = [ - {"name": "boot-file-base", "data": boot_file_prefix} + { + "name": "boot-file-base", + "data": boot_file_prefix, + "always-send": True, + } ] found = True break @@ -100,7 +104,11 @@ def _update_host_reservation(self, hw_address, boot_file_prefix=None, remove=Fal { "hw-address": hw_address, "option-data": [ - {"name": "boot-file-base", "data": boot_file_prefix}, + { + "name": "boot-file-base", + "data": boot_file_prefix, + "always-send": True, + }, ], } ) From 6ce15cba2279eeb7f0d892897477915521022962 Mon Sep 17 00:00:00 2001 From: Milan Fencik Date: Tue, 7 Jul 2026 16:41:06 +0100 Subject: [PATCH 29/34] send only client-classes for kea options --- .../ironic_understack/dhcp/kea.py | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/python/ironic-understack/ironic_understack/dhcp/kea.py b/python/ironic-understack/ironic_understack/dhcp/kea.py index e8c9bb09e..175670c19 100644 --- a/python/ironic-understack/ironic_understack/dhcp/kea.py +++ b/python/ironic-understack/ironic_understack/dhcp/kea.py @@ -90,26 +90,28 @@ def _update_host_reservation(self, hw_address, boot_file_prefix=None, remove=Fal found = False for reservation in reservations: if reservation.get("hw-address") == hw_address: - reservation["option-data"] = [ - { - "name": "boot-file-base", - "data": boot_file_prefix, - "always-send": True, - } - ] + # reservation["option-data"] = [ + # { + # "name": "boot-file-base", + # "data": boot_file_prefix, + # "always-send": True, + # } + # ] + reservation["client-classes"] = ["BOOTSRV_A"] found = True break if not found: reservations.append( { "hw-address": hw_address, - "option-data": [ - { - "name": "boot-file-base", - "data": boot_file_prefix, - "always-send": True, - }, - ], + # "option-data": [ + # { + # "name": "boot-file-base", + # "data": boot_file_prefix, + # "always-send": True, + # }, + # ], + "client-classes": ["BOOTSRV_A"], } ) dhcp4_config["reservations"] = reservations From 705cc8f43737574a22a9d0a1fc129cd8af5f83d5 Mon Sep 17 00:00:00 2001 From: Milan Fencik Date: Tue, 7 Jul 2026 16:57:58 +0100 Subject: [PATCH 30/34] move kea reservations to global scope --- python/ironic-understack/ironic_understack/dhcp/kea.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/ironic-understack/ironic_understack/dhcp/kea.py b/python/ironic-understack/ironic_understack/dhcp/kea.py index 175670c19..2036cd946 100644 --- a/python/ironic-understack/ironic_understack/dhcp/kea.py +++ b/python/ironic-understack/ironic_understack/dhcp/kea.py @@ -84,7 +84,7 @@ def _update_host_reservation(self, hw_address, boot_file_prefix=None, remove=Fal try: config = self.get_config() config["arguments"].pop("hash", None) - dhcp4_config = config["arguments"]["Dhcp4"]["subnet4"][0] + dhcp4_config = config["arguments"]["Dhcp4"] reservations = dhcp4_config.get("reservations", []) found = False @@ -117,7 +117,7 @@ def _update_host_reservation(self, hw_address, boot_file_prefix=None, remove=Fal dhcp4_config["reservations"] = reservations print(dhcp4_config) - config["arguments"]["Dhcp4"]["subnet4"][0] = dhcp4_config + config["arguments"]["Dhcp4"] = dhcp4_config self.set_config(config["arguments"]) return True except Exception as e: From ab84f1a2c096ac78dca8869a2b5475ab0d965e78 Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Tue, 7 Jul 2026 16:59:31 +0100 Subject: [PATCH 31/34] kea: change early-global-reservations-lookup to true --- charts/kea-dhcp/templates/configmap.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/charts/kea-dhcp/templates/configmap.yaml b/charts/kea-dhcp/templates/configmap.yaml index cb8c783ce..c02f357e3 100644 --- a/charts/kea-dhcp/templates/configmap.yaml +++ b/charts/kea-dhcp/templates/configmap.yaml @@ -22,6 +22,7 @@ data: "rebind-timer": {{ .Values.kea.dhcp4.rebindtimer }}, "reservations-global": true, "reservations-in-subnet": false, + "early-global-reservations-lookup": true, "control-socket": { "socket-type": "unix", "socket-name": "/run/kea/kea-dhcp4-ctrl.sock" From 1310e6209313ee46b376f152ff9d51cf18e7ec4b Mon Sep 17 00:00:00 2001 From: Milan Fencik Date: Tue, 7 Jul 2026 17:27:19 +0100 Subject: [PATCH 32/34] add options-data to kea in global reservations --- .../ironic_understack/dhcp/kea.py | 30 +++++++++---------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/python/ironic-understack/ironic_understack/dhcp/kea.py b/python/ironic-understack/ironic_understack/dhcp/kea.py index 2036cd946..7ff49c154 100644 --- a/python/ironic-understack/ironic_understack/dhcp/kea.py +++ b/python/ironic-understack/ironic_understack/dhcp/kea.py @@ -90,28 +90,26 @@ def _update_host_reservation(self, hw_address, boot_file_prefix=None, remove=Fal found = False for reservation in reservations: if reservation.get("hw-address") == hw_address: - # reservation["option-data"] = [ - # { - # "name": "boot-file-base", - # "data": boot_file_prefix, - # "always-send": True, - # } - # ] - reservation["client-classes"] = ["BOOTSRV_A"] + reservation["option-data"] = [ + { + "name": "boot-file-base", + "data": boot_file_prefix, + "always-send": True, + } + ] found = True break if not found: reservations.append( { "hw-address": hw_address, - # "option-data": [ - # { - # "name": "boot-file-base", - # "data": boot_file_prefix, - # "always-send": True, - # }, - # ], - "client-classes": ["BOOTSRV_A"], + "option-data": [ + { + "name": "boot-file-base", + "data": boot_file_prefix, + "always-send": True, + }, + ], } ) dhcp4_config["reservations"] = reservations From 5715578cf647555689711eabc559923d7bc9f3b6 Mon Sep 17 00:00:00 2001 From: Marek Skrobacki Date: Tue, 7 Jul 2026 18:16:23 +0100 Subject: [PATCH 33/34] Revert "add options-data to kea in global reservations" This reverts commit 1310e6209313ee46b376f152ff9d51cf18e7ec4b. We cannot use option 234, because the flex_option expression only has access to the DHCPDISCOVER, not reservations. --- .../ironic_understack/dhcp/kea.py | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/python/ironic-understack/ironic_understack/dhcp/kea.py b/python/ironic-understack/ironic_understack/dhcp/kea.py index 7ff49c154..2036cd946 100644 --- a/python/ironic-understack/ironic_understack/dhcp/kea.py +++ b/python/ironic-understack/ironic_understack/dhcp/kea.py @@ -90,26 +90,28 @@ def _update_host_reservation(self, hw_address, boot_file_prefix=None, remove=Fal found = False for reservation in reservations: if reservation.get("hw-address") == hw_address: - reservation["option-data"] = [ - { - "name": "boot-file-base", - "data": boot_file_prefix, - "always-send": True, - } - ] + # reservation["option-data"] = [ + # { + # "name": "boot-file-base", + # "data": boot_file_prefix, + # "always-send": True, + # } + # ] + reservation["client-classes"] = ["BOOTSRV_A"] found = True break if not found: reservations.append( { "hw-address": hw_address, - "option-data": [ - { - "name": "boot-file-base", - "data": boot_file_prefix, - "always-send": True, - }, - ], + # "option-data": [ + # { + # "name": "boot-file-base", + # "data": boot_file_prefix, + # "always-send": True, + # }, + # ], + "client-classes": ["BOOTSRV_A"], } ) dhcp4_config["reservations"] = reservations From 5c198815344a49ad03bc6203e41d498e174195f8 Mon Sep 17 00:00:00 2001 From: Milan Fencik Date: Wed, 8 Jul 2026 12:07:54 +0100 Subject: [PATCH 34/34] fix cleanup for kea --- .../ironic_understack/dhcp/kea.py | 37 +++++++------------ 1 file changed, 14 insertions(+), 23 deletions(-) diff --git a/python/ironic-understack/ironic_understack/dhcp/kea.py b/python/ironic-understack/ironic_understack/dhcp/kea.py index 2036cd946..8efb234d4 100644 --- a/python/ironic-understack/ironic_understack/dhcp/kea.py +++ b/python/ironic-understack/ironic_understack/dhcp/kea.py @@ -90,33 +90,24 @@ def _update_host_reservation(self, hw_address, boot_file_prefix=None, remove=Fal found = False for reservation in reservations: if reservation.get("hw-address") == hw_address: - # reservation["option-data"] = [ - # { - # "name": "boot-file-base", - # "data": boot_file_prefix, - # "always-send": True, - # } - # ] - reservation["client-classes"] = ["BOOTSRV_A"] + if not remove: + reservation["client-classes"] = ["BOOTSRV_A"] + else: + reservations.pop(reservation) found = True break if not found: - reservations.append( - { - "hw-address": hw_address, - # "option-data": [ - # { - # "name": "boot-file-base", - # "data": boot_file_prefix, - # "always-send": True, - # }, - # ], - "client-classes": ["BOOTSRV_A"], - } - ) - dhcp4_config["reservations"] = reservations + if not remove: + reservations.append( + { + "hw-address": hw_address, + "client-classes": ["BOOTSRV_A"], + } + ) + dhcp4_config["reservations"] = reservations + else: + return True - print(dhcp4_config) config["arguments"]["Dhcp4"] = dhcp4_config self.set_config(config["arguments"]) return True