OpenKeel : la solution Cloud souveraine, française à portée européenne du groupe inherent Découvrir inherent

Ressources

Placement sur les nœuds

Document de conception : où placer chaque composant d'OpenKeel sur un cluster Talos.

OpenKeel node placement

Status: design proposal, 2026-10-29, updated the same day with the user's decisions (§ 0). Implemented on 2026-11-02: the role node selectors and tolerations of every chart, set by okadm in production (§ 5.2); the rest of § 5 (spread, priority classes, platform values) is still a proposal.

0. Decisions (2026-10-29)

#DecisionWhere it applies
D1Production uses the hardened label keys node-restriction.kubernetes.io/openkeel-<role>: kubelets cannot set them, only administrators or Cluster API. The node-role.openkeel.io/<role> keys remain for labs§ 1.1, § 5.1, § 5.4
D2Neutron runs a parallel OVN (mode B) for now: the Go Neutron writes its own OVN NB with ML2/OVN-identical rows, alongside Kube-OVN. Whether Kube-OVN stays the primary CNI is still open (§ 7)§ 3, § 2.2
D3Distributed floating IPs (enable_distributed_floating_ip = true): compute racks have the external VLANs, every hypervisor gets the physnet bridge mappings and NATs the FIPs of its own VMs; network nodes keep SNAT and router gateways§ 3.3, § 1.2, § 2.2
D4The HTTP metadata service at 169.254.169.254 is offered, in addition to config drive / NoCloud§ 3.4
D5Galera on Longhorn with one replica (numberOfReplicas: "1", dataLocality: strict-local); RabbitMQ the same, since quorum queues replicate. (OpenKeel deploys no Memcached or Redis since 2026-11-08: no component read them.)§ 2.1
D6Placement inventories subtract the live requests of the non-VM pods of each compute node (dynamic reservation)§ 4.2
D7Ironic conductor on a dedicated provisioning node§ 1.4, § 2.2
D8Public ingress on the network nodes and on the control plane nodes§ 2.2, § 5.1

This document says which OpenKeel component runs on which node of the Kubernetes cluster, how nodes are given roles, how the charts turn roles into scheduling constraints, and how operators add, drain and lose nodes.

Sources:

  • the skill references kubernetes.md, openkeel-chart.md, and the backends nova-kubevirt.md, neutron-ovn.md, swift-csi-block.md, masakari-kubevirt.md, ironic-baremetal.md, cinder-csi-block.md, glance-csi-block.md, designate-external-dns.md;
  • the charts in openkeel-chart/ and openkeel-{keystone,placement,cinder,glance}/deploy/helm/ (they set topologySpreadConstraints only; no node selector, toleration or priority class yet);
  • the reverse-specs reverse-spec-nova/ (compute host model), reverse-spec-neutron/ (ML2/OVN, gateway scheduling, metadata agent) and reverse-spec-ovn-bgp-agent/ (per-node agent, FRR, privileges);
  • API types of the cloned operators: KubeVirt NodePlacement (kubevirt:staging/src/kubevirt.io/api/core/v1/componentconfig.go:12), MariaDB Operator (mariadb-operator:api/v1alpha1/base_types.go:379-474: affinity, nodeSelector, tolerations, priorityClassName), RabbitmqCluster (rabbitmq-cluster-operator:api/v1beta1/rabbitmqcluster_types.go:77-85: affinity, tolerations, override), Kube-OVN v1.16.15 (ProviderNetwork.spec.nodeSelector, Vpc.spec.bfdPort.nodeSelector).

Statements marked [verify] come from knowledge of upstream projects whose source is not cloned in the workspace.


1. Node roles

1.1 Label scheme

One boolean label per role, so that a node can carry several roles:

node-role.openkeel.io/<role>: ""

A single-valued key such as openkeel.io/role=compute was rejected: a hyperconverged node needs several roles at once, and a label selector cannot match one item of a comma-separated value.

The charts never hard-code the key. Each role's label key and taint live in global.nodeRoles (§ 5), so a deployment can switch to a hardened key set:

ProfileKeyWho can set itSynced by Cluster API from Machine labels
defaultnode-role.openkeel.io/<role>the administrator, and the kubelet of the node itself (--node-labels, or a patch of its own Node object)no (CAPI only syncs node-role.kubernetes.io/*, node-restriction.kubernetes.io/* and *.node.cluster.x-k8s.io [verify on the CAPI version])
hardenednode-restriction.kubernetes.io/openkeel-<role>the administrator or Cluster API only: the NodeRestriction admission plugin forbids kubelets to set this prefixyes

The hardened profile matters for the roles that attract secrets or privileged access (control plane: Galera, Barbican, Keystone keys; provisioning: BMC credentials). With the default keys, a compromised hypervisor could label itself control-plane and get a Galera replica scheduled on it. Decided (D1): hardened keys in production; the default keys are kept for labs. Examples in this document use the short default keys for readability; in production every node-role.openkeel.io/<role> reads node-restriction.kubernetes.io/openkeel-<role>, which global.nodeRoles switches in one place (§ 5.1).

Consequence for the tooling: hardened labels cannot come from the node's own registration (kubelet --node-labels, kubeadm nodeRegistration, and probably Talos machine.nodeLabels, which Talos applies with the node's credentials [verify]). They are set by Cluster API (synced from the Machine template) or by an administrator (kubectl label, § 5.4).

Other labels used by OpenKeel (not roles):

LabelSet byMeaning
topology.kubernetes.io/zone, topology.kubernetes.io/regioninfrastructure (cloud provider, CAPI, administrator)failure domain; source of Nova/Cinder availability zones, Swift zones, spread constraints
openkeel.io/ha-segment=<segment uuid>the Go Masakari (from segments/{id}/hosts)Masakari failover segment; selected by its NodeHealthCheck (masakari-kubevirt.md)
aggregate.openkeel.io/<aggregate uuid>: ""the Go Nova (from os-aggregates)Nova host aggregate membership (§ 4.3)
trait.openkeel.io/<TRAIT>: ""administratorextra Placement traits for the node's resource provider (§ 4.2)
network.openkeel.io/bridge-mappings, network.openkeel.io/external-nicadministratorper-node OVN physical wiring (§ 3)
kube-ovn/role=masteradministrator (Kube-OVN convention [verify])nodes that run Kube-OVN's ovn-central
KubeVirt node-labeller labels (cpu-model.node.kubevirt.io/*, cpu-feature.node.kubevirt.io/*, kubevirt.io/schedulable) and NFD labels (feature.node.kubernetes.io/*)KubeVirt, Node Feature Discoveryhardware capabilities, turned into Placement traits

1.2 Roles

RoleLabel (default key)Taint when dedicatedWhat runs there
control planenode-role.openkeel.io/control-planenode-role.openkeel.io/control-plane=:NoScheduleevery OpenKeel API, scheduler, conductor and worker; MariaDB Galera, RabbitMQ; the OpenKeel OVN central (NB, SB, northd); the operators' controllers; KubeVirt and CDI infra; public ingress (D8)
compute (hypervisor)node-role.openkeel.io/computenode-role.openkeel.io/compute=:NoSchedulevirt-handler, the VMs' virt-launcher pods, the OpenKeel compute agent, the OpenKeel ovn-controller (the node is a chassis of the tenant OVN), the metadata agent (D4), NAT of its VMs' floating IPs (distributed, D3) with ovn-bgp-agent when BGP is used, Longhorn engine and CSI node plugin, Self Node Remediation agent
network (gateway)node-role.openkeel.io/networknode-role.openkeel.io/network=:NoScheduleOpenKeel OVN gateway chassis (router gateways, SNAT for VMs without a FIP; floating IPs are distributed to the hypervisors, D3), ovn-bgp-agent + FRR, public ingress (D8)
containers (Zun)node-role.openkeel.io/containersnode-role.openkeel.io/containers=:NoSchedulethe Zun compute agent and the Pods of Zun's containers and capsules, run by the Kata Containers RuntimeClass (kata: one lightweight VM per Pod, a stronger isolation than the hypervisors' runc), the OpenKeel ovn-controller and its CNI plugin (the containers' Neutron ports); needs /dev/kvm and the Kata runtime (Talos kata-containers extension). Separate nodes keep tenant containers off the hypervisors; a hyperconverged node carries both compute and containers
storagenode-role.openkeel.io/storagenode-role.openkeel.io/storage=:NoScheduleLonghorn disks and replicas, Longhorn RWX share managers (Manila), the Swift storage StatefulSet, CDI import/clone pods
provisioning (bare metal, optional)node-role.openkeel.io/provisioningnode-role.openkeel.io/provisioning=:NoSchedule alwaysopenkeel-ironic-conductor (StatefulSet, upstream Python) with the image HTTP server (dnsmasq with iPXE, later), attached to the provisioning VLAN and the BMC network

The Kubernetes control plane (etcd, kube-apiserver) is a separate concern: it keeps the upstream label node-role.kubernetes.io/control-plane. The OpenKeel control plane may share those nodes on small and medium clusters (§ 6).

1.3 Sizing hints

RoleCPU / RAMDisksNICsKernel / kubelet
control plane16-32 cores, 64-128 GiB; Galera and RabbitMQ dominateNVMe for Galera, RabbitMQ and (when shared) etcd; 200-500 GiB2 × 10-25 GbE bondednothing special
computesized for VMs; reserve 2 cores + 6-8 GiB for the host (systemReserved, kubeReserved) plus ~1 core / 2 GiB for OVS + ovn-controller + kube-ovn-cni, ~0.5 core / 1 GiB for virt-handler, Longhorn and SNRsmall OS disk; VM disks come from Longhorn2 × 25 GbE (tenant overlay + storage); SR-IOV-capable NICs if needed/dev/kvm, hugepages reserved at boot (hugepagesz=1G hugepages=N), CPU manager static + topology manager single-numa-node for dedicated-CPU flavors, IOMMU for SR-IOV/PCI passthrough, /dev/watchdog for Self Node Remediation
network8-16 cores, 32 GiB (OVS datapath, conntrack)OS only2 × 25-100 GbE: one bond to the tenant overlay, one bond to the provider / external VLANs; BGP-facing links if ovn-bgp-agentDPDK optional; large conntrack tables
storage8-16 cores, 64 GiB (Longhorn instance managers, Swift object servers)many NVMe/SSD (Longhorn) or HDD (Swift capacity tiers)2 × 25 GbE, ideally a dedicated storage network (Longhorn storageNetwork via Multus)XFS support for Swift devices
provisioning4-8 cores, 16 GiB200 GiB+ image cacheaccess port on the provisioning VLAN, route to the BMC networknothing special

1.4 Combining roles

CombinationSmall (3 nodes)MediumLarge
control plane + computeyes (hyperconverged)no: VMs and Galera compete for memory and disk I/Ono
control plane + networkyespossible if control plane nodes have the external NICs; otherwise separateno
control plane + storageyesyes, when Longhorn runs on the control plane disksno
compute + networkyesacceptable with distributed floating IPs, where every compute node is a gateway for its own FIPs (D3: compute racks have the external VLANs); dedicated gateways still recommended for SNAT and router gatewaysno, except the compute nodes' own distributed FIP path
compute + storageyesrecommended (hyperconverged storage: Longhorn data locality keeps VM disks local)separate when storage is large or Swift has its own capacity nodes
compute + containersyesyes: VMs and Kata containers share the node's Placement provider (Nova's agent owns the inventory of a node that has both roles)separate when tenant containers must not share hosts with VMs, or need other hardware
provisioning + anythingno: always separate, at least the conductor pod with its taint and its own namespace; on a 3-node lab it may share a control plane nodededicated node (D7)dedicated nodes, one per provisioning network

Taints are off on small clusters (every node carries every role, nothing to repel) and on at scale, where they keep generic pods off hypervisors and gateways. The charts always set the tolerations of their role, so switching taints on is a node operation, not a chart change.


2. Placement matrix

Conventions:

  • Role = the node role selected (nodeSelector on the role label, through the chart helper of § 5). "all" means every node that runs VMs or volumes.
  • Spread: unless stated, Deployments use the baseline of kubernetes.md: topologySpreadConstraints on topology.kubernetes.io/zone (ScheduleAnyway) and on kubernetes.io/hostname (DoNotSchedule when replicas ≤ nodes of the role, else ScheduleAnyway).
  • PDB: APIs minAvailable: 2 (3 replicas); Lease-elected workers maxUnavailable: 1; quorum StatefulSets maxUnavailable: 1.
  • Priority classes (new, created by openkeel-chart; openkeel-vm is created by the openkeel-nova chart when installed alone): | PriorityClass | Value | Used by | |---|---|---| | system-node-critical (built in) | 2000001000 | CNI and OVS DaemonSets, virt-handler, CSI node plugins | | openkeel-infra-critical | 1000000 | Galera, RabbitMQ, OVN central | | openkeel-control | 900000 | OpenKeel APIs, conductors, schedulers, workers, operators' controllers | | openkeel-vm | 500000, preemptionPolicy: Never | virt-launcher pods: never preempt anything, and are only preempted by infra that lost its node |
  • Host / priv: hostNetwork and privileges. "no" means the kubernetes.md baseline (non-root, read-only root FS, all capabilities dropped).

2.1 Shared infrastructure

ComponentRoleKindTolerationsAnti-affinity / spreadPDBHost / privNotes
MariaDB Galera (MariaDB CR)control planeStatefulSet (operator)control-planerequired anti-affinity on hostname; spread on zonemaxUnavailable: 1no**volumes (D5): StorageClass longhorn-local = Longhorn with numberOfReplicas: "1" + dataLocality: strict-local**. Galera replicates already: never stack 3 Galera × 3 Longhorn replicas (same rule as Swift). A strict-local volume cannot follow its pod: when a node is lost for good, delete the member's PVC so that it is recreated empty on another node and rejoins by SST. CR fields nodeSelector, tolerations, affinity, priorityClassName
RabbitMQ (RabbitmqCluster)control planeStatefulSet (operator)control-planerequired anti-affinity on hostname (operator default is preferred: override)maxUnavailable: 1 (operator)noquorum queues replicate: **same longhorn-local class as Galera (D5)**, same recovery (new empty PVC, the member re-syncs). nodeSelector goes through spec.override.statefulSet (no top-level field)
cert-managercontrol planeDeploymentscontrol-plane——no
Prometheus stack (if bundled)control planeStatefulSetcontrol-plane——node-exporter DaemonSet: hostNetwork, hostPID

2.2 OpenKeel services

ComponentRoleKindTolerationsAnti-affinity / spreadPDBHost / privNotes
Keystone APIcontrol planeDeployment (3)control-planebaselineminAvailable: 2noFernet / credential key rotation: CronJob on control plane
Placement APIcontrol planeDeployment (3)control-planebaselineminAvailable: 2no
Nova API (+ metadata API)control planeDeployment (3)control-planebaselineminAvailable: 2nothe metadata API is only reached through a per-node proxy (§ 3.4)
Nova conductorcontrol planeDeployment (2-3)control-planebaselinemaxUnavailable: 1noincludes the VM reconcilers (controller-runtime)
Nova schedulercontrol planeDeployment (2)control-planebaselinemaxUnavailable: 1nopins VMs with nodeSelector: kubernetes.io/hostname until they run (§ 4.4)
Nova console proxy (noVNC, serial)control plane (or network with ingress)Deployment (2)control-planebaselinemaxUnavailable: 1no
Nova compute agentcomputeDaemonSetcompute, openkeel.io/reserved——no host network; RBAC on its own Node and on VMIsone per hypervisor: registers the nova-compute service and the Placement resource provider, reports inventories and traits (§ 4.2)
Nova Ironic compute agentcontrol planeDeployment (1 active, Lease)control-plane——nopublishes Ironic nodes to Placement
KubeVirt virt-operator, virt-api, virt-controllercontrol planeDeploymentscontrol-planespread on hostnameoperator defaultnoKubeVirt.spec.infra.nodePlacement (§ 4.4)
KubeVirt virt-handlercomputeDaemonSetcompute, openkeel.io/reserved——privileged, hostPID, /dev/kvm, host pathsKubeVirt.spec.workloads.nodePlacement
virt-launcher (one per VM)compute (pinned by the Nova scheduler until it runs, § 4.4)Pod owned by the VMIcompute; openkeel.io/reserved only when Masakari picked the reserved hostserver-group (anti-)affinity on openkeel.io/server-groupper-VMI PDB created by KubeVirt for evictionStrategy: LiveMigrateno host network; /dev/kvm via device pluginpriority openkeel-vm; lives in os-<project_id>
CDI operator, controller, apiserver, uploadproxycontrol planeDeploymentscontrol-plane——noCDI.spec.infra
CDI importer / cloner / upload server podsstorage (or compute when hyperconverged)Podsstorage——noCDI.spec.workloads; close to the Longhorn replicas they write
Neutron APIcontrol planeDeployment (3)control-planebaselineminAvailable: 2no
Neutron backend worker (OVN NB / Kube-OVN reconciler, maintenance loop, hash ring)control planeDeployment (2-3)control-planebaselinemaxUnavailable: 1no
OpenKeel OVN central (NB, SB, northd) — mode B, decided (D2)control planeStatefulSet (3, RAFT)control-planerequired anti-affinity on hostnamemaxUnavailable: 1no host network; PVC per member on longhorn-local (RAFT replicates); ClusterIP Services 6641/6642 reached by the chassis (hostNetwork pods reach ClusterIPs)§ 3
Zun compute agentcontainersDaemonSetcontainers, openkeel.io/reserved——no host access; reads its Node and Podsone per container node: zun-compute service, compute_node row, Placement inventory when the node is not a hypervisor (openkeel.kubernetes.nova_compute_label)
Zun containers and capsules (one Pod each)containers (pinned to the node the scheduler picked)Podcontainers (openkeel.kubernetes.compute_toleration)——RuntimeClass kata (its scheduling selects the containers role); no token, no host namespaceslives in os-<project_id>
OpenKeel ovn-controller + ovs-vswitchd (tenant chassis)compute + containers (+ network for gateways)DaemonSetall——privileged, hostNetwork§ 3; openkeel-neutron.chassis.roles (node affinity, one term per role); coexistence with Kube-OVN's OVS is open (§ 7)
Neutron metadata agent (Go, D4)computeDaemonSetcompute——privileged: hostNetwork, CAP_NET_ADMIN + CAP_SYS_ADMIN (namespaces, veths), local OVS DB socket§ 3.4
ovn-bgp-agent + FRR (Python, kept)network + compute (distributed FIPs, D3)DaemonSetnetwork, compute——privileged (CAP_NET_ADMIN, CAP_SYS_ADMIN, CAP_DAC_OVERRIDE: reverse-spec-ovn-bgp-agent § 9), hostNetworkreads the OpenKeel OVN NB/SB (Neutron external_ids), only when the fabric speaks BGP
Public ingress (Gateway API data plane, D8)control plane + networkDaemonSet (or Deployment with node affinity on either role)control-plane, networkspread on hostnamemaxUnavailable: 1hostNetwork or a LoadBalancer VIP (MetalLB / BGP) announced from those nodesnodeAffinity with two nodeSelectorTerms (OR)
ovn-octavia-provider (Python, kept, inside the Octavia API)control planeDeploymentcontrol-planebaselineminAvailable: 1noneeds NB access only; OVN load balancers are distributed, no node role
Glance APIcontrol planeDeployment (3)control-planebaselineminAvailable: 2noimage PVCs in openkeel-images are Longhorn volumes placed by Longhorn on storage nodes
Cinder API, scheduler, volume, backupcontrol planeDeploymentscontrol-planebaselineAPI minAvailable: 2, workers maxUnavailable: 1nothey only call the Kubernetes API; the data path is Longhorn
Manila API, scheduler, sharecontrol planeDeploymentscontrol-planebaselineidemnothe share data path is Longhorn RWX share-manager pods (NFS) on storage nodes
Swift proxycontrol plane (or network/ingress at scale)Deployment (3+)control-planebaselineminAvailable: 2noobject traffic is bandwidth-heavy: give it its own nodes or the ingress nodes at scale
Swift storagestorageStatefulSet (one pod per "storage node")storagerequired anti-affinity on hostname, spread on zone (Swift zones = failure domains)maxUnavailable: 1nodevices: PVCs with numberOfReplicas: 1 + strict-local (swift-csi-block.md § 1)
Swift ring builder, expirer, reconcilercontrol planeDeployment (Lease)control-plane——no
Heat API, API-CFN, enginecontrol planeDeploymentscontrol-planebaselineidemno
ec2-apicontrol planeDeploymentcontrol-planebaselineminAvailable: 2no
Barbican API, worker, Keystone listenercontrol planeDeploymentscontrol-planebaselineidemnoa PCIe HSM would need its own label; network HSMs need egress only
Designate API, central, sinkcontrol planeDeploymentscontrol-planebaselineidemno
ExternalDNS (one per DNS backend)control planeDeployment (1)control-plane——nothe authoritative DNS servers (PowerDNS…) are outside this document; if run in-cluster, put them on network/ingress nodes with a LoadBalancer Service
Masakari API, enginecontrol planeDeploymentscontrol-planebaselineidemno
Medik8s NHC operator, Node Maintenance operatorcontrol planeDeploymentscontrol-plane——no
Self Node Remediation agentall nodesDaemonSetall——privileged, /dev/watchdoga node it cannot fence cannot be remediated (§ 5.6)
Ironic APIcontrol planeDeployment (3)control-planebaselineminAvailable: 2no
Ironic conductor (+ HTTP server; dnsmasq with iPXE, later)provisioning, dedicated node (D7)StatefulSetprovisioningrequired anti-affinity on hostnamemaxUnavailable: 1Multus macvlan/ipvlan on the provisioning VLAN (preferred) or hostNetwork (documented exception); dnsmasq needs NET_ADMIN, NET_RAWown namespace, egress only to the BMC network
Skyline API server + console (Python)control planeDeployment (2)control-planebaselineminAvailable: 1no

2.3 Platform operators and agents

ComponentRoleKindTolerationsHost / privHow it is placed
MariaDB Operator, RabbitMQ Cluster + Topology operators, infra-operatorcontrol planeDeploymentscontrol-planenonodeSelector / tolerations values of their charts
Longhorn managerevery node that consumes or stores volumes (all)DaemonSetallprivilegedlonghornManager.nodeSelector / tolerations (chart values [verify])
Longhorn instance managers, engine, replicasreplicas on storage (disks only there); engines where the volume is attachedmanaged podsallprivilegeddefaultSettings.createDefaultDiskLabeledNodes: true + label node.longhorn.io/create-default-disk=true on storage nodes [verify]; taintToleration setting
Longhorn CSI plugin (node)allDaemonSetallprivilegedfollows Longhorn's taintToleration
Longhorn UI, driver deployer, CSI attacher / provisioner / resizer / snapshottercontrol plane (or storage)Deploymentscontrol-planenosystemManagedComponentsNodeSelector [verify]
Longhorn RWX share managers (Manila)storagepodsstoragenosystemManagedComponentsNodeSelector
KubeVirt / CDIsee § 2.2nodePlacement
Kube-OVN controller, monitor, its own OVN central (pod network)control planeDeploymentscontrol-planehostNetwork [verify]Kube-OVN chart MASTER_NODES / kube-ovn/role=master [verify]
Kube-OVN kube-ovn-cni, ovs-ovn, pingerall nodesDaemonSetsallprivileged, hostNetworkas long as Kube-OVN is the primary CNI (open, § 7)
Kube-OVN speaker (BGP, mode A only)networkDaemonSetnetworkhostNetworknot deployed with the parallel OVN (D2): tenant IPs are announced by ovn-bgp-agent
MultusallDaemonSetallprivileged
SR-IOV network operator + device plugincompute with SR-IOV NICsDaemonSetcomputeprivilegedSriovNetworkNodePolicy.spec.nodeSelector on feature.node.kubernetes.io/network-sriov.capable=true + compute
Node Feature DiscoveryallDaemonSetallread-only host paths
ExternalDNS / External DNS Operatorcontrol planeDeploymentcontrol-planeno

3. Neutron / OVN specifics

3.1 What runs where

PieceMode A: Neutron on Kube-OVN (alternative)Mode B: parallel OVN (ML2/OVN-identical NB/SB) — decided (D2)
OVN NB / SB / northdKube-OVN's ovn-central on kube-ovn/role=master nodes = the OpenKeel control plane nodes (3, RAFT)OpenKeel ovn-central StatefulSet on control plane (3, RAFT, PVCs, ClusterIP Services on 6641/6642); Kube-OVN's own central (if Kube-OVN is the CNI) stays separate
Who writes the NBKube-OVN controller only; Neutron writes Kube-OVN CRDs (neutron-ovn.md)the Go Neutron, with Neutron external_ids (rows identical to Python ML2/OVN)
Chassis (ovn-controller + OVS)ovs-ovn DaemonSet on every node: control plane, compute and network nodes are all chassisOpenKeel ovn-controller DaemonSet on compute and network nodes only; VMs attach through a Multus secondary NIC
Gateway selectionKube-OVN external gateway: ConfigMap kube-system/ovn-external-gw-config (enable-external-gw: "true", type: centralized | distributed, external-gw-nodes, external-gw-nic) [verify on v1.16.15]; plus Vpc.spec.bfdPort.nodeSelector for BFD gateway ports; ProviderNetwork.spec.nodeSelector for the nodes wired to a physnetexternal_ids:ovn-cms-options=enable-chassis-as-gw on the chassis of nodes with the network role, set by the DaemonSet's init step from the Node labels; Neutron schedules router gateways (Gateway_Chassis / HA chassis group) on those chassis
BGPKube-OVN speaker on network nodesovn-bgp-agent + FRR (Python) on network nodes, and on compute nodes when distributed FIPs or provider-network VMs must be announced
Load balancersSwitchLBRule / VPC LB (partial)ovn-octavia-provider, distributed in OVN
Metadata§ 3.4§ 3.4

Decided (D2): mode B, a parallel OVN, for now. Mode A stays in this document as the alternative the Go Neutron's NetworkBackend interface keeps possible.

Two OVNs on one node (mode B on a Kube-OVN cluster) needs two OVS databases and integration bridges per node, which neutron-ovn.md calls fragile: both ovs-vswitchd instances share the one openvswitch kernel datapath module. Three ways out, still open (§ 7):

  1. a non-OVN primary CNI (Cilium, Calico): each hypervisor and gateway has one OVS/OVN stack, OpenKeel's (recommended);
  2. keep Kube-OVN as the CNI and run OpenKeel's ovn-controller against **Kube-OVN's ovs-vswitchd** with a second integration bridge (br-int-ok, ovn-bridge external ID) and its own SB remote [verify that one OVS can serve two ovn-controllers];
  3. keep Kube-OVN only on the nodes that run no tenant traffic (Talos control plane, OpenKeel control plane), which needs a different CNI on hypervisors and gateways, so in practice option 1.

3.2 Gateway chassis selection from the node label

The administrator never edits OVS external IDs by hand. The chassis DaemonSet (mode B) or a small OpenKeel node agent (mode A) reads its Node object (RBAC: get on its own node, the name from the downward API spec.nodeName; node labels are not exposed by the downward API) and derives:

Node labelMode B: OVS external_idsMode A: Kube-OVN
node-role.openkeel.io/networkovn-cms-options=enable-chassis-as-gwnode listed in external-gw-nodes; matched by the ProviderNetwork and bfdPort node selectors
topology.kubernetes.io/zone=<az>ovn-cms-options=…,availability-zones=<az> (Neutron router / network AZs)— (AZs handled by Neutron only)
network.openkeel.io/bridge-mappings=physnet1:br-exovn-bridge-mappingsProviderNetwork.spec.customInterfaces
network.openkeel.io/external-nic=bond1NIC added to br-exexternal-gw-nic / ProviderNetwork.spec.defaultInterface

A change of label is reconciled live (the agent watches its Node). Removing the network label from a node that hosts router gateways: Neutron moves the gateways (higher-priority Gateway_Chassis takes over) before the agent clears the CMS option, so the change is a planned failover, not an outage.

At least two network nodes for HA chassis groups (one active, one standby); three when BFD tie-breaking or a maintenance window must keep redundancy.

3.3 Distributed vs centralised floating IPs

CentralisedDistributed
Mode B setting[ovn] enable_distributed_floating_ip = false (ML2/OVN default)true
Mode A settingovn-external-gw-config type: centralizedtype: distributed [verify]
Where FIP traffic leavesthe active gateway chassis of the router (network nodes)the compute node that hosts the VM
Nodes that need the external NIC / physnet bridge mappingnetwork nodes onlynetwork and compute nodes (ProviderNetwork.spec.nodeSelector must include compute; ovn-bridge-mappings on every hypervisor)
SNAT (no FIP)gateway chassisstill the gateway chassis
ovn-bgp-agent placementnetwork nodesnetwork + compute nodes
Recommended forsmall and medium clusters, simple external cablinglarge clusters with north-south heavy VMs, when every rack has the external VLANs

Decided (D3): distributed (enable_distributed_floating_ip = true; Neutron chart value gateway.floatingIpMode: distributed). Every compute rack has the external VLANs: FIP traffic leaves from the VM's own hypervisor, the network nodes only carry SNAT (VMs without FIP) and router gateway traffic, and losing a gateway does not cut FIPs. The cost: the external bridge mapping (ovn-bridge-mappings) on every hypervisor, and, when BGP is used, ovn-bgp-agent on every hypervisor to announce the FIPs of its VMs. Centralised stays a supported setting.

3.4 Metadata service (169.254.169.254) — decided (D4)

VMs keep config drive / NoCloud from Nova, and also get the HTTP metadata service, built on the ML2/OVN design so that the OVN rows stay identical to Python (reverse-spec-neutron 03 § metadata, 05 § 5.4):

PieceWhoWhere
metadata port per network: LSP type: localport, device_owner: network:distributed, one IP per subnetthe Go Neutron server (NB write, as ovn_client.py:3168-3310)OpenKeel OVN NB
route to 169.254.169.254/32 via the metadata port IPOVN native DHCP (classless_static_route in DHCP_Options), as ML2/OVNin the VM, learnt by DHCP
**openkeel-neutron-metadata agent** (Go)DaemonSet, one per hypervisorcompute nodes
Nova metadata APIopenkeel-nova metadata Deployment, ClusterIP Service on 8775control plane

Request path:

sequenceDiagram
  participant VM as VM (tenant NIC on br-int)
  participant NS as ovnmeta-<net> namespace (veth, iface-id = metadata port)
  participant AG as openkeel-neutron-metadata (Go, DaemonSet)
  participant SB as OpenKeel OVN SB
  participant NM as Nova metadata API (ClusterIP :8775)
  VM->>NS: GET http://169.254.169.254/openstack/latest/meta_data.json
  NS->>AG: listener inside the namespace adds X-OVN-Network-ID, X-Forwarded-For
  AG->>SB: Port_Binding lookup (network, source IP) → port, device_id, project
  AG->>NM: GET + X-Instance-ID, X-Tenant-ID, X-Instance-ID-Signature (HMAC-SHA256)
  NM-->>VM: metadata (through the agent)
  • The agent provisions, per network that has a VIF bound to the local chassis, the namespace ovnmeta-<network id>, the veth pair, the addresses (169.254.169.254/32, the metadata port IP of each subnet) and the OVS port with external_ids:iface-id = <metadata port id>, with the same triggers as the Python agent (SB Port_Binding events, start-up sync, live-migration additional_chassis).
  • No haproxy: the Go agent opens one listener inside each namespace (a goroutine locked to an OS thread does setns, binds 169.254.169.254:80, and the socket stays in that namespace) and handles the request in-process. Same headers, same HMAC as Python, so Nova sees an identical request.
  • Privileges: hostNetwork, CAP_NET_ADMIN and CAP_SYS_ADMIN (namespaces, veths, addresses), hostPath to /run/netns (mount propagation Bidirectional) and to the OpenKeel OVS DB socket. No other host path.
  • metadata_proxy_shared_secret: one Secret, mounted by the agent and by the Nova metadata Deployment.
  • The agent reaches Nova through its ClusterIP Service from the host network namespace (kube-proxy or the CNI's service handling applies to hostNetwork pods).
  • Failure handling: the agent's DaemonSet is listed among the node's critical pods, so Masakari's process monitor cordons the node when it is not ready (masakari-kubevirt.md).
  • Mode A, if ever used: Kube-OVN has no equivalent, and this design does not apply as is.

4. Nova / KubeVirt specifics

4.1 Compute host = Kubernetes node

Nova conceptOpenKeel
compute host (services.host, instances.host)the Kubernetes node name
hypervisor (compute_nodes, os-hypervisors)the same node: hypervisor_hostname = node name, hypervisor_type = KubeVirt, uuid = the Node's metadata.uid
nova-compute servicethe compute agent pod of that node (DaemonSet); its heartbeat = the service updated_at; the agent stops → service down
a node without the compute rolenot a hypervisor: no agent, no service record, no resource provider, no VM can be placed there
openstack compute service set --disablethe agent sets Placement trait COMPUTE_STATUS_DISABLED (as Nova does) and adds the taint openkeel.io/compute-disabled=:NoSchedule; it does not cordon (cordon would also block DaemonSet updates and infra pods)
kubectl cordon by an administratorseen by the agent: service reported disabled with disabled_reason: "cordoned in Kubernetes"

4.2 Placement resource providers

The compute agent creates one root resource provider per compute node (name = node name, uuid = node UID) and keeps its inventories current:

Resource classSourceReserved
VCPUnode allocatable.cpu minus the CPUs of the static CPU manager poollive requests of the non-VM pods on the node (virt-handler, OVS, Longhorn, SNR, metadata agent…), so that Nova never sells what the infra needs; mandatory on hyperconverged nodes where Galera runs too (D6)
PCPUreservedSystemCPUs complement when the CPU manager is static and the node is labelled for dedicated CPUssame
MEMORY_MBallocatable.memory minus the hugepagesnon-VM pod requests + KubeVirt per-VM overhead estimate
MEMORY_MB (hugepages)allocatable.hugepages-1Gi / -2Mi, as a nested provider with trait CUSTOM_HUGEPAGES_1G / _2M—
DISK_GBnot on the compute node: root disks are Longhorn PVCs. One sharing provider per StorageClass and zone (trait MISC_SHARES_VIA_AGGREGATE) in a Placement aggregate with the zone's compute nodes; capacity from Longhorn schedulable spaceover-provisioning ratio of Longhorn
SRIOV_NET_VF, NET_BW_*SR-IOV device plugin allocatable, one nested provider per physical function—
PGPU, VGPU, CUSTOM_PCI_*KubeVirt permittedHostDevices + device plugin allocatable—

Dynamic reservation (D6). The agent watches the pods bound to its node and sets the inventories' reserved to the sum of the requests of every pod that is not a virt-launcher (plus systemReserved/kubeReserved, already outside allocatable). Rules that keep it safe:

  • the update only moves reserved, never total, so existing allocations stay valid; when reserved + used > total after a new infra pod lands, Placement refuses new claims on the node, which is the wanted effect, and the agent logs a WARNING with the pods involved;
  • changes are debounced (default 30 s) and sent only when the reservation moves by more than one VCPU or 256 MiB, so that short-lived Jobs do not flap the inventory;
  • pods in Succeeded/Failed phase are ignored; DaemonSet pods of the node count as soon as they are scheduled, before they run.

Traits: COMPUTE_* capability traits from the KubeVirt node labeller (cpu-feature.node.kubevirt.io/<f> → HW_CPU_X86_<F>), plus every trait.openkeel.io/<TRAIT> label copied as is. Allocation ratios (cpu_allocation_ratio, ram_allocation_ratio) come from the agent's YAML configuration, per node through an annotation override.

4.3 Host aggregates and availability zones

  • Availability zones come from topology.kubernetes.io/zone, which the infrastructure owns. Nova exposes one AZ per zone value: the agent puts its node in an aggregate az-<zone> with metadata availability_zone=<zone>, mirrored in Placement. os-aggregates calls that would move a host to another AZ are rejected with Nova's 400 for conflicting AZs, since the zone label is not Nova's to change.
  • Other host aggregates (flavor isolation, tenant isolation, hardware generations) keep Nova's API and database as the source of truth. On add_host / remove_host the Nova conductor patches the label aggregate.openkeel.io/<aggregate uuid> on the node, and mirrors the aggregate into Placement (limit_tenants_to_placement_aggregate, isolated_aggregate_filtering keep working through Placement's member_of). The label lets KubeVirt-side constraints, Masakari segments and operators see the membership with kubectl get nodes -l.
  • Masakari segments use their own label (openkeel.io/ha-segment), set by Masakari from segments/{id}/hosts.

4.4 Scheduling and KubeVirt node placement

  1. The Go scheduler gets allocation candidates from Placement, claims one, and writes on the VM template nodeSelector: {kubernetes.io/hostname: <node>}, the tolerations of the compute role, and priorityClassName: openkeel-vm.
  2. The Kubernetes scheduler only confirms (resources, taints); if it refuses (ErrorUnschedulable), the Nova conductor drops the claim and reschedules, as a RescheduledException would. The pin only lasts for the start. KubeVirt renders a migration target from the VMI and copies the VM's selector to it (vmRolloutStrategy: LiveUpdate), so a pinned guest could never leave its node on a drain. Once a server runs and has no task, the conductor's periodic pass replaces the pin with a preferredDuringScheduling node affinity on its host, for a restart KubeVirt decides by itself. Each start Nova asks for (start, reboot, resize) pins it again first, since its ports are bound to that host. Without a pin, a target goes where the Kubernetes scheduler sends it (nodes with virt-handler, tolerated taints), the conductor adopts the migration, and Placement confirms the node, or the CNI plugin holds the target until KubeVirt gives up. Availability zones and server groups do not constrain that choice: they apply at Nova's own scheduling only.
  3. KubeVirt's own components are placed by the KubeVirt CR (and CDI's by the CDI CR):
apiVersion: kubevirt.io/v1
kind: KubeVirt
metadata: {name: kubevirt, namespace: kubevirt}
spec:
  infra:                       # virt-api, virt-controller
    nodePlacement:
      nodeSelector: {node-role.openkeel.io/control-plane: ""}
      tolerations:
        - {key: node-role.openkeel.io/control-plane, operator: Exists, effect: NoSchedule}
  workloads:                   # virt-handler (and where VMIs may run)
    nodePlacement:
      nodeSelector: {node-role.openkeel.io/compute: ""}
      tolerations:
        - {key: node-role.openkeel.io/compute, operator: Exists, effect: NoSchedule}
        - {key: openkeel.io/reserved, operator: Exists, effect: NoSchedule}
  configuration:
    migrations: {parallelMigrationsPerCluster: 5, parallelOutboundMigrationsPerNode: 2}
    evictionStrategy: LiveMigrate
---
apiVersion: cdi.kubevirt.io/v1beta1
kind: CDI
metadata: {name: cdi}
spec:
  infra:
    nodeSelector: {node-role.openkeel.io/control-plane: ""}
    tolerations: [{key: node-role.openkeel.io/control-plane, operator: Exists, effect: NoSchedule}]
  workloads:                   # importer, cloner, upload server pods
    nodeSelector: {node-role.openkeel.io/storage: ""}
    tolerations: [{key: node-role.openkeel.io/storage, operator: Exists, effect: NoSchedule}]

spec.workloads.nodePlacement restricts virt-handler; a VMI that lands on a node without virt-handler never starts. Since the Nova scheduler only chooses nodes that have a resource provider, and only compute nodes have one, the two stay consistent.


5. How it is deployed

5.1 Values layout of openkeel-chart

Helm passes global to every subchart, so the role definitions live once in global.nodeRoles; each component names a role, and may override any field.

global:
  nodeRoles:
    taints: false                       # true: dedicated nodes are tainted; charts already tolerate their role
    roles:
      controlPlane:
        label: node-role.openkeel.io/control-plane     # production (D1): node-restriction.kubernetes.io/openkeel-control-plane
        taint: {key: node-role.openkeel.io/control-plane, effect: NoSchedule}
      compute:
        label: node-role.openkeel.io/compute
        taint: {key: node-role.openkeel.io/compute, effect: NoSchedule}
      network:
        label: node-role.openkeel.io/network
        taint: {key: node-role.openkeel.io/network, effect: NoSchedule}
      storage:
        label: node-role.openkeel.io/storage
        taint: {key: node-role.openkeel.io/storage, effect: NoSchedule}
      containers:
        label: node-role.openkeel.io/containers
        taint: {key: node-role.openkeel.io/containers, effect: NoSchedule}
      provisioning:
        label: node-role.openkeel.io/provisioning
        taint: {key: node-role.openkeel.io/provisioning, effect: NoSchedule}
  placementDefaults:
    zoneKey: topology.kubernetes.io/zone
    zoneSpread: ScheduleAnyway
    hostSpread: DoNotSchedule           # relaxed to ScheduleAnyway when replicas > nodes of the role
    priorityClasses:
      infra: openkeel-infra-critical
      control: openkeel-control
      vm: openkeel-vm

priorityClasses:
  create: true                          # openkeel-chart renders the three PriorityClasses

infrastructure:
  mariadb:
    placement: {role: controlPlane, antiAffinity: required}
  rabbitmq:
    placement: {role: controlPlane, antiAffinity: required}

# Per-subchart overrides: <subchart>.placement.<component>
openkeel-nova:
  placement:
    api:          {role: controlPlane}
    conductor:    {role: controlPlane}
    scheduler:    {role: controlPlane}
    consoleProxy: {role: controlPlane}
    computeAgent: {role: compute, extraTolerations: [{key: openkeel.io/reserved, operator: Exists, effect: NoSchedule}]}
openkeel-neutron:
  backend: ovn-nb                       # ovn-nb (decided, D2) | kube-ovn
  placement:
    api:     {role: controlPlane}
    worker:  {role: controlPlane}
    ovnCentral: {role: controlPlane, antiAffinity: required}       # ovn-nb only
    chassis:    {roles: [compute, network]}                         # ovn-nb only (DaemonSet)
    metadataAgent: {role: compute, enabled: true}                   # 169.254.169.254 (D4)
    bgpAgent:   {roles: [network, compute], enabled: false}         # ovn-bgp-agent, when the fabric speaks BGP
  gateway:
    floatingIpMode: distributed         # decided (D3); centralized | distributed
openkeel-chart:
  ingress:                              # public Gateway API data plane (D8)
    placement: {roles: [controlPlane, network]}
openkeel-swift:
  placement:
    proxy:   {role: controlPlane}
    storage: {role: storage, antiAffinity: required}
openkeel-ironic:
  placement:
    api:       {role: controlPlane}
    conductor: {role: provisioning, antiAffinity: required}
openkeel-keystone:
  placement:
    api:
      role: controlPlane
      # any raw field wins over what the role renders:
      nodeSelector: {node-role.openkeel.io/control-plane: "", topology.kubernetes.io/zone: zone-a}

Precedence, highest first: raw fields of the component (nodeSelector, tolerations, affinity, topologySpreadConstraints, priorityClassName) → the component's role / roles → the chart's default role (control plane for every API and worker, documented per chart).

roles: [a, b] (DaemonSets spanning roles) renders a nodeAffinity with one nodeSelectorTerm per role (OR), and the tolerations of all listed roles.

5.2 Chart template change (implemented 2026-11-02)

Every project chart has one helper, shared through the template rather than a library chart (the project charts stay standalone). As implemented, it renders nodeSelector, tolerations and priorityClassName; spread and affinity stay in each template:

{{- /* openkeel.placement: renders nodeSelector/affinity/tolerations/spread/priority for one component */ -}}
{{- define "openkeel.placement" -}}
{{- $g := .root.Values.global.nodeRoles | default dict -}}
{{- $p := .placement | default dict -}}
{{- $role := get ($g.roles | default dict) ($p.role | default .defaultRole) -}}
{{- if $p.nodeSelector }}
nodeSelector: {{- toYaml $p.nodeSelector | nindent 2 }}
{{- else if $role }}
nodeSelector:
  {{ $role.label | quote }}: ""
{{- end }}
tolerations:
{{- if and $role $role.taint }}
  - {key: {{ $role.taint.key | quote }}, operator: Exists, effect: {{ $role.taint.effect }}}
{{- end }}
{{- with $p.extraTolerations }}{{ toYaml . | nindent 2 }}{{ end }}
{{- /* topologySpreadConstraints and affinity follow the same pattern */ -}}
{{- end -}}

Tolerations are always rendered for the component's role, whether or not global.nodeRoles.taints is true, so tainting nodes later needs no redeploy. Without global.nodeRoles (a project chart installed alone), the helper renders nothing and the chart behaves as today.

openkeel-chart additionally renders the operators' CRs with the same roles: MariaDB.spec.{nodeSelector,tolerations,affinity,priorityClassName}, and RabbitmqCluster.spec.{affinity,tolerations} (+ override.statefulSet for the node selector).

What is implemented (2026-11-02):

ChartComponents placed by the helper (placement.<key>, default role)Not placed by role
openkeel-chart (infrastructure)MariaDB CR (nodeSelector, tolerations), RabbitmqCluster (affinity.nodeAffinity on the label plus the required host anti-affinity of § 2.1, and tolerations); infrastructure.<name>.placement, control plane
openkeel-keystone, -placement, -glance, -neutron-fwaas, -ovn-octavia-provider, -skylineAPI Deployment and its Jobs (placement.api), control plane
openkeel-barbicanAPI and Jobs (api), worker and Keystone listener (worker), control plane
openkeel-cinderAPI and Jobs (api), scheduler, volume and backup (workers), control plane
openkeel-masakariAPI and engine (their nodePlacement.<x>.nodeSelector wins, its tolerations are added), migration Job (api), control plane
openkeel-neutronAPI and Jobs (api), OVN central (its ovnCentral.nodeSelector wins; the default is now empty, it was the lab key), control planethe OVN chassis DaemonSet: chassis.nodeSelector, tolerates every taint
openkeel-neutron-vpnaasAPI and Jobs (api), control planethe agent DaemonSet follows the chassis (agent.nodeSelector)
openkeel-novaAPI and migration Job (api), conductor, console proxy, Ironic agent: control plane; compute agent DaemonSet: compute
openkeel-ironicAPI and Jobs: control plane; conductor StatefulSet: provisioning, with required anti-affinity on the host

What okadm sets. In production, okadm install and okadm upgrade pass global.nodeRoles to every release (the infrastructure release and one per component), with the role labels okadm plan checks:

global:
  nodeRoles:
    taints: false
    roles:
      controlPlane: {label: node-restriction.kubernetes.io/openkeel-control-plane, taint: {key: node-role.openkeel.io/control-plane, effect: NoSchedule}}
      compute:      {label: node-restriction.kubernetes.io/openkeel-compute,       taint: {key: node-role.openkeel.io/compute, effect: NoSchedule}}
      network:      {label: node-restriction.kubernetes.io/openkeel-network,       taint: {key: node-role.openkeel.io/network, effect: NoSchedule}}
      storage:      {label: node-restriction.kubernetes.io/openkeel-storage,       taint: {key: node-role.openkeel.io/storage, effect: NoSchedule}}
      containers:   {label: node-restriction.kubernetes.io/openkeel-containers,    taint: {key: node-role.openkeel.io/containers, effect: NoSchedule}}
      provisioning: {label: node-restriction.kubernetes.io/openkeel-provisioning,  taint: {key: node-role.openkeel.io/provisioning, effect: NoSchedule}}

The label keys are the hardened ones (D1); the taint keys stay those of § 1.2 in every mode, since the NodeRestriction admission plugin restricts labels, not taints. It also sets Nova's config.kubernetes.compute_role_label to the compute label, so the driver and the compute agent find the same hypervisors. A global.nodeRoles in the user's values is merged over these (and is the only source in lab mode, where okadm passes none: role labels are optional there and a lab node usually carries none).

What okadm plan requires in production, now that the charts enforce the roles: 3 control plane nodes (Galera, RabbitMQ's required anti-affinity, OVN central), 1 compute node with Nova, 1 network node with Neutron, storage recommended, and one provisioning node per Ironic conductor (openkeel-ironic.conductor.replicas, 2 by default: required anti-affinity). Fewer provisioning nodes fail the plan; the remediation offers to label more nodes or to run as many conductors as there are provisioning nodes.

5.3 Platform components (installed before openkeel-chart)

These are installed from their own charts (openkeel-chart.md, operators section). A companion values file per component, under openkeel-chart/platform/ [proposal], keeps their placement consistent with the roles:

# platform/longhorn-values.yaml
defaultSettings:
  createDefaultDiskLabeledNodes: true          # only nodes labelled node.longhorn.io/create-default-disk=true get disks
  systemManagedComponentsNodeSelector: "node-role.openkeel.io/control-plane:"
  taintToleration: "node-role.openkeel.io/control-plane:NoSchedule;node-role.openkeel.io/compute:NoSchedule;node-role.openkeel.io/storage:NoSchedule;node-role.openkeel.io/network:NoSchedule"
  storageNetwork: longhorn-system/storage-net  # Multus NAD, optional
# [verify every key on the deployed Longhorn version]

# platform/kube-ovn-values.yaml  (mode A, or Kube-OVN as plain CNI)
MASTER_NODES_LABEL: "kube-ovn/role=master"     # put on the control plane nodes [verify chart key]

# platform/mariadb-operator-values.yaml, rabbitmq-*-values.yaml, infra-operator-values.yaml
nodeSelector: {node-role.openkeel.io/control-plane: ""}
tolerations: [{key: node-role.openkeel.io/control-plane, operator: Exists, effect: NoSchedule}]

Plus the KubeVirt and CDI CRs of § 4.4, the Medik8s NodeHealthCheck per Masakari segment (created by Masakari) and one for the control plane (§ 5.6).

5.4 Labelling the nodes

Manually (kubeadm, bare metal, labs):

# control plane (kube-ovn/role=master: Kube-OVN's own central, while Kube-OVN is the CNI)
kubectl label node cp-1 cp-2 cp-3 node-role.openkeel.io/control-plane= kube-ovn/role=master
# hypervisors
kubectl label node cmp-01 cmp-02 cmp-03 node-role.openkeel.io/compute=
# gateways, with their external wiring
kubectl label node net-1 net-2 node-role.openkeel.io/network= network.openkeel.io/external-nic=bond1
# Zun container nodes (Kata)
kubectl label node kata-1 kata-2 node-role.openkeel.io/containers=
# storage
kubectl label node sto-1 sto-2 sto-3 node-role.openkeel.io/storage= node.longhorn.io/create-default-disk=true
# zones (if the infrastructure does not set them)
kubectl label node cmp-01 topology.kubernetes.io/zone=zone-a
# taints, on dedicated nodes only (global.nodeRoles.taints: true)
kubectl taint node cmp-01 cmp-02 cmp-03 node-role.openkeel.io/compute=:NoSchedule
kubectl taint node net-1 net-2 node-role.openkeel.io/network=:NoSchedule

**At node registration (kubeadm JoinConfiguration, also used by CAPI's KubeadmConfigTemplate):**

nodeRegistration:
  kubeletExtraArgs:
    - {name: node-labels, value: "node-role.openkeel.io/compute="}
  taints:
    - {key: node-role.openkeel.io/compute, effect: NoSchedule}

This works for the default keys only (labs); the kubelet cannot self-set the hardened node-restriction.kubernetes.io/* keys used in production (D1). Taints can still come from registration.

Talos Linux. Taints come from machine.nodeTaints and kubelet settings from machine.kubelet in each role's machine config patch. Hardened role labels cannot be put in machine.nodeLabels (applied with the node's own identity [verify on the Talos version]): set them with kubectl label after the node joins, or through Cluster API (Sidero / CAPI Talos providers) from the Machine template.

# production labels (D1), by an administrator
kubectl label node ok-cp-1 ok-cp-2 ok-cp-3 node-restriction.kubernetes.io/openkeel-control-plane=
kubectl label node ok-net-1 ok-net-2 ok-net-3 node-restriction.kubernetes.io/openkeel-network=
kubectl label node ok-prov-1 node-restriction.kubernetes.io/openkeel-provisioning=

With Cluster API, one MachineDeployment (machine pool) per role, hardened keys carried by the Machine template and synced to the Node by CAPI:

apiVersion: cluster.x-k8s.io/v1beta1
kind: MachineDeployment
metadata: {name: openkeel-compute-zone-a}
spec:
  replicas: 8
  template:
    metadata:
      labels:
        node-restriction.kubernetes.io/openkeel-compute: ""   # synced to the Node by CAPI
    spec:
      failureDomain: zone-a                                     # becomes topology.kubernetes.io/zone via the provider [verify]
      bootstrap: {configRef: {kind: KubeadmConfigTemplate, name: openkeel-compute}}   # taints, hugepages kernel args, CPU manager
      infrastructureRef: {kind: Metal3MachineTemplate, name: openkeel-compute}

The hypervisor-specific kubelet settings (cpuManagerPolicy: static, topologyManagerPolicy, reservedSystemCPUs, systemReserved) belong to that role's KubeadmConfigTemplate, so that a node gets them with its role.

5.5 Day 2: adding, draining and removing a hypervisor

Add

  1. Provision the node with the compute role (scale the MachineDeployment, or kubeadm join + labels), kernel arguments for hugepages and IOMMU, kubelet CPU manager settings.
  2. DaemonSets start by themselves: CNI/OVS (chassis registered in OVN SB), virt-handler (node becomes kubevirt.io/schedulable=true), Longhorn, SNR, the Nova compute agent.
  3. The agent registers the nova-compute service and the resource provider. With Nova's enable_new_services (default true) the host is schedulable at once; set it to false to add hosts disabled and enable them after a smoke test (openstack compute service set --enable).
  4. Add the host to its aggregates (openstack aggregate add host, which labels the node) and to its Masakari segment (openstack segment host create, which labels it openkeel.io/ha-segment).

Drain (maintenance)

  1. openstack segment host update --on_maintenance True so that Masakari ignores the coming events (or create a Medik8s NodeMaintenance CR, which Masakari also reads as maintenance).
  2. openstack compute service set --disable --disable-reason maintenance <node>: no new VMs.
  3. kubectl drain <node> --ignore-daemonsets --delete-emptydir-data: KubeVirt turns the eviction of each virt-launcher pod into a live migration (evictionStrategy: LiveMigrate; the per-VMI PDB blocks plain eviction). VMs that cannot migrate (RWO volumes, host devices) follow their strategy (LiveMigrateIfPossible: restart elsewhere; External: left to Nova, which cold-migrates them). Each move is recorded by Masakari as a vmove with payload.reason: maintenance.
  4. After the work: kubectl uncordon, re-enable the service, clear on_maintenance.

Remove

  1. Drain as above.
  2. openstack compute service delete <id>: refused while the resource provider has allocations (Nova/Placement behaviour), then the agent deletes the provider.
  3. Remove the role label (the DaemonSets leave), kubectl delete node, scale the MachineDeployment down.

5.6 Failure: Masakari and Medik8s

sequenceDiagram
  participant N as compute node cmp-07
  participant NHC as NodeHealthCheck (segment S)
  participant SNR as Self Node Remediation
  participant M as Go Masakari
  participant NV as Go Nova
  N--xNHC: Ready=Unknown for 300 s
  NHC->>SNR: SelfNodeRemediation cmp-07 (strategy OutOfServiceTaint)
  SNR->>N: watchdog reboot (fencing)
  SNR->>N: taint node.kubernetes.io/out-of-service
  M->>M: notification COMPUTE_HOST STOPPED (only once fenced)
  M->>NV: evacuate HA VMs of cmp-07 within segment S (auto / reserved_host)
  NV->>NV: new Placement claims, VMs pinned to healthy segment nodes

Placement consequences:

  • **One NodeHealthCheck per Masakari segment**, selecting openkeel.io/ha-segment=<uuid> (always within the compute role). maxUnhealthy stops remediation storms (masakari-kubevirt.md).
  • **A separate NodeHealthCheck for the control plane**, without Masakari, with a stricter maxUnhealthy: 1 so that a Galera or RabbitMQ quorum is never fenced away; Medik8s is control-plane aware [verify on v0.12.0]. Network and storage nodes get their own NHC too: fencing a gateway triggers an OVN gateway failover, fencing a storage node a Longhorn / Swift rebuild.
  • Reserved hosts carry the taint openkeel.io/reserved=true:NoSchedule; only virt-handler, the compute agent and the DaemonSets tolerate it, so the node stays empty until Masakari removes the taint.
  • The SNR agent must run on every node it may fence, including tainted ones (it tolerates everything).

6. Reference topologies

flowchart TB
  subgraph S["Small: 3 nodes, hyperconverged, no taints"]
    direction LR
    s1["node-1<br/>k8s CP + etcd<br/>control-plane, compute,<br/>network, storage"]
    s2["node-2<br/>same roles"]
    s3["node-3<br/>same roles"]
  end
  subgraph M["Medium: 3 control + N compute + 2 network"]
    direction LR
    m1["cp-1..3<br/>k8s CP + etcd<br/>control-plane<br/>(Galera, RabbitMQ, OVN central, APIs)"]
    m2["cmp-01..N<br/>compute + storage<br/>(VMs, Longhorn disks, Swift storage)<br/>tainted compute"]
    m3["net-1..2<br/>network<br/>(gateway chassis, BGP)<br/>tainted"]
  end
  subgraph L["Large: dedicated everything, 3 zones"]
    direction LR
    l0["k8s-cp-1..3<br/>etcd + kube-apiserver only"]
    l1["ok-cp-1..6 (2/zone)<br/>control-plane"]
    l2["cmp-zone-a/b/c<br/>compute (+ distributed FIP path)"]
    l3["net-1..3 (1/zone)<br/>network + ingress"]
    l4["sto-1..9 (3/zone)<br/>storage: Longhorn, Swift"]
    l5["prov-1..2<br/>provisioning (Ironic)"]
  end
  m1 --- m2
  m1 --- m3
  l1 --- l2
  l1 --- l3
  l1 --- l4
  l1 --- l5

6.1 Small: 3-node hyperconverged (lab, edge, PoC)

  • Every node carries every role and kube-ovn/role=master; global.nodeRoles.taints: false.
  • Galera, RabbitMQ, OVN central: 3 replicas, one per node (required anti-affinity holds with exactly 3 nodes; one node down leaves quorum, two down does not).
  • Longhorn: 3 replicas for VM, image and Cinder volumes; Galera / RabbitMQ / Swift volumes with 1 replica strict-local (they replicate themselves). Swift: 3 pods, replication policy with 3 replicas, one per node.
  • Floating IPs: distributed (D3); all 3 nodes are also gateway chassis for SNAT (HA chassis group of 3).
  • Placement reserved on each node = everything the infra requests (§ 4.2): budget roughly 8 cores / 32 GiB per node for OpenKeel + platform before any VM.
  • Masakari: one segment of 3 nodes, maxUnhealthy: 1; live migration needs Longhorn RWX (migratable) volumes.
  • No Ironic, or the conductor on one node with its taint and a provisioning VLAN.

6.2 Medium: 3 control + N compute + 2 network

  • Control plane nodes also host the Kubernetes control plane; label control-plane, taint optional (needed only if generic workloads run in the cluster).
  • Compute nodes are also storage nodes (Longhorn data locality best-effort for VM disks); taint compute.
  • 2 network nodes (3 recommended), tainted, with the external NICs; distributed floating IPs (compute racks have the external VLANs, D3), so the network nodes carry SNAT and router gateways; ovn-bgp-agent on network and compute nodes if the fabric speaks BGP.
  • One dedicated provisioning node for the Ironic conductor (D7).
  • Masakari segments per rack or zone; one reserved host per segment if reserved_host is used.
  • Public ingress (Gateway API) on the network and control plane nodes (D8), behind a LoadBalancer VIP (MetalLB, or BGP via the network nodes).

6.3 Large: dedicated storage and network, 3 zones

  • Kubernetes control plane alone on 3 nodes (etcd latency is not shared with Galera).
  • OpenKeel control plane on 6+ nodes spread over 3 zones; Galera 3 (or 5) members, one per zone; RabbitMQ 3, one per zone; APIs scaled by HPA.
  • Compute per zone, one MachineDeployment per zone and hardware class; host aggregates for hardware generations, GPU and SR-IOV pools.
  • Network nodes per zone (gateway chassis carry availability-zones=<zone>), distributed floating IPs when every rack has the external VLANs, ovn-bgp-agent on network and compute nodes.
  • Storage nodes per zone: Longhorn (block for Cinder, Glance, VM disks) and Swift capacity nodes (separate StorageClass on HDD, Swift zones = Kubernetes zones), dedicated storage network.
  • Provisioning nodes, one pair per provisioning network, in their own namespace.
  • Hardened label keys (§ 1.1), with labels coming only from Cluster API.

7. Open questions

Answered on 2026-10-29: label keys, network mode, distributed floating IPs, metadata service, Galera storage, capacity reservation, Ironic, ingress (§ 0). Still open:

  1. Primary CNI with the parallel OVN (D2): replace Kube-OVN by a non-OVN CNI (Cilium, Calico) so that each node runs one OVS/OVN stack (recommended), or keep Kube-OVN and share its ovs-vswitchd with a second integration bridge (§ 3.1, to be validated)?
  2. Aggregates and AZs: AZs read-only from topology.kubernetes.io/zone (Nova rejects AZ changes through os-aggregates), other aggregates owned by Nova and mirrored to node labels: is this split acceptable?
  3. Galera / RabbitMQ recovery with strict-local volumes (D5): when a control plane node is lost for good, the member's PVC must be deleted to be recreated elsewhere. Automate it (an OpenKeel controller reacting to a deleted Node), or keep it a documented manual step?
  4. Hardened labels on Talos (D1): labels applied by an administrator after join, or Talos machines managed through Cluster API so that labels come from Machine templates?
  5. Upstream keys to confirm on the deployed versions (marked [verify]): Kube-OVN master-node label, Longhorn settings, CAPI label sync prefixes, Talos machine.nodeLabels identity, Medik8s control-plane handling, one OVS serving two ovn-controllers.

Source: openkeel-docs/NODE-PLACEMENT.md