Trilio Site Recovery for Kubernetes
Guide

Custom Resource Schemas

CRD structure and validation rules for site recovery resources


Overview

This page is the authoritative reference for every Custom Resource Definition (CRD) in the siterecovery.trilio.io/v1alpha1 API group. Trilio Site Recovery is driven entirely by these custom resources: you protect VMs, trigger failovers, run DR tests, and monitor replication health by creating and updating CRs with oc or kubectl. Understanding the schema, validation rules, and status fields of each resource is essential for scripting DR workflows, building GitOps pipelines, and diagnosing failures. Eight CRD kinds are registered on the quorum cluster (ProtectionZone, ProtectionGroup, ProtectionRequest, DRBDReplicationPolicy, FailoverRequest, TestFailover, ReplicationGroupStatus, RPOEvent); one additional kind (DRBDResource) is registered on each workload cluster only.


Prerequisites

Before working with these custom resources, ensure the following:

  • OpenShift 4.17 or later on the quorum cluster and both workload clusters. OpenShift 4.14–4.16 is within the stated support floor but not validated for Tech Preview; upstream Kubernetes is not supported.
  • Trilio Site Recovery operator installed on the quorum cluster via OLM (package site-recovery, channel tech-preview, catalog quay.io/triliovault/site-recovery-catalog) in the trilio-site-recovery-system namespace. The operator bundle registers the ProtectionZone CRD and deploys the protection-zone controller, site-recovery-backend, and site-recovery-frontend.
  • A ProtectionZone CR in Available phase before creating any zone-scoped resources. The ProtectionZone controller installs the quorum and workload control-plane Helm charts automatically once the zone reaches Available; those charts register the remaining seven quorum-side CRDs and the workload-side DRBDResource CRD.
  • oc CLI — required for OpenShift-specific paths (oc adm must-gather, Routes). Plain kubectl-only operation is not a supported mode.
  • Kubeconfig files for all three clusters (quorum, primary, DR) stored as Kubernetes Secrets in the zone namespace on the quorum cluster.
  • DRBD kernel module ≥ 9.x loaded on every worker node of both workload clusters.
  • LVM thin pool configured on each worker node of both workload clusters, backed by a dedicated block device.
  • TCP ports 7000–8000 inclusive open between primary and DR worker nodes (one port per replicated VM volume; port 8000 is mandatory — a firewall rule that stops at 7999 silently prevents replication on one volume).
  • TCP port 6443 open from the quorum cluster to both workload cluster API servers.
  • Sub-50 ms RTT between primary and DR clusters (required for Protocol C synchronous replication).
  • KubeVirt or OpenShift Virtualization (CNV) ≥ 1.0 on both workload clusters.

Installation

CRDs are installed automatically — you do not apply CRD manifests by hand in normal operation.

Quorum cluster CRDs (8 kinds) are installed in two stages:

  1. The OLM bundle installs the ProtectionZone CRD when you create the Subscription.
  2. The ProtectionZone controller installs the remaining seven quorum-side CRDs (DRBDReplicationPolicy, FailoverRequest, ProtectionGroup, ProtectionRequest, ReplicationGroupStatus, RPOEvent, TestFailover) by server-side-applying the charts embedded in the operator binary, using field manager site-recovery-pz-controller. Out-of-band edits to these CRDs are reverted within one reconcile cycle.

Workload cluster CRDs (1 kind) — DRBDResource — are installed on each workload cluster by the site-recovery-workload-control-plane chart, also applied automatically by the ProtectionZone controller. DRBDResource is never registered on the quorum cluster; running oc get drbdresource against the quorum cluster returns server doesn't have a resource type.

To verify CRD registration after the zone reaches Available:

# On the quorum cluster — expect 8 CRDs
oc get crd | grep siterecovery.trilio.io

Expected output:

drbdreplicationpolicies.siterecovery.trilio.io
failoverrequests.siterecovery.trilio.io
protectiongroups.siterecovery.trilio.io
protectionrequests.siterecovery.trilio.io
protectionzones.siterecovery.trilio.io
replicationgroupstatuses.siterecovery.trilio.io
rpoevents.siterecovery.trilio.io
testfailovers.siterecovery.trilio.io
# On each workload cluster — expect 1 CRD
oc --context=<workload-context> get crd | grep siterecovery.trilio.io

Expected output:

drbdresources.siterecovery.trilio.io

Namespace placement rules — get these wrong and queries return empty:

Resource kindLives onNamespace
ProtectionZoneQuorumdr-<name>
ProtectionGroup, ProtectionRequest, DRBDReplicationPolicy, FailoverRequest, TestFailover, ReplicationGroupStatus, RPOEventQuorumdr-<name>
DRBDResourceEach workload clusterVM's namespace
Quorum control-plane podsQuorumdr-<name>
Workload control-plane pods and node-agent DaemonSetEach workload clustertrilio-site-recovery-system

Configuration

All configuration is expressed as fields in the custom resource spec. There are no separate config files. The subsections below document each CRD's spec fields, validation rules, and significant defaults.


ProtectionZone

Cluster: Quorum · Namespace: dr-<name> · Short name: pz

A ProtectionZone groups exactly two workload clusters into one DR deployment. Creating it triggers automatic deployment of the quorum and workload control planes.

FieldTypeRequiredDefaultValid values / constraints
spec.displayNamestringYes—5–253 characters
spec.descriptionstringNo—Max 1024 characters
spec.storageBackendModestringYes—DRBD only. Linstor is accepted by the schema but rejected at admission: Linstor backend is not supported yet; use DRBD.
spec.clustersarrayYes—Exactly 2 entries (minItems: 2, maxItems: 2)
spec.clusters[].namestringYes—1–63 characters; unique within the zone; listMapKey
spec.clusters[].clusterIDstringYes—cluster1 or cluster2; the two entries must occupy different slots
spec.clusters[].kubeconfigSecretRefobjectYes—corev1.SecretKeySelector shape: name (Secret name) and key (key within the Secret)
spec.clusters[].distributionNamestringNoRHEL-CoreOS-9RHEL-CoreOS-9 or RHEL-CoreOS-10. Selects the DRBD kernel-module loader image. RHEL-CoreOS-10 is not validated in Tech Preview.
spec.logging.levelstringYes (if logging set)—error, info, debug, trace
spec.logging.controlPlaneLevelstringNoinherits levelSame enum; overrides level for quorum and workload control-plane managers only
spec.logging.nodeAgentLevelstringNoinherits levelSame enum; overrides level for DRBD node agents only

status.phase values: Provisioning → Available → (Degraded | Unavailable | Deleting)

  • Unavailable: at least one member cluster is unreachable.
  • Degraded: both clusters reachable but a provisioning stage failed.

kubectl get pz columns: Display Name, Backend, Clusters, Status, Age


ProtectionGroup

Cluster: Quorum · Namespace: dr-<name> · Short name: pg

Defines a set of VMs that fail over together as one atomic unit. One ProtectionRequest per VM in the group is expected.

FieldTypeRequiredDefaultNotes
spec.virtualMachinesarrayYes—At least 1 entry (minItems: 1)
spec.virtualMachines[].namestringYes—DNS-1123 label; must match the KubeVirt VM's metadata.name
spec.virtualMachines[].namespacestringNoCR's namespaceOmit to use the ProtectionGroup's own namespace
spec.virtualMachines[].bootOrderintegerNo—Lower values boot first during failover; VMs sharing a value boot as one parallel batch; VMs without a value boot last
spec.desiredStatestringNorunningrunning or stopped
spec.sourceClusterstringNo—cluster1 or cluster2
spec.sla.rtoTargetSecondsintegerYes (if sla set)—Target RTO in seconds
spec.sla.rpoTargetSecondsintegerNo0Recorded for compliance reporting. Not consumed by the replication monitor in Tech Preview.

status.state values: Protected | Syncing | Degraded | Failed

status.activeClusterID: cluster1 or cluster2 — which cluster currently holds the running VMs. Written only by the failover controller.

kubectl get pg columns: VMs, State, Active, Age


ProtectionRequest

Cluster: Quorum · Namespace: dr-<name> · Short names: pr, protect

Requests block-level DRBD protection for a single VM. The protection controller creates a pair of DRBDResource CRs (one per workload cluster), orchestrates initial sync, and switches the VM to DRBD-backed frontend PVCs.

FieldTypeRequiredDefaultNotes
spec.sourceClusterstringYes—cluster1 or cluster2 — the cluster where the VM currently runs
spec.virtualMachine.namestringYes—DNS-1123 label; 1–63 characters
spec.virtualMachine.namespacestringNoCR's namespace
spec.replicationConfig.replicationProtocolstringNoCC only in Tech Preview. A is accepted by the schema but rejected at admission: ReplicationProtocolA is not supported yet.
spec.replicationConfig.rpointegerNo0Must be 0 when replicationProtocol is C; non-zero is rejected at admission.
spec.replicationConfig.drbdMetadataConfig.storageClassNamestringNocluster defaultStorageClass for per-volume DRBD external-metadata PVCs. Must bind immediately — WaitForFirstConsumer classes are rejected during validation.
spec.replicationConfig.drbdMetadataConfig.sizequantityNo128MiMinimum 128Mi. Per-volume metadata PVC size.

status.phase values (exact): Pending → Validating → CreatingDRBD → Syncing → ReadyToActivate → Activating → Protected; terminal failure: Failed; teardown: Unprotecting

Key status fields:

FieldMeaning
status.phaseCurrent lifecycle phase
status.syncProgressInteger 0–100; mirrors slowestVolumeSyncProgress.percentage; read by the kubectl get Sync column
status.slowestVolumeSyncProgress.percentageAuthoritative VM-level sync progress — the least-advanced volume
status.drbdResourceRef.nameShared name of both DRBDResource copies (the DRBD resource name)
status.drbdResourceRef.portAllocated replication port (7000–8000)
status.activeClusterObserved cluster holding the DRBD Primary — confirmed only after the promoted copy reports kernel Primary
status.consistencySummaryHuman-readable rendering of the Consistent condition

Admission constraints:

  • replicationProtocol: A → rejected.
  • rpo non-zero with replicationProtocol: C → rejected.
  • A WaitForFirstConsumer metadata StorageClass → rejected during Validating phase.

Operational constraints:

  • The protected VM must be running when you create the ProtectionRequest.
  • spec.nodeName on the resulting DRBDResource is immutable once set. Moving a protected VM to a different node requires unprotect then re-protect (full initial resync).
  • Live migration and cold migration of a protected VM are denied by the admission webhook.
  • oc adm drain of a node hosting a running protected VM will not complete.

kubectl get pr columns: VM, Namespace, Phase, Sync, Age


DRBDReplicationPolicy

Cluster: Quorum · Namespace: dr-<name> · Short names: drp, drbdpolicy

Do not create or modify this resource. The ProtectionZone controller automatically creates exactly one policy per zone, named <zoneName>-default, with isDefault: true and Protocol C. Only the isDefault policy in a ProtectionRequest's namespace is consulted. All fields below are documented for reference and diagnostics only.

FieldTypeDefaultNotes
spec.clusters[].namestring—cluster1 or cluster2
spec.clusters[].storageClassMappingsobject—Maps source StorageClass names to DR-side StorageClass names. If a source class is absent, the source class name is used as-is on the target.
spec.clusters[].replicationEndpoint.addressstring—Override replication IP/hostname. When unset, node IPs are used. Accepted but not consumed in Tech Preview.
spec.clusters[].nodeSelectorobject—Accepted but not consumed in Tech Preview.
spec.replicationModestringSynchronousSynchronous only in Tech Preview. Asynchronous is accepted by the schema but not supported.
spec.drbdProtocolstringCC only in Tech Preview.
spec.isDefaultbooleanfalseAt most one default policy per namespace (webhook-enforced).
spec.networkConfig.portRangeobjectstart: 7000, end: 7999Accepted but not consumed. Ports are always allocated from the fixed 7000–8000 range regardless of this value.
spec.networkConfig.maxBuffersinteger8192DRBD max-buffers net tuning.
spec.networkConfig.maxEpochSizeinteger8192DRBD max-epoch-size net tuning.
spec.networkConfig.sndbufSizestring1MTCP send buffer size.
spec.networkConfig.rcvbufSizestring1MTCP receive buffer size.
spec.networkConfig.connectTimeoutstring10sDRBD connection timeout.
spec.networkConfig.pingTimeoutstring5sConnection-health ping timeout.
spec.diskConfig.onIoErrorstringdetachdetach, pass_on, or call-local-io-error
spec.diskConfig.fencingstringresource-onlydont-care, resource-only, or resource-and-stonith
spec.diskConfig.diskFlushesbooleantrue
spec.diskConfig.diskBarrierbooleantrue
spec.resyncConfig.ratestring100MMaximum resync rate.
spec.resyncConfig.verifybooleanfalseEnables periodic online verification.
spec.resyncConfig.verifyIntervalstring24hOnline-verify interval.
spec.rpo.maxLagSecondsinteger00 = unlimited (correct for Protocol C).
spec.rpo.alertThresholdSecondsinteger30Lag at which an RPO alert is raised.

kubectl get drp columns: Clusters, Mode, Protocol, Default, Phase, Age


DRBDResource

Cluster: Each workload cluster · Namespace: VM's namespace · Short name: dres

Represents all replicated disks of a single VM as one DRBD resource on one cluster side. The protection controller creates a pair — one per cluster — with the shared name drbd-<namespace>-<vm>-<hash8>. You never create this resource directly; you read it to observe replication state.

FieldTypeNotes
spec.sidestringSource or Target. Immutable; does not change on failover.
spec.rolestringPrimary or Secondary. Mutable; written by the protection controller.
spec.nodeNamestringNode this copy is pinned to. Immutable once set.
spec.portintegerReplication port (7000–8000). Shared across both copies.
spec.volumesarray1–15 entries (listMapKey: volumeNumber).
spec.volumes[].volumeNumberintegerStable cross-copy join key. 0 = boot disk.
spec.volumes[].pvcNamestringLocal backing PVC for this copy.
spec.volumes[].metadata.sizequantityDefault 128Mi. External-metadata PVC size per volume.
spec.replication.protocolstringA or C (materialized from the default DRBDReplicationPolicy; always C in Tech Preview).
spec.pausedbooleanDefault false. Suspends agent convergence (maintenance knob).
spec.replicationIntentstringEnabled (default) or Suspended. Suspended → Enabled in-place is denied by the webhook; only unprotect → re-protect restores it.
spec.endpointsarrayPopulated by the protection controller once both copies report ReadyToActivate. Delivery of endpoints is the activation signal.

status.phase values: Pending | Provisioning | ReadyToActivate | Syncing | Consistent | Degraded | Failed | Deleting

Key status fields:

FieldMeaning
status.roleObserved DRBD role (Primary, Secondary, Unknown)
status.connectionStateResource-scoped connection state: Connected, Connecting, Disconnected, StandAlone, Unknown
status.volumes[].replicationStatePer-volume replication state: Established, SyncSource, SyncTarget, PausedSyncS, PausedSyncT, Off, Unknown
status.volumes[].syncProgress.percentagePer-volume sync progress 0–100
status.volumes[].metadata.pvcNameExternal-metadata PVC name: drbd-md-<resourceName>-v<volumeNumber>
status.conditionsIncludes ConditionPlacementReady which gates metadata-PVC provisioning

Deletion guard: A DRBDResource with a frontend PVC still mounted cannot be deleted. In emergencies, annotate with siterecovery.trilio.io/force-delete to override the guard.

kubectl get dres columns: Side, Role, Node, State, Phase, Age


FailoverRequest

Cluster: Quorum · Namespace: dr-<name> · Short name: fr

Triggers a planned or unplanned failover of a ProtectionGroup from its current active cluster to a target cluster.

FieldTypeRequiredDefaultNotes
spec.protectionGroupRef.namestringYes—Name of the ProtectionGroup to fail over
spec.protectionGroupRef.namespacestringNoCR's namespace
spec.targetClusterstringYes—cluster1 or cluster2 — the cluster to fail over to
spec.failoverTypestringNoplannedplanned (graceful, waits for sync) or unplanned (force-promote, does not wait for primary shutdown)
spec.batchBootTimeoutSecondsintegerNo300Max seconds to wait for one boot-order batch's VMs to reach Running before proceeding with a warning.
spec.drainTimeoutSecondsintegerNo300Accepted and range-checked (30–3600) but not consumed by the failover controller in Tech Preview. Do not use this field to control drain behavior.

status.state values: Pending | InProgress | Completed | Failed | Cancelled

status.phase values (granular): Initializing → Validating → StoppingSource → WaitingForLocks → RemovingTaints → StartingTarget → Monitoring → Completed | Failed

Per-VM detail: status.vmStatuses[] — one entry per VM with name, phase (Pending | Stopping | Stopped | Starting | Running | Failed), sourceStatus, targetStatus, and error. There is no status.message field and no status.failedVMs field on this resource.

Unplanned failover behavior: DRBD volumes are force-promoted on the DR cluster without waiting for the primary to shut down. If replication was Consistent at the moment of failure, zero data loss occurs. If replication was Degraded, minimal data loss may occur.

Failover wall-clock budget: 5 minutes.

kubectl get fr columns: Protection Group, Target, State, Phase, Age


TestFailover

Cluster: Quorum · Namespace: dr-<name> · Short name: tfo

Triggers a non-disruptive DR validation using volume snapshots on the DR cluster. Production replication is not interrupted. Test VMs run in an isolated namespace created and managed by the controller.

FieldTypeRequiredDefaultNotes
spec.protectionGroupRef.namestringYes—ProtectionGroup to test
spec.protectionGroupRef.namespacestringNoCR's namespace
spec.cleanupPolicystringNoAutomaticAutomatic — cleans up retentionTime after completion; Manual — resources remain until you delete the CR
spec.retentionTimestringNo2hPattern: ^[0-9]+(s|m|h|d)$. Only meaningful when cleanupPolicy: Automatic.
spec.timeoutstringNo(none)Pattern: ^[0-9]+(s|m|h)$. No default by design — test duration is data-size-dominated. Set only to enforce a specific SLA.
spec.batchBootTimeoutSecondsintegerNo300Per-batch boot timeout during StartingVMs phase.
spec.verification.reachabilityarrayNo—Optional per-VM network probes. Boot/agent/filesystem checks are automatic and always run.
spec.verification.reachability[].typestringYes (per probe)—tcp or http
spec.verification.reachability[].portintegerYes (per probe)—1–65535
spec.verification.reachability[].vmNamestringNo—Restrict probe to this VM (source VM name from the ProtectionGroup). Empty = probe all VMs.
spec.verification.reachability[].endpointstringNo/HTTP path. http type only.
spec.verification.reachability[].expectedStatusintegerNo200Expected HTTP status code (100–599). http type only.
spec.verification.reachability[].timeoutstringNo30sPattern: ^[0-9]+(s|m|h)$

status.phase values (exact): Queued → PreparingEnvironment → CreatingSnapshots → CreatingVolumes → CreatingVMs → StartingVMs → Verifying → Succeeded | Failed → CleaningUp → Cleaned

Key status fields:

FieldMeaning
status.testNamespaceGenerated namespace for test VMs: site-recovery-<protection-group>-<random>. Set by the controller; never user-provided.
status.targetClustercluster1 or cluster2 — where test VMs are running
status.targetClusterReasonWhy the controller selected that cluster: vm_runstrategy, pg_status, or pg_spec_sourceCluster
status.verificationResults.passedOverall verification result
status.verificationResults.checks[]Per-check detail with vm, type, passed, message

Important: Do not attempt to construct, guess, or manually grep for the test namespace. Read status.testNamespace only. Do not patch finalizers to force-delete a TestFailover CR.

Network isolation: On OpenShift with OVN-Kubernetes, a NetworkPolicy perimeter is created in PreparingEnvironment phase. On non-OVN CNI, network isolation silently fails — test VMs may reach production networks.

kubectl get tfo columns: Protection Group, Phase, Passed, Age


ReplicationGroupStatus

Cluster: Quorum · Namespace: dr-<name> · Short name: rgs

Aggregates DRBD replication health for a ProtectionGroup. The replication monitor auto-creates exactly one per ProtectionGroup, named <protection-group-name>-replication. Do not create this resource manually.

FieldTypeDefaultNotes
spec.protectionGroupRef.namestring—Required; references the ProtectionGroup being monitored
spec.pollingIntervalSecondsinteger15How often DRBD metrics are polled (5–300)
spec.replicationProtocolstringC
spec.rpoObjectiveSecondsinteger0Not consumed in Tech Preview.

Key status fields:

FieldMeaning
status.rpoStatusMet, Degraded, Violated, or Unknown
status.aggregated.outOfSyncBytesTotal out-of-sync bytes across all VMs in the group
status.aggregated.totalVolumeCountNumber of DRBD resources (= number of VMs) in the group; one per VM since TSR-90, not one per disk
status.aggregated.syncedVolumeCountNumber of VMs fully in sync (all disks in sync)
status.aggregated.connectionStateWorst-case connection state across all VMs: Connected, Degraded, or Disconnected
status.volumeReplicationStatuses[]One entry per VM (not per disk). Fields: vmName, drbdResourceName, pvcName (boot disk only), outOfSyncBytes, replicationState, connectionState, writeLatencyMilliseconds
status.lastFullySyncedAtWhen every VM in the group was last fully in sync

Label for filtering: siterecovery.trilio.io/protection-group: <pg-name> — use this label selector because CRD field selectors on spec.* or status.* are not supported.

kubectl get rgs columns: Protection-Group, RPO-Status, Out-Of-Sync, Synced, Connection, Last-Sync, Age


RPOEvent

Cluster: Quorum · Namespace: dr-<name> · Short name: rpoe

An immutable audit record of a single replication event. Created by the replication monitor on RPO status transitions and significant replication events. Has no status subresource — all data is in spec.

FieldTypeRequiredNotes
spec.protectionGroupRef.namestringYes
spec.eventTypestringYesSyncCompleted, RPODegraded, RPOViolated, ConnectionLost, ConnectionRestored, ResyncStarted, ResyncCompleted, VolumeAdded, VolumeRemoved, MetadataUndersized, ResizePending, ResizeComplete
spec.timestampstringYesRFC 3339 timestamp of the event
spec.rpoAtEventintegerNoEstimated RPO in seconds at event time
spec.outOfSyncBytesAtEventintegerNoTotal out-of-sync bytes at event time
spec.previousRPOStatusstringNoMet, Degraded, Violated, or Unknown
spec.newRPOStatusstringNoSame enum
spec.violationReasonstringNoConnectionLost, NetworkTimeout, SyncLagExceeded, ProtocolCOutOfSync, ResyncInProgress, Unknown
spec.messagestringNoHuman-readable description
spec.affectedVolumes[]arrayNoPer-VM replication state at event time
spec.connectionDetailsobjectNoConnectivity detail for connection events
spec.recoveryEstimateobjectNoEstimated recovery time

Label for filtering: siterecovery.trilio.io/protection-group: <pg-name>

kubectl get rpoe columns: Protection-Group, Event-Type, RPO-Seconds, Out-Of-Sync, Reason, Timestamp, Age


Usage

The following patterns cover the most common day-to-day operations. All commands run against the quorum cluster unless noted otherwise. Substitute dr-<name> with your zone namespace.

List all resources in a zone namespace:

oc get pz,pg,pr,fr,tfo,rgs,rpoe -n dr-<name>

Check zone health:

oc get pz -n dr-<name>
# STATUS column should read: Available

Check replication health for all protection groups:

oc get rgs -n dr-<name>
# RPO-Status column: Met = healthy; Degraded or Violated = investigate

Find the replication status for a specific ProtectionGroup (filter by label):

oc get rgs -n dr-<name> \
  -l siterecovery.trilio.io/protection-group=<pg-name>

Note: --field-selector spec.protectionGroupRef.name=<pg-name> does not work on custom resources. Use the label selector shown above.

Watch protection progress for a VM:

oc get pr -n dr-<name> -w
# Phase column: Pending → Validating → CreatingDRBD → Syncing → ReadyToActivate → Activating → Protected
# Sync column: integer 0-100

Inspect per-volume replication state (on the workload cluster):

oc --context=<workload-context> get dres -n <vm-namespace>
# State column: Connected = healthy; Disconnected = investigate
# Phase column: Consistent = fully in sync

Watch a failover in progress:

oc get fr -n dr-<name> -w
# State column: Pending → InProgress → Completed
# Phase column: Initializing → Validating → StoppingSource → … → Completed

Inspect per-VM failover detail:

oc get fr <fr-name> -n dr-<name> -o jsonpath='{.status.vmStatuses}'

Check recent RPO events:

oc get rpoe -n dr-<name> --sort-by=.spec.timestamp

Identify all protected VMs across both workload clusters:

# On each workload cluster
oc --context=<workload-context> get vm -A \
  -l siterecovery.trilio.io/protected-vm=true

Adjust runtime log level without restarting pods (takes effect within ~15s):

oc patch pz <zone-name> -n dr-<name> \
  --type=merge \
  -p '{"spec":{"logging":{"level":"debug"}}}'

Examples

Example 1: ProtectionZone CR

Creates a zone pairing two OpenShift clusters. The zone namespace must be dr-<metadata.name>.

apiVersion: siterecovery.trilio.io/v1alpha1
kind: ProtectionZone
metadata:
  name: prod-dr
  namespace: dr-prod-dr
spec:
  displayName: "Production DR Zone"
  description: "Pairs the primary site (cluster1) with the DR site (cluster2)."
  storageBackendMode: DRBD
  clusters:
    - name: primary
      clusterID: cluster1
      distributionName: RHEL-CoreOS-9
      kubeconfigSecretRef:
        name: cluster1-kubeconfig
        key: kubeconfig
    - name: dr-site
      clusterID: cluster2
      distributionName: RHEL-CoreOS-9
      kubeconfigSecretRef:
        name: cluster2-kubeconfig
        key: kubeconfig

Expected kubectl get pz output after the zone becomes Available:

NAME      DISPLAY NAME          BACKEND   CLUSTERS          STATUS      AGE
prod-dr   Production DR Zone    DRBD      primary,dr-site   Available   3m

Example 2: ProtectionGroup CR

Groups three VMs with boot ordering. The db-primary VM boots first (order 1), then db-replica and app-server boot in parallel (both order 2).

apiVersion: siterecovery.trilio.io/v1alpha1
kind: ProtectionGroup
metadata:
  name: app-stack
  namespace: dr-prod-dr
spec:
  sourceCluster: cluster1
  desiredState: running
  virtualMachines:
    - name: db-primary
      namespace: app-production
      bootOrder: 1
    - name: db-replica
      namespace: app-production
      bootOrder: 2
    - name: app-server
      namespace: app-production
      bootOrder: 2
  sla:
    rtoTargetSeconds: 300

Expected kubectl get pg output when all VMs are protected and synced:

NAME        VMS                               STATE       ACTIVE     AGE
app-stack   db-primary,db-replica,app-server  Protected   cluster1   12m

Example 3: ProtectionRequest CR

Protects a single VM on cluster1. The replicationConfig block is optional — omitting it selects all defaults (Protocol C, 128Mi metadata PVCs from the cluster's default StorageClass).

apiVersion: siterecovery.trilio.io/v1alpha1
kind: ProtectionRequest
metadata:
  name: protect-db-primary
  namespace: dr-prod-dr
spec:
  sourceCluster: cluster1
  virtualMachine:
    name: db-primary
    namespace: app-production
  replicationConfig:
    replicationProtocol: C
    rpo: 0
    drbdMetadataConfig:
      storageClassName: fast-ssd
      size: 128Mi

Watch protection progress:

oc get pr protect-db-primary -n dr-prod-dr -w
NAME                VM           NAMESPACE         PHASE       SYNC   AGE
protect-db-primary  db-primary   app-production    Syncing     47     2m
protect-db-primary  db-primary   app-production    Protected   100    4m

Example 4: Planned FailoverRequest CR

Fails over the app-stack ProtectionGroup to cluster2. The failover controller gracefully shuts down VMs on cluster1, promotes DRBD volumes on cluster2, and starts VMs there.

apiVersion: siterecovery.trilio.io/v1alpha1
kind: FailoverRequest
metadata:
  name: failover-to-dr
  namespace: dr-prod-dr
spec:
  protectionGroupRef:
    name: app-stack
  targetCluster: cluster2
  failoverType: planned
  batchBootTimeoutSeconds: 300

Watch the failover:

oc get fr failover-to-dr -n dr-prod-dr -w
NAME              PROTECTION GROUP   TARGET     STATE       PHASE            AGE
failover-to-dr    app-stack          cluster2   InProgress  StoppingSource   30s
failover-to-dr    app-stack          cluster2   InProgress  StartingTarget   90s
failover-to-dr    app-stack          cluster2   Completed   Completed        3m

Inspect per-VM status during the failover:

oc get fr failover-to-dr -n dr-prod-dr \
  -o jsonpath='{range .status.vmStatuses[*]}{.name}{"\t"}{.phase}{"\t"}{.error}{"\n"}{end}'

Example 5: Unplanned FailoverRequest CR

Used when cluster1 is unavailable. DRBD volumes are force-promoted on cluster2 without waiting for the primary to shut down.

apiVersion: siterecovery.trilio.io/v1alpha1
kind: FailoverRequest
metadata:
  name: emergency-failover
  namespace: dr-prod-dr
spec:
  protectionGroupRef:
    name: app-stack
  targetCluster: cluster2
  failoverType: unplanned

Example 6: TestFailover CR

Runs a non-disruptive DR test. Volume snapshots are taken from the DR cluster, test VMs are started in an isolated namespace, automatic boot/agent/filesystem checks run, and then an HTTP probe verifies the app-server VM is serving traffic on port 8080.

apiVersion: siterecovery.trilio.io/v1alpha1
kind: TestFailover
metadata:
  name: quarterly-dr-test
  namespace: dr-prod-dr
spec:
  protectionGroupRef:
    name: app-stack
  cleanupPolicy: Automatic
  retentionTime: 4h
  verification:
    reachability:
      - type: http
        vmName: app-server
        port: 8080
        endpoint: /healthz
        expectedStatus: 200
        timeout: 30s

Check test results:

oc get tfo quarterly-dr-test -n dr-prod-dr
NAME                PROTECTION GROUP   PHASE       PASSED   AGE
quarterly-dr-test   app-stack          Succeeded   true     18m

Read the generated test namespace (do not guess or construct it):

oc get tfo quarterly-dr-test -n dr-prod-dr \
  -o jsonpath='{.status.testNamespace}'

Read per-check verification detail:

oc get tfo quarterly-dr-test -n dr-prod-dr \
  -o jsonpath='{range .status.verificationResults.checks[*]}{.vm}{"\t"}{.type}{"\t"}{.passed}{"\t"}{.message}{"\n"}{end}'

Example 7: Checking DRBDResource replication state on a workload cluster

# List all DRBDResources in the VM's namespace on the primary workload cluster
oc --context=<primary-context> get dres -n app-production
NAME                              SIDE     ROLE      NODE             STATE       PHASE       AGE
drbd-app-production-db-primary-a1b2c3d4   Source   Primary   worker-0   Connected   Consistent  10m

Inspect per-volume sync progress:

oc --context=<primary-context> get dres drbd-app-production-db-primary-a1b2c3d4 \
  -n app-production \
  -o jsonpath='{range .status.volumes[*]}{.volumeNumber}{"\t"}{.replicationState}{"\t"}{.syncProgress.percentage}{"\n"}{end}'

Troubleshooting

Use the following format for each issue: Symptom, Likely cause, Fix.


Issue 1: ProtectionZone admission denied — Linstor backend is not supported yet; use DRBD

Symptom: oc apply of a ProtectionZone CR returns an admission webhook error containing the message above.

Likely cause: spec.storageBackendMode is set to Linstor.

Fix: Change spec.storageBackendMode to DRBD. This is the only accepted value in Tech Preview.

spec:
  storageBackendMode: DRBD

Issue 2: ProtectionRequest stuck in Failed phase with Validated=False

Symptom: The ProtectionRequest reaches the Validating phase and then transitions to Failed. A Validated=False condition and a warning event describe the rejection.

Likely causes:

  • One or more of the VM's disks is not volumeMode: Block.
  • The VM is not running at the time of protection (the VM must be running).
  • The sourceCluster field references a clusterID that does not match either cluster1 or cluster2.

Fix: Correct the VM configuration or the CR field that the warning event identifies. Delete the failed ProtectionRequest and create a new one—the controller does not retry failed requests.

# Read the event for the exact reason
oc describe protectionrequest <name> -n <namespace> --context=primary-cluster

Issue 3: oc adm drain does not complete on a node hosting a protected VM

Symptom: oc adm drain <node> hangs or reports that eviction of a VM pod was denied.

Likely cause: The node hosts a running protected VM. All KubeVirt migration paths (user-initiated, evacuation, drain) are realised as VirtualMachineInstanceMigration objects, and the TSR admission webhook denies these for protected VMs.

Fix: There is no way to drain a node hosting a running protected VM without first unprotecting the VM. To perform node maintenance:

  • For in-place maintenance (reboot, kernel update): proceed with the reboot. DRBD modules reload on boot, the anchor pod returns to the same node, and DRBD reconnects and resynchronises the delta.
  • For permanent node replacement: unprotect the VM, perform the node replacement, then protect the VM again (full initial resync required).

Issue 4: TestFailover stuck in CleaningUp or not cleaning up

Symptom: The TestFailover CR has been in CleaningUp for an extended period, or resources remain in the test namespace after the test completed.

Likely cause: cleanupPolicy: Manual was set, or the test failed partway through cleanup.

Fix: Do not patch finalizers. Delete the TestFailover CR through the normal API and the controller will handle cleanup:

oc delete testfailover <name> -n dr-production --context=quorum-cluster

If cleanup is blocked, inspect the test namespace (read from status.testNamespace) for stuck resources:

TEST_NS=$(oc get testfailover <name> -n dr-production \
  --context=quorum-cluster \
  -o jsonpath='{.status.testNamespace}')

oc get all -n "$TEST_NS" --context=dr-cluster

Issue 5: FailoverRequest transitions to Failed before completing

Symptom: The FailoverRequest transitions from InProgress directly to Failed.

Likely causes:

  • The failover exceeded the 5-minute wall-clock budget.
  • VMs failed to start on the target cluster within batchBootTimeoutSeconds.
  • DRBD volume promotion failed on the target side.

Fix:

  1. Check per-VM detail in status.vmStatuses:
oc get failoverrequest <name> -n dr-production \
  --context=quorum-cluster \
  -o jsonpath='{.status.vmStatuses}'
  1. Check the failover-controller logs on the quorum cluster:
oc logs -l app.kubernetes.io/part-of=site-recovery,app.kubernetes.io/component=quorum-control-plane \
  -n dr-production \
  --context=quorum-cluster \
  -c manager
  1. Check DRBD resource state on both workload clusters:
oc get drbdresource -n <vm-namespace> --context=primary-cluster
oc get drbdresource -n <vm-namespace> --context=dr-cluster
  1. If the primary cluster is unreachable and you used failoverType: planned, re-apply the request with failoverType: unplanned to force-promote without waiting for the primary.

Issue 6: ReplicationGroupStatus shows Degraded or Critical

Symptom: oc get replicationgroupstatus reports Degraded or Critical health for a Protection Group.

Likely causes:

  • Network connectivity between primary and DR worker nodes on TCP ports 7000–8000 has been interrupted.
  • The DRBD node agent on a worker node is not running or has not converged (check for the siterecovery.trilio.io/not-ready:NoSchedule taint on nodes).
  • A worker node hosting a protected VM was lost.

Fix:

  1. Check RPOEvent records for the affected group:
oc get rpoevent -n <vm-namespace> --context=primary-cluster
  1. Check node agent DaemonSet pod status on both workload clusters:
oc get pods -l app.kubernetes.io/part-of=site-recovery,app.kubernetes.io/component=workload-control-plane \
  -n <agent-namespace> --context=primary-cluster

oc get pods -l app.kubernetes.io/part-of=site-recovery,app.kubernetes.io/component=workload-control-plane \
  -n <agent-namespace> --context=dr-cluster
  1. Check for the node taint:
oc get nodes --context=primary-cluster \
  -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.taints}{"\n"}{end}'
  1. If a node was permanently lost, follow the node-loss recovery runbook at docs/runbooks/drbdresource-node-loss.md. Remove only the agent finalizer—removing the placement finalizer will permanently leak a PVC/PV pair.

Issue 7: replicationProtocol: A rejected at admission

Symptom: Applying a DRBDReplicationPolicy with drbdProtocol: A results in an admission error: ReplicationProtocolA is not supported yet.

Likely cause: Protocol A (asynchronous replication) is not supported in Tech Preview. The DRBDReplicationPolicy is also created automatically by the ProtectionZone controller—you should not be creating it manually.

Fix: Do not create or modify DRBDReplicationPolicy objects. The ProtectionZone controller creates the default policy (<zoneName>-default) automatically when the zone reaches Available. Only synchronous replication (Protocol C, RPO=0) is supported in Tech Preview.


Issue 8: Collecting a diagnostic bundle

Symptom: You need to provide a support bundle to Trilio support.

Fix: Run tsr-gather via oc adm must-gather against the quorum cluster. This collects logs, CRD state, and configuration from all clusters in the DR estate without capturing secrets or credentials:

oc adm must-gather \
  --image=quay.io/triliovault/tsr-gather:latest \
  --context=quorum-cluster \
  -- /usr/bin/tsr-gather

The resulting bundle in ./must-gather.local.* can be shared with support.