Provider oVirt (shturval-capov)

About the service

ParameterDescription
GUI nameProvider oVirt
System name (identifier)shturval-capov
Cluster AssignmentIntegration with oVirt: deployment of virtual machines for cluster nodes via the Cluster API. Installed in the management cluster.
CriticalYes, if the management cluster is deployed with the oVirt
Public repository— (provider for the delivery of the Shturval platform)
Dependenciesshturval-capov-crds, shturval-capi-crds
Namespacecapov-system

CAPOV (Cluster API Provider for oVirt) is a Cluster API infrastructure provider for the oVirt (Red Hat Virtualization) platform. The operator runs in the management cluster and automates VM creation, configuration, and removal in oVirt when deploying production clusters with CAPI.

For reference: For a list of OVirt* objects, see the section Custom node life cycle resources - oVirt.


Contents


1. Review

CAPOV implements the Cluster API contract for the infrastructure provider: it receives requests from CAPI controllers and performs the necessary operations in oVirt through its REST API.

Place in the chain of command

Illustration: Place in the chain of command

Management chain: user or platform — CAPI — CAPOV — oVirt Engine and worker cluster API

Key Features

  • API group: infrastructure.cluster.x-k8s.io/v1beta1
  • Operator Namespace: capov-system
  • Parallel processing: up to 10 simultaneous reconcile for OVirtMachine
  • Leader election enabled: safe operation with multiple replicas
  • Metrics: HTTPS on port :8443; health/readiness: HTTP on :8081

2. Architecture

Components

Illustration: Components

CAPOV operator architecture in the management cluster

CR and resource relationships

Illustration: CR and resource relationships

Cluster API and oVirt infrastructure resource relationships

Two controllers

ControllerResourceWhat does
OVirtClusterReconcilerOVirtClusterSets status.ready=true, controls finalizer
OVirtMachineReconcilerOVirtMachineCreates a VM in oVirt, configures disks/network, launches the VM, sets ProviderID

Pod ports

PortProtocolPurpose
8081HTTP/healthz (liveness), /readyz (readiness)
8443HTTPS/metrics (Prometheus; see below)

Metrics and ServiceMonitor

  • Service capov-metrics-service in the operator namespace exposes port 8443 (https) on the sub labeled control-plane: controller-manager.
  • If there is a CRD Prometheus Operator (monitoring.coreos.com/v1) in the cluster, the chart creates ServiceMonitor capov-metrics-monitor: scrape the path /metrics via HTTPS with the service account token (label release: shturval-metrics - coordinate with your Prometheus stack).

Launch flags (from Helm chart)

The chart specifies only part of the arguments; the rest are the default values of the main.go binary:

/manager
  --metrics-bind-address=:8443
  --leader-elect
  [--feature-gates=disk-keep-template-name=true,...]

Additional flags not set by the Helm template, but are supported during manual debugging:

FlagDefaultPurpose
--metrics-securetrueHTTPS for endpoint metrics; at false - HTTP
--enable-http2falseEnable HTTP/2 for metrics/webhook
--debugfalseDetailed logs (development and diagnostics)

3. Installing the service

CAPOV — infrastructure provider for Cluster API (infrastructure.cluster.x-k8s.io). In the Shturval platform, cluster-manager manages provider lifecycle in the management cluster. ShturvalServiceConfig (SSC) objects for provider charts are created and maintained as part of the platform configuration, not as arbitrary manual chart installations.

In the production environment, the provider is represented by two SSCs:

  • shturval-capov — main chart of the oVirt provider;
  • shturval-capov-crds — CRD chart of the oVirt provider module.

If the service was disabled when the platform was deployed, the provider’s SSC could receive an absence mode (mode: absent). To make the provider available for working with virtualization (zVirt, oVirt, HostVM, ED Virtualization, ROSA Virtualization, etc.), first switch the shturval-capov-crds mode to auto, then shturval-capov to auto. You can change the mode in the platform GUI on the Providers page.

In the GUI, this action is performed by the service mode switch. The screenshot shows the selection of the Auto mode for the provider service.

Switching provider service mode

3.1 Customvalues parameters

The structure of chart values is described by the spec.customvalues field of ShturvalServiceConfig and coincides with helm/main/values.yaml. Full list of supported parameters:

Recommendation: do not edit the ShturvalServiceConfig object manually to change limits, nodeSelector, feature gates and other release parameters - such an object belongs to the platform configuration and can be overwritten during updates. Make all changes to service behavior via ShturvalServicePatch (SSP), which overlays spec.customvalues on top of the base SSC (see example below).

ParameterDescriptionDefault
manager.repositoryOperator image repositoryr.shturval.tech/capov-manager
manager.tagImage tagChart.AppVersion
resources.limits.cpuCPU limit500m
resources.limits.memoryMemory limit128Mi
resources.requests.cpuCPU Request10m
resources.requests.memoryMemory Request64Mi
nodeSelectorSelecting a node by tags{} (any node)
tolerationsTolerationsinfra and control-plane NoSchedule
affinityAffinity rulesPrefers infra nodes
featureGatesFeature gates (see section 6)not specified

Default Affinity

The chart configures soft preference (preferredDuringSchedulingIgnoredDuringExecution): operator pods are preferentially scheduled on nodes with role infra. In the absence of such nodes, the pod can be scheduled on any working node, including the Control Plane.

Example of customization via SSP (resources and nodeSelector)

apiVersion: ops.shturval.tech/v1beta2
kind: ShturvalServicePatch
metadata:
  name: shturval-capov-resources
  labels:
    ops.shturval.tech/ssc: shturval-capov
spec:
  shturvalServiceConfigName: shturval-capov
  customvalues:
    manager:
      tag: "2.14.0"
    resources:
      limits:
        cpu: 1000m
        memory: 256Mi
      requests:
        cpu: 50m
        memory: 128Mi
    nodeSelector:
      node-role.kubernetes.io/infra: ""

Where to configure

Services and repositories → Installed services → shturval-capov → Manage. The service is hosted in the management cluster.

Configuring provider instances in the platform - oVirt and similar providers. General overview - How to work with providers in the platform works.


4. Diagnosis and troubleshooting

4.1 Checking operator status

# Pod status
kubectl get pods -n capov-system
kubectl describe pod -n capov-system -l control-plane=controller-manager

# Operator logs (last 100 lines)
kubectl logs -n capov-system deployment/capov-controller-manager --tail=100

# Logs in follow mode
kubectl logs -n capov-system deployment/capov-controller-manager -f

# Enable debug logging (via args override)
# Add to deployment args: --debug=true

# Metrics service (if you need manual access to /metrics)
kubectl get svc -n capov-system capov-metrics-service

# ServiceMonitor (with Prometheus Operator installed)
kubectl get servicemonitor -n capov-system 2>/dev/null || true

4.2 Checking resources

# All OVirtProviders and their status
kubectl get capovp
# NAME READY
# ovirt-prod true

# All OVirtClusters in all namespaces
kubectl get capovc -A

# All OVirtMachine in all namespaces
kubectl get capovm -A
# NAMESPACE NAME READY
# default worker-abc123 true
# default worker-def456 false

# Detailed information about the car
kubectl describe capovm worker-def456 -n default

4.3 The machine is stuck in the “not Ready” state

First step: get the machine’s conditions.

kubectl get capovm worker-def456 -n default \
  -o jsonpath='{range .status.conditions[*]}{.type}{"\t"}{.status}{"\t"}{.reason}{"\t"}{.message}{"\n"}{end}'

Condition CanConnectToOVirt = False

The operator cannot connect to oVirt. Reasons:

  • Invalid URL in spec.engine object OVirtProvider
  • Incorrect credentials (spec.user / spec.pass)
  • TLS problems (invalid certificate, try spec.tls.insecure: true for diagnostics)
  • Network problems between the operator pod and oVirt Engine
# Check OVirtProvider
kubectl get capovp ovirt-prod -o yaml

# Check the network from the operator's side
kubectl exec -n capov-system deployment/capov-controller-manager -- \
  wget -qO- https://ovirt.example.com/ovirt-engine/api --no-check-certificate

If status.failureReason = OVirtEngineConnectionCritical, you need to fix OVirtProvider and reset FailureReason (delete the field via patch or recreate OVirtMachine):

kubectl patch capovm worker-def456 -n default \
  --type=merge \
  -p '{"status":{"failureReason":null,"failureMessage":null}}'

Condition MachineCreated = False

VM not created in oVirt:

  • Template spec.template does not exist in oVirt
  • Cluster oVirt spec.ovirtcluster not found
  • spec.storagedomainname is specified, but a storage domain with that name is missing in oVirt or is not available to host disks
  • Bootstrap Secret is not ready yet (normally at the beginning of creation, operationor will try again)

Condition MachineDiskResized = False

  • The requested disk size osdisksizegb is smaller than the disk size in the template - cannot be reduced

Condition MachineNetworkSet = False

  • vNIC Profile with name from spec.nics[].vnicprofile was not found in oVirt

Condition K8sProviderIDSetup = False

  • The VM has not yet registered in the working cluster as Node
  • Problems with operator access to the worker cluster API

4.4 The machine is not deleted

kubectl describe capovm worker-def456 -n default | grep -A5 "Conditions"

If MachineDeleted = False with reason VMHasNonSystemDisk:

message: VM has non system disk, cannot delete

This means that there is a CSI disk connected to the VM, which has not yet been disconnected. The operator will try again every minute. Solution: Make sure PVC/PV is freed before removing Node.

If CSI is frozen, the disk can be detached manually in the oVirt interface - the operator will delete the VM in the next iteration.

5. Machine life cycle

5.1 Creation

The OVirtMachineReconciler controller performs the steps sequentially. If the step fails, the object goes into the retry queue. The progress of each step is reflected in status.conditions.

The storage domain for the disks of the new VM is specified by the optional field spec.storagedomainname. If it is not specified, the disks are created in the same storage domain as the template disks in oVirt (see §7.3).

Illustration: 5.1 Creation

OVirtMachine creation sequence (E2E)

5.2 Uninstallation

When deletionTimestamp is set, the controller follows the Deleting branch on the diagram: powers off the VM, deletes it in oVirt, and removes the finalizer. If a non-system (CSI) disk is attached to the VM, deletion is blocked until it is detached (see §4.4).

Illustration: 5.2 Uninstallation

OVirtMachine reconcile logic on deletion (Deleting branch)

5.3 Conditions

ConditionWhat does
CanConnectToOVirtThe operator successfully connected to the oVirt API using data from OVirtProvider
MachineCreatedThe VM was created in oVirt from a template with data transferred to cloud-init
MachineDiskResizedSystem disk expanded to osdisksizegb and disks renamed
MachineNetworkSetNetwork interfaces are configured (vNIC Profiles are connected)
MachineOnVM is up and running
K8sProviderIDSetupspec.providerID installed on the corresponding Node in the working cluster
MachineDeletedVM removed from oVirt (only present during removal process)
ReadySummary condition - all of the above are met

View terms:

kubectl get capovm worker-abc123 -n default \
  -o jsonpath='{.status.conditions}' | jq .

Example output for a machine in an intermediate state (starting):

[
  {"type": "CanConnectToOVirt", "status": "True"},
  {"type": "MachineCreated",    "status": "True"},
  {"type": "MachineDiskResized","status": "True"},
  {"type": "MachineNetworkSet", "status": "True"},
  {"type": "MachineOn",         "status": "False",
   "reason": "VMPoweringUp",    "severity": "Info",
   "message": "powering up"},
  {"type": "K8sProviderIDSetup","status": "False"}
]

5.4 Critical errors (FailureReason)

If the operator encounters an error after which automatic retry is impossible or meaningless, it sets status.failureReason and status.failureMessage.

FailureReasonReasonWhat to do
OVirtEngineConnectionCriticalObject OVirtProvider not found, invalid credentials or certificateCheck object OVirtProvider: fields engine, user, pass, tls
NodeHasIncorrectProviderIDOn Node in the working cluster another providerIDCheck kubectl get node -o wide in production cluster

6. Feature Gates

Feature gates - flags that change the behavior of the operator when working with VM disks. Passed through the argument --feature-gates (Helm: values.featureGates). In Shturval environment, they are set in customvalues.featureGates via SSP; It is not recommended to edit SSC for this purpose. The general procedure for working with SSP is How to manage CustomValues of services.

Format

--feature-gates=disk-keep-template-name=true,disk-add-vm-name=false

Unknown keys and invalid values are silently ignored.

Available gates

KeyDefaultDescription
disk-keep-template-nametrueWhen connecting a disk to a new VM, save the disk name from the template
disk-add-vm-namefalseWhen creating a VM, add the VM name to the disk name
disk-clonefalseClone a disk when creating a VM

Create or modify ShturvalServicePatch associated with shturval-capov (see How to manage CustomValues of services):

apiVersion: ops.shturval.tech/v1beta2
kind: ShturvalServicePatch
metadata:
  name: shturval-capov-featuregates
  labels:
    ops.shturval.tech/ssc: shturval-capov
spec:
  shturvalServiceConfigName: shturval-capov
  customvalues:
    featureGates:
      disk-add-vm-name: true
      disk-clone: false

SSP is overlaid on top of SSC.spec.customvalues. After applying or changing the patch, the platform operator updates the Deployment - the pod is recreated automatically.

Moving a disk to a Storage Domain

If you have specified a storage domain name (Storage Domain) in oVirt provider or in InfraMachineTemplate, where virtual machine disks will be created, you will need to enable disk cloning.

Apply ShturvalServicePatch to the management cluster:

apiVersion: ops.shturval.tech/v1beta2
kind: ShturvalServicePatch
metadata:
  name: shturval-capov-disk-clone
  labels:
    ops.shturval.tech/ssc: shturval-capov
spec:
  shturvalServiceConfigName: shturval-capov
  customvalues:
    featureGates:
      disk-clone: true

After applying the patch, wait for the shturval-capov service to update and repeat creating or restoring nodes.

Why do you need to rename disks?

When CAPI creates multiple VMs from the same template, oVirt inherits the disk aliases directly from the template. As a result, all disks of all created VMs receive the same aliases - the same as the template disks had.

The problem becomes obvious in the Storage → Disks section: this is a global directory of all disks in the data center. It is impossible to determine by name which cluster node this or that disk belongs to - all records look the same. Setting disk-add-vm-name: true fixes the problem: the VM name is added to the alias, the alias becomes unique and uniquely points to the owner VM.

Where the disk alias is visible in oVirt

Alias is the display name of the Disk object in oVirt. It is visible in two places:

  • Storage → Disks — global list of all disks in the data center. This is where identical aliases create confusion when there are a large number of nodes.
  • Compute → Virtual Machines → [VM] → Disks tab — list of disks connected to a specific VM.

Disk names: behavior based on flag combinations

In all the examples below: the VM name is worker-abc123, the template is rhcos-4.14.

disk-keep-template-name: true, disk-add-vm-name: false (default values)

Renaming is not performed. The operator does not touch aliases - they remain as oVirt assigned them when creating a VM from a template.

Disk alias in templateAlias ​​after VM creationChange
rhcos-4.14_Disk1rhcos-4.14_Disk1no
(empty)(empty)no

disk-keep-template-name: true, disk-add-vm-name: true

The VM name is inserted before the _Disk… suffix. The template part of the alias is preserved.

Disk alias in templateAlias ​​after VM creation (worker-abc123)Change
rhcos-4.14_Disk1rhcos-4.14_worker-abc123_Disk1yes

disk-keep-template-name: false, disk-add-vm-name: true

The template part of the alias is removed. The alias is built only from the VM name and the _Disk… suffix. If the VM name is already present in the alias, renaming is not performed.

Disk alias in templateAlias ​​after VM creation (worker-abc123)Change
rhcos-4.14_Disk1worker-abc123_Disk1yes
worker-abc123_Disk1worker-abc123_Disk1no (the VM name is already in the alias)

Behavior with disk-clone: true

When creating a VM, the feature gate disk-clone value is passed to the request to add a VM to oVirt as the Clone parameter: for disk-clone: true - Clone(true), for disk-clone: false - Clone(false). There is no other logic in the operator code that is tied to this flag, other than passing a Boolean value to this call.

Check the current feature gates in the logs when starting the pod:

kubectl logs -n capov-system deployment/capov-controller-manager | grep "feature gates"

7. CRD: Resource reference

7.1 OVirtProvider

Scope: cluster-scoped (no namespace) Short name: capovp Group: infrastructure.cluster.x-k8s.io/v1beta1

Stores connection parameters to oVirt Engine. One object can be used by several machines from different namespaces.

Specification fields

FieldTypeRequiredDescription
spec.enginestringYesFull oVirt Engine API URL. Example: https://ovirt.example.com/ovirt-engine/api
spec.userstringYesUsername to connect to oVirt Engine
spec.passstringYesUser Password
spec.tls.insecureboolYestrue - do not check the server’s TLS certificate (not recommended in production)
spec.tls.certboolNotrue - use system CAs to verify thecertificate
spec.tls.certsbytesNoPEM CA certificate in base64 to verify server certificate

Status

FieldTypeDescription
status.readyboolResult of checking connection to oVirt Engine

Examples

Option 1: Disable TLS checking (dev/test)

apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
kind: OVirtProvider
metadata:
  name: ovirt-dev
spec:
  engine: "https://ovirt-dev.example.com/ovirt-engine/api"
  user: "admin@internal"
  pass: "changeme"
  tls:
    insecure: true

Option 2: System CA (public certificate)

apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
kind: OVirtProvider
metadata:
  name: ovirt-prod
spec:
  engine: "https://ovirt.example.com/ovirt-engine/api"
  user: "admin@internal"
  pass: "s3cr3t"
  tls:
    insecure: false
    cert: true

Option 3: Own CA certificate

apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
kind: OVirtProvider
metadata:
  name: ovirt-internal
spec:
  engine: "https://ovirt.internal/ovirt-engine/api"
  user: "admin@internal"
  pass: "s3cr3t"
  tls:
    insecure: false
    certs: LS0tLS1CRUdJTi... # base64-encoded PEM

Check connection status:

kubectl get capovp
# NAME READY
# ovirt-prod true

7.2 OVirtCluster

Area: namespaced Short name: capovc Group: infrastructure.cluster.x-k8s.io/v1beta1

Represents an infrastructure cluster in oVirt. Created as part of a CAPI cluster: the Cluster object references OVirtCluster through the spec.infrastructureRef field.

Specification fields

FieldTypeRequiredDescription
spec.controlPlaneEndpoint.hoststringYesHostname or IP address of the API server of the created cluster
spec.controlPlaneEndpoint.portint32YesAPI Server Port

Status

FieldTypeDescription
status.readyboolThe infrastructure is ready (set by the controller immediately after creation)
status.conditionslistConditions (CAPI conditions)
status.failureDomainsmapFailure domains (not used in current implementation)

Example

apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
kind: OVirtCluster
metadata:
  name: my-cluster
  namespace: default
spec:
  controlPlaneEndpoint:
    host: "10.0.1.100" # VIP or control-plane balancer address
    port: 6443

Controllerbehavior:

  • When creating: adds finalizer capovmachine.infrastructure.cluster.x-k8s.io, sets status.ready = true.
  • On deletion: removes the finalizer - the object is deleted from etcd.
  • If object Cluster is paused (spec.paused: true), reconcile is skipped. (see the screenshot below)

OVirtCluster reconcile logic

Check status:

kubectl get capovc -n default
# NAME         CONTROLPLANE ENDPOINT   READY
# my-cluster   10.0.1.100              true

7.3 OVirtMachine

Area: namespaced Short name: capovm Group: infrastructure.cluster.x-k8s.io/v1beta1

Represents one virtual machine in oVirt. Created automatically by the CAPI controller based on OVirtMachineTemplate when scaling MachineDeployment or KubeadmControlPlane. Direct creation of OVirtMachine by an administrator is not a typical scenario.

Specification fields

FieldTypeRequiredDescription
spec.providerstringYesObject name OVirtProvider (cluster-scoped)
spec.ovirtclusterstringYesCluster name in oVirt (not Kubernetes cluster name)
spec.templatestringYesoVirt VM template name
spec.storagedomainnamestringNoThe name of the storage domain (datastore) in oVirt on which VM disks are created. If not specified, the disks are created in the same storage domain as the template disks
spec.vmtypestringNoVM type: desktop, server, high_performance
spec.cpu.socketsuintNoNumber of CPU sockets
spec.cpu.coresuintNoNumber of cores per socket
spec.cpu.threadsuintNoNumber of threads per core
spec.memory.sizembint64NoRAM capacity in MB
spec.memory.guaranteedmbint64NoGuaranteed RAM capacity in MB
spec.osdisksizegbint64NoSystem disk size in GB (minimum 1)
spec.nics[]listNoList of network interfaces
spec.nics[].vnicprofilestringYesvNIC Profile name in oVirt
spec.providerIDstringNoProviderID in the format capov://<UUID> (filled in automatically by the controller)

Status

FieldTypeDescription
status.readyboolThe car is ready (all conditions are met)
status.addressstringIPv4 address of the main interface
status.conditionslistDetailed progress (see section 5)
status.failureReasonMachineStatusError (CAPI)Critical error code (see §5.4); not an arbitrary string
status.failureMessagestringCritical error text

Example

apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
kind: OVirtMachine
metadata:
  name: worker-abc123
  namespace: default
spec:
  provider: ovirt-prod # name OVirtProvider
  ovirtcluster: "Default" # cluster name in oVirt (Default - standard)
  template: "rhcos-4.14" # VM template in oVirt
  # storagedomainname: "my-datastore" # optional: different storage domain for disks than the template
  vmtype:server
  CPU:
    sockets: 1
    cores: 4
    threads: 1
  memory:
    sizemb: 8192
    guaranteedmb: 4096
  osdisksizegb: 80
  nics:
    - vnicprofile: "ovirtmgmt" # vNIC Profile name in oVirt

Check the condition of the machine:

kubectl get capovm -n default
# NAME READY
# worker-abc123 true

kubectl describe capovm worker-abc123 -n default

7.4 OVirtMachineTemplate

Area: namespaced Group: infrastructure.cluster.x-k8s.io/v1beta1

Configuration template for OVirtMachine. Used by MachineDeployment and KubeadmControlPlane objects as spec.template.spec.infrastructureRef. The CAPOV controller does not directly manage this resource; it only reads it when needed.

Structure

OVirtMachineTemplate.spec.template
  ├── metadata (optional) - standard `labels` and `annotations` node (CAPI MachineTemplate contract; inherited to `Machine` and further to Node according to CAPI rules)
  └── spec - all fields `OVirtMachine.spec`, except `providerID` (it is set by the controller on the created machine)

The spec.template.spec field corresponds to the OVirtMachine schema (including storagedomainname, cpu, memory, nics, etc.).

Example

apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
kind: OVirtMachineTemplate
metadata:
  name: worker-template
  namespace: default
spec:
  template:
    # metadata:
    #labels:
    # node.kubernetes.io/instance-type: "standard"
    spec:
      provider: ovirt-prod
      ovirtcluster: "Default"
      template: "rhcos-4.14"
      # storagedomainname: "my-datastore"
      vmtype:server
      CPU:
        sockets: 1
        cores: 4
        threads: 1
      memory:
        sizemb: 8192
        guaranteedmb: 4096
      osdisksizegb: 80
      nics:
        - vnicprofile: "ovirtmgmt"

Changing OVirtMachineTemplate does not affect already created machines. To apply the changes, you need to update the link in MachineDeployment (rolling update).