Skip to content

OVNCentral CRD ​

ovncentrals.ovn.openstack.c5c3.io/v1alpha1, kind OVNCentral. The CRD is generated from operators/ovn/api/v1alpha1/ovncentral_types.go; the defaulting and validating webhooks live in operators/ovn/api/v1alpha1/ovncentral_webhook.go.

One CR describes the whole OVN control plane: the Northbound and Southbound Raft databases, the northd daemon that translates between them, an optional Southbound relay tier, the certificates every connection is authenticated with, and the recurring database backup. The two databases share one CR because northd only works against both of them and their Raft clusters have to be sized together.

kubectl get ovncentrals prints Ready (.status.conditions[?(@.type=='Ready')].status), NB (.status.northbound.readyReplicas), SB (.status.southbound.readyReplicas), and Age.

Spec ​

FieldTypeRequiredDefaultDescription
image*commonv1.ImageSpecnooperator-resolved ghcr.io/c5c3/ovn:26.03.2The image that runs ovsdb-server, northd, and the relay. All three ship in the one OVN image, so one reference covers them. The operator resolves it at reconcile time, so an unset field keeps tracking its tested version across upgrades (image.go). An unset image names no pullPolicy of its own, so its containers take the operator default (--default-image-pull-policy) or the rule: Always for the tag (see pull policy resolution)
northboundOVNDatabaseSpecno{}The Northbound database, the one the CMS writes the logical network model into
southboundOVNDatabaseSpecno{}The Southbound database, the one northd writes translated flows into and every chassis reads from. It is the busier of the two, which is why it can be fronted by a relay
northdOVNNorthdSpecno{}The ovn-northd daemon that compiles the Northbound model into Southbound flows
relay*OVNRelaySpecnonilFronts the Southbound database with ovsdb-server relays. Every chassis holds an open Southbound connection, so past a few hundred nodes the read load is what limits the cluster. When nil the chassis connect to the database directly
jobs*commonv1.JobSpecnonilSizes, prioritizes and places the pods of the backup CronJob, its backup init container and the S3 shifter included. A field left unset falls back to spec.northd.deployment. Unset resources resolve to the request floor: a 70m CPU and a 256Mi memory request, no limit. See Backup
tlsOVNTLSSpecyes—The cert-manager issuer every OVN certificate is requested from. Required: the databases carry the entire logical network model, so an unauthenticated listener would let any pod that reaches the port rewrite the network
backup*OVNBackupSpecnonilTunes the recurring database backup. A nil block still gets a CronJob; it only means every setting resolves to the operator default. See Backup
targetClusterRef*commonv1.TargetClusterRefSpecnonil (the local cluster)The registered target cluster the children are created on. The CR itself, its status and its finalizer stay on the management cluster. Immutable, enforced by two CEL transition rules and by the webhook. See Target Clusters

OVNDatabaseSpec ​

Configures one of the two Raft databases. Three fields are frozen after creation by CEL transition rules, because they are baked into the cluster when its first member creates the database file. See Defaulting and validation.

FieldTypeRequiredDefaultDescription
replicasint32 (Minimum=1, Maximum=5)no3The number of Raft members. It must be odd: an even cluster tolerates no more failures than the odd one below it and has two ways to split the vote. Immutable
storageOVNStorageSpecno{}Sizes the per-member claim holding the database file and its Raft log. Immutable, because a StatefulSet volume claim template cannot change
externallyReachableboolnofalsePublishes every member on a node port, making the database reachable on the IP of every node. The client this exists for is an OVNChassis on a node without cluster networking, which dials the Southbound database alone
nodePortBase*int32 (Minimum=30000, Maximum=32767)no30641 for northbound, 30651 for southboundThe first node port of this database's range. Member i is published on nodePortBase + i, because a Raft client has to address the individual members
electionTimerMsint32 (Minimum=1000, Maximum=180000)no1000How long a follower waits without hearing from the leader before it starts an election. Written into the database when it is created, so it is immutable through this field
inactivityProbeMsint32 (Minimum=0)no60000How long ovsdb-server lets a client connection sit idle before probing it. Zero disables the probe, which is what a client behind a connection-tracking middlebox needs when the probe is what tears the connection down
resources*corev1.ResourceRequirementsnooperator-resolved 70m CPU, 256Mi memory requests; no limitRequests and limits for the ovsdb container. The floor is filled per resource: a CPU the block names neither as request nor as limit gets a 70m request, and a memory it names neither way gets a 256Mi request, both without a limit, so each Raft member runs in the Burstable QoS class. Other resources the block names, such as an ephemeral-storage limit, are kept beside the floor. A member without requests would run BestEffort, the class the kubelet evicts first under node memory pressure. There is no default limit because the database grows with the number of logical ports: size the memory request, and any limit, from that count. A resource the block names is used as written, and no request is added beside a limit the block sets. The floor is resolved at reconcile time and never written into the CR, so upgrading to an operator that changes it rolls the members once, one at a time. See The request floor for LimitRanges and the upgrade roll
verticalAutoscaling*VerticalAutoscalingSpecnonilOpts the member StatefulSet ({name}-nb or {name}-sb) into a VerticalPodAutoscaler that controls the requests of its containers; see VerticalAutoscalingSpec. The operator fills minAllowed with the request floor, 70m CPU and 256Mi memory, for each of the two resources the block's minAllowed does not name, so the VPA never moves a member below what the floor renders. A value the block names is kept, even one below the floor. On a cluster without the VPA, VPAReady turns False with reason VPANotInstalled

The request floor ​

The floor is an explicit request, so a LimitRange in the namespace no longer fills the ovsdb container's requests from its defaultRequest; it still fills a missing limit from its default. The API server then rejects every member pod when the LimitRange sets a default limit below 70m CPU or 256Mi memory, a min above them, or a maxLimitRequestRatio smaller than its default limit divided by the floor. The rejection shows up as FailedCreate events on the StatefulSet, while the CR reports only how many members are ready. In such a namespace, set resources to values the LimitRange admits, naming both CPU and memory, because the floor still fills a resource the block leaves out.

Upgrading from an operator without the floor changes the pod template of every database whose resources sets no CPU or memory value, and upgrading from an operator that filled the floor only when the block named neither changes the template of every database whose block names only one of the two. The StatefulSet recreates its members one at a time, highest ordinal first. Each recreated member now needs 70m CPU and 256Mi memory of unrequested allocatable on a node it can run on. A member on node-local storage, such as local-path, TopoLVM or a local PersistentVolume, can run only on the node that holds its volume. If that node lacks the room the member stays Pending, and if a LimitRange rejects it the member is never created. Either way the rollout stops there: a three-member database runs on two members and tolerates no further loss. Setting resources to values that fit the node and the LimitRange recovers the rollout, because a CPU or memory request or limit replaces the floor for that resource.

Placement and priority ​

FieldTypeRequiredDefaultDescription
priorityClassName*stringnonilThe priority class of the member pods. The webhook checks that the class exists. Unset or "" renders none, and the cluster default applies
nodeSelectormap[string]stringnonilRestricts the member pods to nodes that carry every listed label. The webhook checks the label grammar
tolerations[]corev1.TolerationnonilLets the member pods onto nodes with matching taints. The webhook applies the API server's toleration rules
affinity*corev1.AffinitynonilNode affinity and pod (anti-)affinity rules for the member pods, rendered verbatim

The four fields let an operator pin OVN central to dedicated nodes as a whole. Unset fields render nothing, so a database whose CR sets none of them does not roll. Each member keeps its database on a claim, and node-local storage binds that claim to one node. A selector, toleration set or affinity that excludes the node a member's claim is bound to leaves that member Pending, and the rolling update stops there while the other members keep serving. Move the members off such storage before you narrow their placement.

Disruption budget and spread ​

Each database gets a PodDisruptionBudget named after its StatefulSet, {name}-nb or {name}-sb, with maxUnavailable: 1. It selects the database's own members, so the backup pods (component backup) are not counted. One voluntary disruption at a time fits every member count:

  • Three members keep quorum through a node drain, because two stay up.
  • A single member, as in CI and the quick start, never blocks a drain. The database is unavailable while its one pod restarts.
  • Five members are evicted one at a time and keep one further failure of margin.

The members also carry the two spread constraints the API Deployments get by default: topology.kubernetes.io/zone and kubernetes.io/hostname, both with maxSkew: 1 and whenUnsatisfiable: ScheduleAnyway, both selecting the database's members. The spread is soft, so three members still schedule on a single node. To make it hard, set a required pod anti-affinity on kubernetes.io/hostname through the database block's affinity:

yaml
spec:
  northbound:
    affinity:
      podAntiAffinity:
        requiredDuringSchedulingIgnoredDuringExecution:
        - topologyKey: kubernetes.io/hostname
          labelSelector:
            matchLabels:
              app.kubernetes.io/name: ovncentral
              app.kubernetes.io/instance: ovn
              app.kubernetes.io/component: nb

With a hard spread every member needs a node of its own, and a member that finds none stays Pending.

OVNStorageSpec ​

Sizes a PersistentVolumeClaim. Shared by the two database members and by the backup volume, which have the same two knobs.

FieldTypeRequiredDefaultDescription
sizestring (Pattern ^[0-9]+(Mi|Gi|Ti)$)no1GiThe requested volume size. The pattern admits binary units only: a decimal 1G differs from 1Gi by enough to matter on a volume this small
storageClassName*stringnonilSelects the StorageClass. When nil the cluster's default class is used

OVNNorthdSpec ​

FieldTypeRequiredDefaultDescription
deploymentcommonv1.DeploymentSpecno{}The pod-level knobs of the northd Deployment: replicas (default 3), resources (resolved per resource when the pod is rendered: 70m CPU request, no CPU limit, and 368Mi as memory request and limit at one thread, plus 32Mi per extra thread, see the resource defaults), terminationGracePeriodSeconds, preStopSleepSeconds, strategy, topologySpreadConstraints, priorityClassName, and the node placement nodeSelector, tolerations and affinity (see NodePlacementSpec). Three northd pods are one active instance and two standbys, so the count sizes failover. The block is also the fallback of spec.jobs
deployment.verticalAutoscaling*VerticalAutoscalingSpecnonilOpts the northd Deployment ({name}-northd) into a VerticalPodAutoscaler that controls the requests of its containers; see VerticalAutoscalingSpec. On a cluster without the VPA, VPAReady turns False with reason VPANotInstalled.
threadsint32 (Minimum=1, Maximum=16)no1Parallel logical-flow computation threads. Past a handful the lock contention inside northd eats the gain, so the ceiling stays low

OVNRelaySpec ​

Relays are stateless caches, so they scale independently of the Raft cluster behind them. The block has no deployment field: there is nothing to drain and no rollout ordering to respect, so the operator applies the shared replica default and the render-time resource defaults to the replica count and the resources, and renders the node placement fields verbatim.

FieldTypeRequiredDefaultDescription
replicasint32 (Minimum=1)yes—The number of relay pods. Unlike the database replicas this is a plain scaling knob with no odd-count or immutability constraint
externallyReachableboolnofalsePublishes the relay Service on one node port and the addresses of the nodes the relay pods run on in status.relayDbAddress. Turn it on when an OVNChassis on another cluster attaches to this central: such a chassis cannot reach the relay's cluster IP, and without a published relay it dials the Southbound node ports, which puts its connection on the Raft members
nodePort*int32 (Minimum=30000, Maximum=32767)nooperator-resolved 30661The node port the relay is published on. Requires externallyReachable, and must not fall inside either database's node-port range (see Webhook rules)
resources*corev1.ResourceRequirementsnooperator-resolved 70m CPU request, 368Mi memory request and limitRequests and limits for the relay container. The operator resolves defaults per resource when it renders the pod: a CPU the block names neither as request nor as limit gets a 70m request and no limit, and a memory it names neither way gets 368Mi as both request and limit, the figure for one single-threaded process (see the resource defaults). Anything else the block sets is kept
verticalAutoscaling*VerticalAutoscalingSpecnonilOpts the relay Deployment ({name}-sb-relay) into a VerticalPodAutoscaler that controls the requests of its containers; see VerticalAutoscalingSpec. On a cluster without the VPA, VPAReady turns False with reason VPANotInstalled.
nodeSelectormap[string]stringnonilRestricts the relay pods to nodes that carry every listed label. The webhook checks the label grammar
tolerations[]corev1.TolerationnonilLets the relay pods onto nodes with matching taints. The webhook applies the API server's toleration rules
affinity*corev1.AffinitynonilNode affinity and pod (anti-)affinity rules for the relay pods, rendered verbatim

OVNTLSSpec ​

FieldTypeRequiredDefaultDescription
issuerRefOVNIssuerRefyes—The issuer. It must be CA-capable: OVN authenticates peers against the issuing CA certificate, so an issuer that cannot expose one (ACME, for instance) produces certificates the databases reject

The operator sets no role= on the OVSDB connection rows, so every certificate this issuer signs has unrestricted read and write on both databases. Do not share the issuer with workloads outside this control plane.

OVNIssuerRef ​

FieldTypeRequiredDefaultDescription
namestring (MinLength=1)yes—The cert-manager issuer's name
kindstring (Enum Issuer, ClusterIssuer)noClusterIssuerThe issuer scope. ClusterIssuer is the default because the OVN CA is normally shared with the chassis namespaces, which a namespaced Issuer cannot reach

OVNBackupSpec ​

The snapshot volume is a child of the OVNCentral like any other, so deleting the CR deletes it along with the database volumes. Configure s3 for a copy that outlives the CR.

FieldTypeRequiredDefaultDescription
schedulestringno0 2 * * *The cron expression the backup runs on, resolved at reconcile time when empty. A stated expression is parsed at admission with the library the CronJob controller uses
retentionDays*int32 (Minimum=1)no14How long a snapshot is kept before a later run deletes it. Shortening the window is echoed back as an admission warning
suspendboolnofalseStops the CronJob from firing without deleting it or the snapshots already taken. The switch to reach for during a maintenance window that would otherwise snapshot a half-migrated database
storageOVNStorageSpecno{}Sizes the claim the snapshots are written to
s3*OVNBackupS3SpecnonilCopies each snapshot off-cluster. Without it the snapshots share the fate of the cluster that holds them

OVNBackupS3Spec ​

FieldTypeRequiredDefaultDescription
bucketstring (MinLength=1)yes—The target bucket name
prefixstringno""The key prefix each snapshot is written under, so one bucket can hold the backups of several deployments
endpointstring (Pattern ^https://)yes—The S3 service URL. HTTPS only: the upload carries the access key beside a full snapshot of both databases, and SigV4 authenticates a request without encrypting it
regionstringno""The S3 region. Optional because most S3-compatible implementations ignore it
credentialsSecretRefcommonv1.SecretRefSpecyes—The Secret holding the access key under the keys access-key-id and secret-access-key. It lives in the OVNCentral's own namespace
image*commonv1.ImageSpecnooperator-resolved ghcr.io/c5c3/backup-shifter:latestThe image the upload step runs. An unset image names no pullPolicy of its own and takes the operator default or Always. See the warning in Backup

Defaulting and validation ​

The mutating webhook leaves the object untouched. Every default is either a +kubebuilder:default the API server applies from the CRD schema, or a value the operator resolves at reconcile time: the image, the two node-port bases, the backup schedule and retention, the database request floor, and the container resources of northd and the relays. Resolving those late keeps an unset field tracking the operator default across upgrades instead of freezing today's value into the stored CR. The webhook stays registered so a default that has to be materialized later can be added without changing the deployed webhook configuration.

ovncentral_webhook.go holds the constants those resolutions read: DefaultBackupSchedule is 0 2 * * *, DefaultBackupRetentionDays is 14, DefaultNorthboundNodePortBase is 30641 and DefaultSouthboundNodePortBase is 30651. The two bases carry their database's OVSDB port in the last two digits, and sit ten apart so both ranges reach the five-replica ceiling without colliding. DefaultRelayNodePort is 30661, ten above the Southbound base and outside both default ranges at that ceiling. The database request floor reads its 70m CPU request from the shared DefaultCPURequest in internal/common/types/workload.go and sets its 256Mi memory request in WithRequestFloor. The northd and relay containers get theirs from WithResourceDefaults and MemoryForProcesses in internal/common/types/resources.go: 368Mi at one thread, and 32Mi more per extra northd thread.

Schema-layer rules ​

These hold even when the webhook is down.

MessageWhere it comes from
targetClusterRef is immutableTwo CEL transition rules on OVNCentralSpec, one for adding or removing the ref and one for renaming it
replicas is immutable: Raft membership changes are not supportedTransition rule on OVNDatabaseSpec. A membership change needs an ovsdb-tool/ovs-appctl procedure against the running cluster that the operator does not perform
electionTimerMs is immutable: it is applied when the clustered database is createdTransition rule on OVNDatabaseSpec
storage is immutable: volumeClaimTemplates cannot changeTransition rule on OVNDatabaseSpec. The API server refuses the StatefulSet update anyway; rejecting it here reports the constraint at admission
replicas must be oddField rule on OVNDatabaseSpec.replicas
exactly one of image.tag or image.digest must be setInherited from commonv1.ImageSpec, on spec.image and on spec.backup.s3.image
preStopSleepSeconds must be strictly less than terminationGracePeriodSecondsInherited from commonv1.DeploymentSpec on spec.northd.deployment; the rule substitutes the effective defaults 5 and 30 for an unset pointer
relay.nodePort requires relay.externallyReachableRule on OVNRelaySpec. A node port the relay is not published on would name a port nothing listens on

Each of the three OVNDatabaseSpec transition rules is guarded by a has() check on both sides. The API server evaluates every rule of the type against the empty-object default the two referencing fields carry, and that object has none of the three keys, so an unguarded rule makes the whole CRD unapplicable. The guard changes nothing for a stored CR: all three keys carry a schema default, so admission has filled them long before an update can reach these rules.

Webhook rules ​

The validating webhook accumulates every violation into one admission response.

MessageTrigger
name must be at most %d characters: the backup CronJob appends %q and Kubernetes caps CronJob names at %d charactersmetadata.name longer than MaxOVNCentralNameLength. The three arguments are the bound (45), the suffix (-backup), and MaxCronJobNameLength (52)
issuerRef.name must be setspec.tls.issuerRef.name is empty
repository must be setspec.image.repository or spec.backup.s3.image.repository is empty
exactly one of image.tag or image.digest must be setBoth or neither are set on either image reference, re-checked outside the schema
nodePortBase %d leaves no room for %d replicas below %dA base whose range runs past 32767. The arguments are the effective base, the effective replica count, and the ceiling. The last member's Service would be rejected by the API server, leaving that member unreachable from outside the cluster
northbound and southbound nodePort ranges overlapThe two ranges intersect. Each runs over as many consecutive ports as there are members, so two bases that look far apart still collide once both databases are scaled up
relay.nodePort requires relay.externallyReachablespec.relay.nodePort set without spec.relay.externallyReachable, the twin of the schema rule
relay nodePort %d falls inside the %s nodePort range %d-%dA published relay's effective port (spec.relay.nodePort, or 30661) lies inside the effective range of northbound or southbound. The arguments are the port, the database, and the first and last port of its range. The check runs whether or not the database is published, so a later edit that publishes it cannot collide with the relay
retentionDays must be at least 1spec.backup.retentionDays below 1, alongside the Minimum=1 marker. Zero would delete every snapshot the run just took
invalid cron expression: %vspec.backup.schedule is non-empty and cron.ParseStandard refuses it. The argument is the parser's own error. An empty schedule is not parsed, since it resolves the operator default
credentialsSecretRef.name must be setspec.backup.s3 is set with no credentials Secret named
target cluster name must be setspec.targetClusterRef is present with an empty name
targetClusterRef is immutable (adding or removing it after creation is not permitted)An update adds or drops the ref. Both strand the children already created on the previously selected cluster
targetClusterRef is immutable (the children already exist on the previously named cluster)An update renames the ref
field.Invalid on nodeSelectorA key is not a qualified label name, or a value (reported at nodeSelector[<key>]) is not a valid label value, on spec.northbound, spec.southbound, spec.northd.deployment, spec.relay or spec.jobs
field.Invalid / field.NotSupported on tolerations[i]A toleration the API server would refuse (an invalid key, an empty key without Exists, Exists with a value, an Lt or Gt value that is not an integer, tolerationSeconds without NoExecute, or an unknown operator or effect), on the same five blocks
field.NotFound on priorityClassNamespec.northbound.priorityClassName, spec.southbound.priorityClassName or spec.jobs.priorityClassName names a PriorityClass that does not exist. Skipped when no lookup client is injected, and for ""
%s request must not exceed limit (%s)A request in spec.jobs.resources above its own limit

The name bound is MaxOVNCentralNameLength, computed as MaxCronJobNameLength minus len("-backup"), which is 45 characters. It is enforced on create only. metadata.name is immutable, so on update the rule could only fire against an object a pre-upgrade operator already admitted, and the validating webhook also sees the finalizer-removal update that completes a deletion: rejecting that one would wedge the CR in Terminating with no field left to edit.

One update is admitted with a warning:

text
spec.backup.retentionDays reduced %d → %d: the next backup run deletes the snapshots taken between %d and %d days ago, which cannot be undone

The effect is immediate and irreversible, and a typo (3 for 30) is indistinguishable from an intended change at admission time. Shortening the window is a legitimate operational choice, so it stays a warning. The comparison runs against the resolved retention on both sides, so dropping spec.backup entirely does not read as a reduction to nothing.

Of the shared deployment validators the service operators run, only the node placement checks cover spec.northd.deployment and spec.relay. Their priority class, topology-spread selectors and requests against limits are checked by the inherited CEL rule alone. The two databases get the placement checks and the priority-class lookup.

Status ​

FieldTypeDescription
conditions[]metav1.ConditionList-map keyed by type; see Conditions
observedGenerationint64The .metadata.generation the controller last reconciled
northboundOVNDatabaseStatusThe observed state of the Northbound database
southboundOVNDatabaseStatusThe observed state of the Southbound database
relayAddressstringThe Southbound relay Service, ssl:<clusterIP>:6642. Set while spec.relay is set and cleared when the relay is removed
relayDbAddressstringThe relay for clients outside the cluster, ssl:<hostIP>:<nodePort> per node a relay pod runs on, from the pod's status.hostIP, comma-separated. Empty unless spec.relay.externallyReachable is set. An OVNChassis on another cluster dials it (see Address computation)
clientSecretNamestringThe Secret holding the client certificate every OVN client authenticates with (tls.crt, tls.key, ca.crt). An OVNChassis mounts it, so this is the field that connects the two kinds
installedImagestringThe image reference the running control plane was projected from, recorded once northd runs on it. It tells a rollout that has not reached the pods from one that has

OVNDatabaseStatus ​

FieldTypeDescription
internalDbAddressstringThe connection string for clients inside the cluster, one entry per member
dbAddressstringThe connection string for clients outside the cluster. Empty unless externallyReachable is set
readyReplicasint32The number of Raft members that are ready. A cluster keeps serving writes while a minority is down, so this is a health signal

Address computation ​

reconcile_endpoints.go assembles both strings from the per-member Services and pods, read through the target cluster's uncached reader.

The internal address lists ssl:<clusterIP>:<port> per member in ordinal order, joined with commas, at port 6641 for the Northbound and 6642 for the Southbound database. The external address is assembled only under spec.<db>.externallyReachable, as ssl:<hostIP>:<nodePortBase + ordinal> per member, from the HostIP of the member's pod.

The relay step publishes status.relayDbAddress the same way under spec.relay.externallyReachable: one ssl:<hostIP>:<nodePort> per node a relay pod runs on, with the port from spec.relay.nodePort or 30661. The host IPs are deduplicated and sorted, because a change in the string rolls the ovn-controller DaemonSet of every chassis on another cluster, and a reordered pod list must not count as one. A relay pod not scheduled yet names no node, and while no relay pod has a node address the field stays empty. It is cleared with the flag and with spec.relay.

Both are IP literals, never DNS names. ovsdb-server resolves a remote once at startup and never again, so a name whose address changes leaves the client wedged against the old one.

The figure shows which client dials which of these addresses.

What one OVNCentral runs and who talks to it. The Northbound database {central}-nb and the Southbound database {central}-sb are Raft clusters of three members each by default. northd reads the Northbound database and writes the flows into the Southbound database. A relay in front of the Southbound database exists only with spec.relay. A CronJob backs both databases up, and cert-manager issues a server certificate per database, one for the relay and one client certificate from the ClusterIssuer that spec.tls.issuerRef names. On the Neutron side the API with its ML2/OVN driver, the maintenance worker and the periodic workers hold both connections, and the optional CronJob {neutron}-ovn-db-sync compares the Northbound database with the Neutron database or rewrites it. On every chassis node ovn-controller registers the chassis in the Southbound database and reads its flows there, and the metadata agent watches the Southbound database and writes its Chassis_Private row. Two Jobs of an OVNChassis write as well: the evacuation removes gateway bindings from the Northbound database, and the chassis deletion removes the chassis row from the Southbound database. Only ovn-controller is pointed at the relay; every other client dials the database members.

A member whose pod is gone is skipped in the node-facing list: a rescheduling member has no node to name, while the members beside it are still reachable. A member whose Service is missing or carries no cluster IP stops the whole database, and so does a published database on which no member has a node address at all. While either database is in that state every address field is cleared, on both databases, and EndpointsReady reports EndpointsPending. A client is handed the member list as one string, so a list missing the member that happens to be the leader reads as a cluster that cannot serve writes rather than as one whose address is still being assembled.

Conditions ​

Eight sub-reconcilers each own one condition type. The aggregate Ready is True only when all eight are.

TypeStatusReasonMeaning
TLSReadyTrueCertificatesIssuedBoth server certificates and the client certificate are issued, and the client Secret carries tls.crt, tls.key, and ca.crt
TLSReadyFalseCertificatePendingcert-manager has not issued one of the Certificates yet, or has not written the client Secret
TLSReadyFalseCertificateErrorA Certificate could not be applied, the client Secret could not be read, or it is missing a key. A missing ca.crt names its cause: an issuer that is not CA-backed
TLSReadyFalseCertManagerUnavailableThe cluster the children land on does not serve the cert-manager.io/v1 Certificate kind. A wait, not an error: spec.tls is required, so there is nothing to fall back to
TLSReadyFalseCapabilityProbeFailedThe probe for that kind failed against the target cluster's RESTMapper
TLSReadyFalseTargetClusterUnavailablespec.targetClusterRef names a cluster that does not resolve. TLS is the pipeline's first gate, so the failure lands on the condition the rest of the graph waits behind
NorthboundReady, SouthboundReadyTrueStatefulSetReadyEvery Raft member of that database is ready
NorthboundReady, SouthboundReadyFalseStatefulSetProgressingFewer members are ready than replicas, or the StatefulSet's counters still describe the template before the last apply. The message counts the ready members
NorthboundReady, SouthboundReadyFalseStatefulSetErrorA child of that database could not be applied or read; the message carries the wrapped error. A target cluster that grants the operator no statefulsets or poddisruptionbudgets verb lands here; a failed budget apply reads ensuring nb PodDisruptionBudget (or sb)
EndpointsReadyTrueEndpointsPublishedBoth databases are reachable at the published addresses
EndpointsReadyFalseEndpointsPendingA member Service has no cluster IP yet, no member of a published database runs on a node, or a read failed
NorthdReadyTrueDeploymentReadyThe northd Deployment is available
NorthdReadyFalseDeploymentProgressingThe Deployment is rolling out
NorthdReadyFalseDeploymentErrorThe Deployment could not be applied
NorthdReadyFalseWaitingForEndpointsOne of the two database addresses is not published yet. northd is configured with both, so applying a Deployment with an empty --ovnnb-db would crash-loop the pods
RelayReadyTrueRelayNotRequiredspec.relay is not set; clients connect to the Southbound database directly
RelayReadyTrueDeploymentReadyThe relay Deployment is available at status.relayAddress. The message also names status.relayDbAddress when it is set
RelayReadyFalseDeploymentProgressingThe relay Deployment is rolling out
RelayReadyFalseDeploymentErrorThe relay Deployment, its Service, or its removal failed, or listing the relay pods for the node address did (listing sb-relay pods)
RelayReadyFalseServicePendingThe relay Service has no cluster IP yet. Reported ahead of the Deployment's own state: without an address the relays are unreachable however many are running
RelayReadyFalseWaitingForEndpointsThe Southbound address is not published yet
BackupReadyTrueBackupScheduledThe CronJob is in place and the most recent terminal run did not fail
BackupReadyTrueBackupSuspendedspec.backup.suspend is set. It outranks a failed run, because a suspended CronJob spawns no successor to supersede it; the failure is still named in the message
BackupReadyFalseBackupJobFailedThe newest terminal backup Job failed, so a restore would lose everything written since the last successful run
BackupReadyFalseBackupPVCInvalidThe API server rejected the snapshot claim, which is what lowering spec.backup.storage.size produces. No error is returned: only a spec edit can undo it
BackupReadyFalseBackupErrorThe claim, the CronJob, or the Job listing failed
BackupReadyFalseWaitingForEndpointsBoth database addresses reach the run as environment variables, and one is not published yet
ReadyTrueAllReadyAll eight sub-conditions are True
ReadyFalseNotAllReadyAt least one is not
VPAReadyTrueVPAReadyThe VerticalPodAutoscaler of every opted-in workload is applied; the message names them. See VerticalAutoscalingSpec
VPAReadyTrueVPANotRequiredNo workload opts in (every verticalAutoscaling block unset); a VPA the CR created before is deleted
VPAReadyFalseVPANotInstalledA workload opts in, but the cluster the children land on does not serve autoscaling.k8s.io/v1 VerticalPodAutoscaler. Nothing is created, and the other conditions still converge
VPAReadyFalseCapabilityProbeFailedThe target cluster the CR names could not be probed for the kind
VPAReadyFalseVPAErrorListing, applying or deleting a VPA failed; the message carries the error

northd, the relay, and the backup read the published addresses and nothing of each other's output, so they run as a parallel group after the endpoint step.

Sub-Resource Naming Convention ​

Every child takes the CR name plus a component suffix. For an OVNCentral named {name}:

ResourceNameNotes
StatefulSet{name}-nb, {name}-sbOne per database. Each shares its name with the headless Service that gives its members their stable DNS names, because a StatefulSet derives the per-pod names from its serviceName
Pod{name}-nb-0, {name}-nb-1, … and {name}-sb-0 upwardOne per Raft member, in ordinal order
PodDisruptionBudget{name}-nb, {name}-sbOne per database, maxUnavailable: 1, selecting that database's members (see Disruption budget and spread)
Service (per member){name}-nb-0 upward, matching the podClusterIP on 6641 (NB) or 6642 (SB); NodePort at nodePortBase + ordinal under externallyReachable. A Raft client addresses the members individually, so a single load-balanced Service would send half the writes to a follower
PersistentVolumeClaimdb-{name}-nb-0 upwardFrom the db volume claim template
Secret{name}-nb-server, {name}-sb-serverThe server keypair each database's members listen with, written by cert-manager under the Certificate's name
ConfigMap{name}-central-scriptsThe run and set-connection scripts of both databases plus the backup script. The backup shares it so the snapshot script cannot drift from the run scripts
Deployment{name}-northdnorthd. It has no Service: the daemon connects out to both databases and nothing connects to it
Deployment, Service{name}-sb-relayOnly while spec.relay is set. The relay's own keypair is issued under the same name. The Service is NodePort on spec.relay.nodePort (30661 by default) under spec.relay.externallyReachable
Secret{name}-clientThe keypair every OVN client authenticates with, published as status.clientSecretName
PersistentVolumeClaim, CronJob{name}-backupThe snapshot volume and the CronJob that writes to it, under one name

The Raft ports are fixed: the Northbound database serves clients on 6641 and replicates on 6643, the Southbound serves on 6642 and replicates on 6644.

An OVNCentral name longer than 45 characters is refused on create, so the plain {name}-backup form always fits. A CR that a pre-upgrade operator admitted above the bound keeps working: the CronJob name then collapses onto a content-stable hash, so the apply keeps succeeding.

Backup ​

The CronJob snapshots both databases with ovsdb-client and keeps the snapshots on the {name}-backup claim. A Raft cluster survives the loss of a minority of its members but not an operator error applied to all of them, so these snapshots are the only path back from a corrupted logical model.

The schedule and the retention resolve at reconcile time (reconcile_backup.go, constants in ovncentral_webhook.go): 0 2 * * * and 14 days when spec.backup leaves them unset. A nil block behaves like an empty one, so every OVNCentral gets a backup.

Each run writes nb-<timestamp>.backup and sb-<timestamp>.backup under /backup, where the timestamp is date -u +%Y%m%dT%H%M%SZ, for example nb-20260906T020000Z.backup. The script writes to a .tmp file first and renames it only after ovsdb-client exits zero with a non-empty result, so a run that dies mid-copy leaves nothing that looks like a snapshot. A volume that filled up lets ovsdb-client exit zero with nothing written, and a zero-byte file restores no database, which is why an empty result fails the run. Retention prunes *.backup files older than retentionDays, sweeps *.backup.tmp leftovers older than a day, and deletes any zero-byte file it still finds.

With spec.backup.s3 set, the snapshot moves into an init container and a shifter container copies /backup to the bucket with rclone once it has finished. The shifter mounts the volume read-only: the retention window is the snapshot container's to enforce, and an upload that could delete would make a misconfigured prefix destructive. rclone is configured entirely through environment variables, with the access key read from credentialsSecretRef.

Both containers carry the pod settings of spec.jobs, which falls back to spec.northd.deployment for the priority class and the node placement. Their resources resolve through the request floor: a 70m CPU and a 256Mi memory request and no limit. ovsdb-client holds each snapshot in memory, so the working set grows with the logical model, and a default limit would OOM-kill the run that outgrew it.

WARNING

spec.backup.s3.image defaults to ghcr.io/c5c3/backup-shifter:latest (image.go). The tag is mutable and the operator sets imagePullPolicy: Always for it, so every firing runs whatever the last merge pushed, with the S3 credentials in its environment and both database snapshots on its volume. Pin the field to a digest to opt out. An operator default of IfNotPresent or Never (--default-image-pull-policy) applies to this image too, and a node then keeps the build it pulled first.

Restoring a snapshot is a manual procedure: see Restore an OVN database snapshot.

Example ​

yaml
apiVersion: ovn.openstack.c5c3.io/v1alpha1
kind: OVNCentral
metadata:
  name: ovn-basic
  namespace: openstack
spec:
  tls:
    issuerRef:
      name: openstack-ovn-ca-issuer
  northbound:
    externallyReachable: true
  southbound:
    externallyReachable: true
  northd:
    deployment:
      replicas: 1
  relay:
    replicas: 1
    externallyReachable: true

This is the fixture the central-basic-deployment suite applies (tests/e2e/ovn/central-basic-deployment/00-ovncentral-cr.yaml) plus a relay published on the default node port, the shape an OVNChassis on another cluster attaches to. Both databases keep the default three Raft members, and spec.image is left unset so the operator resolves its own OVN image.