OpenKeel: la soluzione Cloud sovrana, francese e di portata europea, del gruppo inherent Scopri inherent
Risorse
Documento di progettazione: dove collocare ogni componente OpenKeel su un cluster Talos.
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.
| # | Decision | Where it applies |
|---|---|---|
| D1 | Production 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 |
| D2 | Neutron 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 |
| D3 | Distributed 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 |
| D4 | The HTTP metadata service at 169.254.169.254 is offered, in addition to config drive / NoCloud | § 3.4 |
| D5 | Galera 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 |
| D6 | Placement inventories subtract the live requests of the non-VM pods of each compute node (dynamic reservation) | § 4.2 |
| D7 | Ironic conductor on a dedicated provisioning node | § 1.4, § 2.2 |
| D8 | Public 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:
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;openkeel-chart/ and openkeel-{keystone,placement,cinder,glance}/deploy/helm/ (they set topologySpreadConstraints only; no node selector, toleration or priority class yet);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);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.
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:
| Profile | Key | Who can set it | Synced by Cluster API from Machine labels |
|---|---|---|---|
| default | node-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]) |
| hardened | node-restriction.kubernetes.io/openkeel-<role> | the administrator or Cluster API only: the NodeRestriction admission plugin forbids kubelets to set this prefix | yes |
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):
| Label | Set by | Meaning |
|---|---|---|
topology.kubernetes.io/zone, topology.kubernetes.io/region | infrastructure (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>: "" | administrator | extra Placement traits for the node's resource provider (§ 4.2) |
network.openkeel.io/bridge-mappings, network.openkeel.io/external-nic | administrator | per-node OVN physical wiring (§ 3) |
kube-ovn/role=master | administrator (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 Discovery | hardware capabilities, turned into Placement traits |
| Role | Label (default key) | Taint when dedicated | What runs there |
|---|---|---|---|
| control plane | node-role.openkeel.io/control-plane | node-role.openkeel.io/control-plane=:NoSchedule | every 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/compute | node-role.openkeel.io/compute=:NoSchedule | virt-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/network | node-role.openkeel.io/network=:NoSchedule | OpenKeel 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/containers | node-role.openkeel.io/containers=:NoSchedule | the 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 |
| storage | node-role.openkeel.io/storage | node-role.openkeel.io/storage=:NoSchedule | Longhorn disks and replicas, Longhorn RWX share managers (Manila), the Swift storage StatefulSet, CDI import/clone pods |
| provisioning (bare metal, optional) | node-role.openkeel.io/provisioning | node-role.openkeel.io/provisioning=:NoSchedule always | openkeel-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).
| Role | CPU / RAM | Disks | NICs | Kernel / kubelet |
|---|---|---|---|---|
| control plane | 16-32 cores, 64-128 GiB; Galera and RabbitMQ dominate | NVMe for Galera, RabbitMQ and (when shared) etcd; 200-500 GiB | 2 × 10-25 GbE bonded | nothing special |
| compute | sized 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 SNR | small OS disk; VM disks come from Longhorn | 2 × 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 |
| network | 8-16 cores, 32 GiB (OVS datapath, conntrack) | OS only | 2 × 25-100 GbE: one bond to the tenant overlay, one bond to the provider / external VLANs; BGP-facing links if ovn-bgp-agent | DPDK optional; large conntrack tables |
| storage | 8-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 |
| provisioning | 4-8 cores, 16 GiB | 200 GiB+ image cache | access port on the provisioning VLAN, route to the BMC network | nothing special |
| Combination | Small (3 nodes) | Medium | Large |
|---|---|---|---|
| control plane + compute | yes (hyperconverged) | no: VMs and Galera compete for memory and disk I/O | no |
| control plane + network | yes | possible if control plane nodes have the external NICs; otherwise separate | no |
| control plane + storage | yes | yes, when Longhorn runs on the control plane disks | no |
| compute + network | yes | acceptable 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 gateways | no, except the compute nodes' own distributed FIP path |
| compute + storage | yes | recommended (hyperconverged storage: Longhorn data locality keeps VM disks local) | separate when storage is large or Swift has its own capacity nodes |
| compute + containers | yes | yes: 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 + anything | no: 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 node | dedicated 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.
Conventions:
nodeSelector on the role label, through the chart helper of § 5). "all" means every node that runs VMs or volumes.kubernetes.md: topologySpreadConstraints on topology.kubernetes.io/zone (ScheduleAnyway) and on kubernetes.io/hostname (DoNotSchedule when replicas ≤ nodes of the role, else ScheduleAnyway).minAvailable: 2 (3 replicas); Lease-elected workers maxUnavailable: 1; quorum StatefulSets maxUnavailable: 1.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 |hostNetwork and privileges. "no" means the kubernetes.md baseline (non-root, read-only root FS, all capabilities dropped).| Component | Role | Kind | Tolerations | Anti-affinity / spread | PDB | Host / priv | Notes |
|---|---|---|---|---|---|---|---|
MariaDB Galera (MariaDB CR) | control plane | StatefulSet (operator) | control-plane | required anti-affinity on hostname; spread on zone | maxUnavailable: 1 | no | **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 plane | StatefulSet (operator) | control-plane | required anti-affinity on hostname (operator default is preferred: override) | maxUnavailable: 1 (operator) | no | quorum 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-manager | control plane | Deployments | control-plane | — | — | no | |
| Prometheus stack (if bundled) | control plane | StatefulSet | control-plane | — | — | node-exporter DaemonSet: hostNetwork, hostPID |
| Component | Role | Kind | Tolerations | Anti-affinity / spread | PDB | Host / priv | Notes |
|---|---|---|---|---|---|---|---|
| Keystone API | control plane | Deployment (3) | control-plane | baseline | minAvailable: 2 | no | Fernet / credential key rotation: CronJob on control plane |
| Placement API | control plane | Deployment (3) | control-plane | baseline | minAvailable: 2 | no | |
| Nova API (+ metadata API) | control plane | Deployment (3) | control-plane | baseline | minAvailable: 2 | no | the metadata API is only reached through a per-node proxy (§ 3.4) |
| Nova conductor | control plane | Deployment (2-3) | control-plane | baseline | maxUnavailable: 1 | no | includes the VM reconcilers (controller-runtime) |
| Nova scheduler | control plane | Deployment (2) | control-plane | baseline | maxUnavailable: 1 | no | pins 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-plane | baseline | maxUnavailable: 1 | no | |
| Nova compute agent | compute | DaemonSet | compute, openkeel.io/reserved | — | — | no host network; RBAC on its own Node and on VMIs | one per hypervisor: registers the nova-compute service and the Placement resource provider, reports inventories and traits (§ 4.2) |
| Nova Ironic compute agent | control plane | Deployment (1 active, Lease) | control-plane | — | — | no | publishes Ironic nodes to Placement |
KubeVirt virt-operator, virt-api, virt-controller | control plane | Deployments | control-plane | spread on hostname | operator default | no | KubeVirt.spec.infra.nodePlacement (§ 4.4) |
KubeVirt virt-handler | compute | DaemonSet | compute, openkeel.io/reserved | — | — | privileged, hostPID, /dev/kvm, host paths | KubeVirt.spec.workloads.nodePlacement |
virt-launcher (one per VM) | compute (pinned by the Nova scheduler until it runs, § 4.4) | Pod owned by the VMI | compute; openkeel.io/reserved only when Masakari picked the reserved host | server-group (anti-)affinity on openkeel.io/server-group | per-VMI PDB created by KubeVirt for evictionStrategy: LiveMigrate | no host network; /dev/kvm via device plugin | priority openkeel-vm; lives in os-<project_id> |
| CDI operator, controller, apiserver, uploadproxy | control plane | Deployments | control-plane | — | — | no | CDI.spec.infra |
| CDI importer / cloner / upload server pods | storage (or compute when hyperconverged) | Pods | storage | — | — | no | CDI.spec.workloads; close to the Longhorn replicas they write |
| Neutron API | control plane | Deployment (3) | control-plane | baseline | minAvailable: 2 | no | |
| Neutron backend worker (OVN NB / Kube-OVN reconciler, maintenance loop, hash ring) | control plane | Deployment (2-3) | control-plane | baseline | maxUnavailable: 1 | no | |
| OpenKeel OVN central (NB, SB, northd) — mode B, decided (D2) | control plane | StatefulSet (3, RAFT) | control-plane | required anti-affinity on hostname | maxUnavailable: 1 | no 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 agent | containers | DaemonSet | containers, openkeel.io/reserved | — | — | no host access; reads its Node and Pods | one 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) | Pod | containers (openkeel.kubernetes.compute_toleration) | — | — | RuntimeClass kata (its scheduling selects the containers role); no token, no host namespaces | lives in os-<project_id> |
OpenKeel ovn-controller + ovs-vswitchd (tenant chassis) | compute + containers (+ network for gateways) | DaemonSet | all | — | — | 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) | compute | DaemonSet | compute | — | — | 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) | DaemonSet | network, compute | — | — | privileged (CAP_NET_ADMIN, CAP_SYS_ADMIN, CAP_DAC_OVERRIDE: reverse-spec-ovn-bgp-agent § 9), hostNetwork | reads the OpenKeel OVN NB/SB (Neutron external_ids), only when the fabric speaks BGP |
| Public ingress (Gateway API data plane, D8) | control plane + network | DaemonSet (or Deployment with node affinity on either role) | control-plane, network | spread on hostname | maxUnavailable: 1 | hostNetwork or a LoadBalancer VIP (MetalLB / BGP) announced from those nodes | nodeAffinity with two nodeSelectorTerms (OR) |
| ovn-octavia-provider (Python, kept, inside the Octavia API) | control plane | Deployment | control-plane | baseline | minAvailable: 1 | no | needs NB access only; OVN load balancers are distributed, no node role |
| Glance API | control plane | Deployment (3) | control-plane | baseline | minAvailable: 2 | no | image PVCs in openkeel-images are Longhorn volumes placed by Longhorn on storage nodes |
| Cinder API, scheduler, volume, backup | control plane | Deployments | control-plane | baseline | API minAvailable: 2, workers maxUnavailable: 1 | no | they only call the Kubernetes API; the data path is Longhorn |
| Manila API, scheduler, share | control plane | Deployments | control-plane | baseline | idem | no | the share data path is Longhorn RWX share-manager pods (NFS) on storage nodes |
| Swift proxy | control plane (or network/ingress at scale) | Deployment (3+) | control-plane | baseline | minAvailable: 2 | no | object traffic is bandwidth-heavy: give it its own nodes or the ingress nodes at scale |
| Swift storage | storage | StatefulSet (one pod per "storage node") | storage | required anti-affinity on hostname, spread on zone (Swift zones = failure domains) | maxUnavailable: 1 | no | devices: PVCs with numberOfReplicas: 1 + strict-local (swift-csi-block.md § 1) |
| Swift ring builder, expirer, reconciler | control plane | Deployment (Lease) | control-plane | — | — | no | |
| Heat API, API-CFN, engine | control plane | Deployments | control-plane | baseline | idem | no | |
| ec2-api | control plane | Deployment | control-plane | baseline | minAvailable: 2 | no | |
| Barbican API, worker, Keystone listener | control plane | Deployments | control-plane | baseline | idem | no | a PCIe HSM would need its own label; network HSMs need egress only |
| Designate API, central, sink | control plane | Deployments | control-plane | baseline | idem | no | |
| ExternalDNS (one per DNS backend) | control plane | Deployment (1) | control-plane | — | — | no | the authoritative DNS servers (PowerDNS…) are outside this document; if run in-cluster, put them on network/ingress nodes with a LoadBalancer Service |
| Masakari API, engine | control plane | Deployments | control-plane | baseline | idem | no | |
| Medik8s NHC operator, Node Maintenance operator | control plane | Deployments | control-plane | — | — | no | |
| Self Node Remediation agent | all nodes | DaemonSet | all | — | — | privileged, /dev/watchdog | a node it cannot fence cannot be remediated (§ 5.6) |
| Ironic API | control plane | Deployment (3) | control-plane | baseline | minAvailable: 2 | no | |
| Ironic conductor (+ HTTP server; dnsmasq with iPXE, later) | provisioning, dedicated node (D7) | StatefulSet | provisioning | required anti-affinity on hostname | maxUnavailable: 1 | Multus macvlan/ipvlan on the provisioning VLAN (preferred) or hostNetwork (documented exception); dnsmasq needs NET_ADMIN, NET_RAW | own namespace, egress only to the BMC network |
| Skyline API server + console (Python) | control plane | Deployment (2) | control-plane | baseline | minAvailable: 1 | no |
| Component | Role | Kind | Tolerations | Host / priv | How it is placed |
|---|---|---|---|---|---|
| MariaDB Operator, RabbitMQ Cluster + Topology operators, infra-operator | control plane | Deployments | control-plane | no | nodeSelector / tolerations values of their charts |
| Longhorn manager | every node that consumes or stores volumes (all) | DaemonSet | all | privileged | longhornManager.nodeSelector / tolerations (chart values [verify]) |
| Longhorn instance managers, engine, replicas | replicas on storage (disks only there); engines where the volume is attached | managed pods | all | privileged | defaultSettings.createDefaultDiskLabeledNodes: true + label node.longhorn.io/create-default-disk=true on storage nodes [verify]; taintToleration setting |
| Longhorn CSI plugin (node) | all | DaemonSet | all | privileged | follows Longhorn's taintToleration |
| Longhorn UI, driver deployer, CSI attacher / provisioner / resizer / snapshotter | control plane (or storage) | Deployments | control-plane | no | systemManagedComponentsNodeSelector [verify] |
| Longhorn RWX share managers (Manila) | storage | pods | storage | no | systemManagedComponentsNodeSelector |
| KubeVirt / CDI | see § 2.2 | nodePlacement | |||
| Kube-OVN controller, monitor, its own OVN central (pod network) | control plane | Deployments | control-plane | hostNetwork [verify] | Kube-OVN chart MASTER_NODES / kube-ovn/role=master [verify] |
Kube-OVN kube-ovn-cni, ovs-ovn, pinger | all nodes | DaemonSets | all | privileged, hostNetwork | as long as Kube-OVN is the primary CNI (open, § 7) |
| Kube-OVN speaker (BGP, mode A only) | network | DaemonSet | network | hostNetwork | not deployed with the parallel OVN (D2): tenant IPs are announced by ovn-bgp-agent |
| Multus | all | DaemonSet | all | privileged | |
| SR-IOV network operator + device plugin | compute with SR-IOV NICs | DaemonSet | compute | privileged | SriovNetworkNodePolicy.spec.nodeSelector on feature.node.kubernetes.io/network-sriov.capable=true + compute |
| Node Feature Discovery | all | DaemonSet | all | read-only host paths | |
| ExternalDNS / External DNS Operator | control plane | Deployment | control-plane | no |
| Piece | Mode A: Neutron on Kube-OVN (alternative) | Mode B: parallel OVN (ML2/OVN-identical NB/SB) — decided (D2) |
|---|---|---|
| OVN NB / SB / northd | Kube-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 NB | Kube-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 chassis | OpenKeel ovn-controller DaemonSet on compute and network nodes only; VMs attach through a Multus secondary NIC |
| Gateway selection | Kube-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 physnet | external_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 |
| BGP | Kube-OVN speaker on network nodes | ovn-bgp-agent + FRR (Python) on network nodes, and on compute nodes when distributed FIPs or provider-network VMs must be announced |
| Load balancers | SwitchLBRule / 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):
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];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 label | Mode B: OVS external_ids | Mode A: Kube-OVN |
|---|---|---|
node-role.openkeel.io/network | ovn-cms-options=enable-chassis-as-gw | node 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-ex | ovn-bridge-mappings | ProviderNetwork.spec.customInterfaces |
network.openkeel.io/external-nic=bond1 | NIC added to br-ex | external-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.
| Centralised | Distributed | |
|---|---|---|
| Mode B setting | [ovn] enable_distributed_floating_ip = false (ML2/OVN default) | true |
| Mode A setting | ovn-external-gw-config type: centralized | type: distributed [verify] |
| Where FIP traffic leaves | the active gateway chassis of the router (network nodes) | the compute node that hosts the VM |
| Nodes that need the external NIC / physnet bridge mapping | network nodes only | network and compute nodes (ProviderNetwork.spec.nodeSelector must include compute; ovn-bridge-mappings on every hypervisor) |
| SNAT (no FIP) | gateway chassis | still the gateway chassis |
ovn-bgp-agent placement | network nodes | network + compute nodes |
| Recommended for | small and medium clusters, simple external cabling | large 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.
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):
| Piece | Who | Where |
|---|---|---|
metadata port per network: LSP type: localport, device_owner: network:distributed, one IP per subnet | the 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 IP | OVN native DHCP (classless_static_route in DHCP_Options), as ML2/OVN | in the VM, learnt by DHCP |
**openkeel-neutron-metadata agent** (Go) | DaemonSet, one per hypervisor | compute nodes |
| Nova metadata API | openkeel-nova metadata Deployment, ClusterIP Service on 8775 | control 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)
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).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.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.masakari-kubevirt.md).| Nova concept | OpenKeel |
|---|---|
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 service | the compute agent pod of that node (DaemonSet); its heartbeat = the service updated_at; the agent stops → service down |
| a node without the compute role | not a hypervisor: no agent, no service record, no resource provider, no VM can be placed there |
openstack compute service set --disable | the 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 administrator | seen by the agent: service reported disabled with disabled_reason: "cordoned in Kubernetes" |
The compute agent creates one root resource provider per compute node (name = node name, uuid = node UID) and keeps its inventories current:
| Resource class | Source | Reserved |
|---|---|---|
VCPU | node allocatable.cpu minus the CPUs of the static CPU manager pool | live 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) |
PCPU | reservedSystemCPUs complement when the CPU manager is static and the node is labelled for dedicated CPUs | same |
MEMORY_MB | allocatable.memory minus the hugepages | non-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_GB | not 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 space | over-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:
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;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.
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.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.openkeel.io/ha-segment), set by Masakari from segments/{id}/hosts.nodeSelector: {kubernetes.io/hostname: <node>}, the tolerations of the compute role, and priorityClassName: openkeel-vm.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.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.
openkeel-chartHelm 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.
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):
| Chart | Components 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, -skyline | API Deployment and its Jobs (placement.api), control plane | |
openkeel-barbican | API and Jobs (api), worker and Keystone listener (worker), control plane | |
openkeel-cinder | API and Jobs (api), scheduler, volume and backup (workers), control plane | |
openkeel-masakari | API and engine (their nodePlacement.<x>.nodeSelector wins, its tolerations are added), migration Job (api), control plane | |
openkeel-neutron | API and Jobs (api), OVN central (its ovnCentral.nodeSelector wins; the default is now empty, it was the lab key), control plane | the OVN chassis DaemonSet: chassis.nodeSelector, tolerates every taint |
openkeel-neutron-vpnaas | API and Jobs (api), control plane | the agent DaemonSet follows the chassis (agent.nodeSelector) |
openkeel-nova | API and migration Job (api), conductor, console proxy, Ironic agent: control plane; compute agent DaemonSet: compute | |
openkeel-ironic | API 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.
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).
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.
Add
kubeadm join + labels), kernel arguments for hugepages and IOMMU, kubelet CPU manager settings.virt-handler (node becomes kubevirt.io/schedulable=true), Longhorn, SNR, the Nova compute agent.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).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)
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).openstack compute service set --disable --disable-reason maintenance <node>: no new VMs.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.kubectl uncordon, re-enable the service, clear on_maintenance.Remove
openstack compute service delete <id>: refused while the resource provider has allocations (Nova/Placement behaviour), then the agent deletes the provider.kubectl delete node, scale the MachineDeployment down.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:
NodeHealthCheck per Masakari segment**, selecting openkeel.io/ha-segment=<uuid> (always within the compute role). maxUnhealthy stops remediation storms (masakari-kubevirt.md).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.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.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 --- l5kube-ovn/role=master; global.nodeRoles.taints: false.reserved on each node = everything the infra requests (§ 4.2): budget roughly 8 cores / 32 GiB per node for OpenKeel + platform before any VM.maxUnhealthy: 1; live migration needs Longhorn RWX (migratable) volumes.control-plane, taint optional (needed only if generic workloads run in the cluster).best-effort for VM disks); taint compute.ovn-bgp-agent on network and compute nodes if the fabric speaks BGP.reserved_host is used.availability-zones=<zone>), distributed floating IPs when every rack has the external VLANs, ovn-bgp-agent on network and compute nodes.Answered on 2026-10-29: label keys, network mode, distributed floating IPs, metadata service, Galera storage, capacity reservation, Ironic, ingress (§ 0). Still open:
ovs-vswitchd with a second integration bridge (§ 3.1, to be validated)?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?machine.nodeLabels identity, Medik8s control-plane handling, one OVS serving two ovn-controllers.Fonte: openkeel-docs/NODE-PLACEMENT.md