Custom Resource Schemas
CRD structure and validation rules for site recovery resources
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.
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, channeltech-preview, catalogquay.io/triliovault/site-recovery-catalog) in thetrilio-site-recovery-systemnamespace. The operator bundle registers theProtectionZoneCRD and deploys the protection-zone controller, site-recovery-backend, and site-recovery-frontend. - A
ProtectionZoneCR inAvailablephase before creating any zone-scoped resources. The ProtectionZone controller installs the quorum and workload control-plane Helm charts automatically once the zone reachesAvailable; those charts register the remaining seven quorum-side CRDs and the workload-sideDRBDResourceCRD. ocCLI — required for OpenShift-specific paths (oc adm must-gather, Routes). Plainkubectl-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.
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:
- The OLM bundle installs the
ProtectionZoneCRD when you create theSubscription. - 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 managersite-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 kind | Lives on | Namespace |
|---|---|---|
ProtectionZone | Quorum | dr-<name> |
ProtectionGroup, ProtectionRequest, DRBDReplicationPolicy, FailoverRequest, TestFailover, ReplicationGroupStatus, RPOEvent | Quorum | dr-<name> |
DRBDResource | Each workload cluster | VM's namespace |
| Quorum control-plane pods | Quorum | dr-<name> |
| Workload control-plane pods and node-agent DaemonSet | Each workload cluster | trilio-site-recovery-system |
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.
| Field | Type | Required | Default | Valid values / constraints |
|---|---|---|---|---|
spec.displayName | string | Yes | — | 5–253 characters |
spec.description | string | No | — | Max 1024 characters |
spec.storageBackendMode | string | Yes | — | DRBD only. Linstor is accepted by the schema but rejected at admission: Linstor backend is not supported yet; use DRBD. |
spec.clusters | array | Yes | — | Exactly 2 entries (minItems: 2, maxItems: 2) |
spec.clusters[].name | string | Yes | — | 1–63 characters; unique within the zone; listMapKey |
spec.clusters[].clusterID | string | Yes | — | cluster1 or cluster2; the two entries must occupy different slots |
spec.clusters[].kubeconfigSecretRef | object | Yes | — | corev1.SecretKeySelector shape: name (Secret name) and key (key within the Secret) |
spec.clusters[].distributionName | string | No | RHEL-CoreOS-9 | RHEL-CoreOS-9 or RHEL-CoreOS-10. Selects the DRBD kernel-module loader image. RHEL-CoreOS-10 is not validated in Tech Preview. |
spec.logging.level | string | Yes (if logging set) | — | error, info, debug, trace |
spec.logging.controlPlaneLevel | string | No | inherits level | Same enum; overrides level for quorum and workload control-plane managers only |
spec.logging.nodeAgentLevel | string | No | inherits level | Same 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.
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
spec.virtualMachines | array | Yes | — | At least 1 entry (minItems: 1) |
spec.virtualMachines[].name | string | Yes | — | DNS-1123 label; must match the KubeVirt VM's metadata.name |
spec.virtualMachines[].namespace | string | No | CR's namespace | Omit to use the ProtectionGroup's own namespace |
spec.virtualMachines[].bootOrder | integer | No | — | Lower values boot first during failover; VMs sharing a value boot as one parallel batch; VMs without a value boot last |
spec.desiredState | string | No | running | running or stopped |
spec.sourceCluster | string | No | — | cluster1 or cluster2 |
spec.sla.rtoTargetSeconds | integer | Yes (if sla set) | — | Target RTO in seconds |
spec.sla.rpoTargetSeconds | integer | No | 0 | Recorded 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.
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
spec.sourceCluster | string | Yes | — | cluster1 or cluster2 — the cluster where the VM currently runs |
spec.virtualMachine.name | string | Yes | — | DNS-1123 label; 1–63 characters |
spec.virtualMachine.namespace | string | No | CR's namespace | |
spec.replicationConfig.replicationProtocol | string | No | C | C only in Tech Preview. A is accepted by the schema but rejected at admission: ReplicationProtocolA is not supported yet. |
spec.replicationConfig.rpo | integer | No | 0 | Must be 0 when replicationProtocol is C; non-zero is rejected at admission. |
spec.replicationConfig.drbdMetadataConfig.storageClassName | string | No | cluster default | StorageClass for per-volume DRBD external-metadata PVCs. Must bind immediately — WaitForFirstConsumer classes are rejected during validation. |
spec.replicationConfig.drbdMetadataConfig.size | quantity | No | 128Mi | Minimum 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:
| Field | Meaning |
|---|---|
status.phase | Current lifecycle phase |
status.syncProgress | Integer 0–100; mirrors slowestVolumeSyncProgress.percentage; read by the kubectl get Sync column |
status.slowestVolumeSyncProgress.percentage | Authoritative VM-level sync progress — the least-advanced volume |
status.drbdResourceRef.name | Shared name of both DRBDResource copies (the DRBD resource name) |
status.drbdResourceRef.port | Allocated replication port (7000–8000) |
status.activeCluster | Observed cluster holding the DRBD Primary — confirmed only after the promoted copy reports kernel Primary |
status.consistencySummary | Human-readable rendering of the Consistent condition |
Admission constraints:
replicationProtocol: A→ rejected.rponon-zero withreplicationProtocol: C→ rejected.- A
WaitForFirstConsumermetadata StorageClass → rejected during Validating phase.
Operational constraints:
- The protected VM must be running when you create the ProtectionRequest.
spec.nodeNameon the resultingDRBDResourceis 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 drainof 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, withisDefault: trueand Protocol C. Only theisDefaultpolicy in a ProtectionRequest's namespace is consulted. All fields below are documented for reference and diagnostics only.
| Field | Type | Default | Notes |
|---|---|---|---|
spec.clusters[].name | string | — | cluster1 or cluster2 |
spec.clusters[].storageClassMappings | object | — | 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.address | string | — | Override replication IP/hostname. When unset, node IPs are used. Accepted but not consumed in Tech Preview. |
spec.clusters[].nodeSelector | object | — | Accepted but not consumed in Tech Preview. |
spec.replicationMode | string | Synchronous | Synchronous only in Tech Preview. Asynchronous is accepted by the schema but not supported. |
spec.drbdProtocol | string | C | C only in Tech Preview. |
spec.isDefault | boolean | false | At most one default policy per namespace (webhook-enforced). |
spec.networkConfig.portRange | object | start: 7000, end: 7999 | Accepted but not consumed. Ports are always allocated from the fixed 7000–8000 range regardless of this value. |
spec.networkConfig.maxBuffers | integer | 8192 | DRBD max-buffers net tuning. |
spec.networkConfig.maxEpochSize | integer | 8192 | DRBD max-epoch-size net tuning. |
spec.networkConfig.sndbufSize | string | 1M | TCP send buffer size. |
spec.networkConfig.rcvbufSize | string | 1M | TCP receive buffer size. |
spec.networkConfig.connectTimeout | string | 10s | DRBD connection timeout. |
spec.networkConfig.pingTimeout | string | 5s | Connection-health ping timeout. |
spec.diskConfig.onIoError | string | detach | detach, pass_on, or call-local-io-error |
spec.diskConfig.fencing | string | resource-only | dont-care, resource-only, or resource-and-stonith |
spec.diskConfig.diskFlushes | boolean | true | |
spec.diskConfig.diskBarrier | boolean | true | |
spec.resyncConfig.rate | string | 100M | Maximum resync rate. |
spec.resyncConfig.verify | boolean | false | Enables periodic online verification. |
spec.resyncConfig.verifyInterval | string | 24h | Online-verify interval. |
spec.rpo.maxLagSeconds | integer | 0 | 0 = unlimited (correct for Protocol C). |
spec.rpo.alertThresholdSeconds | integer | 30 | Lag 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.
| Field | Type | Notes |
|---|---|---|
spec.side | string | Source or Target. Immutable; does not change on failover. |
spec.role | string | Primary or Secondary. Mutable; written by the protection controller. |
spec.nodeName | string | Node this copy is pinned to. Immutable once set. |
spec.port | integer | Replication port (7000–8000). Shared across both copies. |
spec.volumes | array | 1–15 entries (listMapKey: volumeNumber). |
spec.volumes[].volumeNumber | integer | Stable cross-copy join key. 0 = boot disk. |
spec.volumes[].pvcName | string | Local backing PVC for this copy. |
spec.volumes[].metadata.size | quantity | Default 128Mi. External-metadata PVC size per volume. |
spec.replication.protocol | string | A or C (materialized from the default DRBDReplicationPolicy; always C in Tech Preview). |
spec.paused | boolean | Default false. Suspends agent convergence (maintenance knob). |
spec.replicationIntent | string | Enabled (default) or Suspended. Suspended → Enabled in-place is denied by the webhook; only unprotect → re-protect restores it. |
spec.endpoints | array | Populated 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:
| Field | Meaning |
|---|---|
status.role | Observed DRBD role (Primary, Secondary, Unknown) |
status.connectionState | Resource-scoped connection state: Connected, Connecting, Disconnected, StandAlone, Unknown |
status.volumes[].replicationState | Per-volume replication state: Established, SyncSource, SyncTarget, PausedSyncS, PausedSyncT, Off, Unknown |
status.volumes[].syncProgress.percentage | Per-volume sync progress 0–100 |
status.volumes[].metadata.pvcName | External-metadata PVC name: drbd-md-<resourceName>-v<volumeNumber> |
status.conditions | Includes 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.
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
spec.protectionGroupRef.name | string | Yes | — | Name of the ProtectionGroup to fail over |
spec.protectionGroupRef.namespace | string | No | CR's namespace | |
spec.targetCluster | string | Yes | — | cluster1 or cluster2 — the cluster to fail over to |
spec.failoverType | string | No | planned | planned (graceful, waits for sync) or unplanned (force-promote, does not wait for primary shutdown) |
spec.batchBootTimeoutSeconds | integer | No | 300 | Max seconds to wait for one boot-order batch's VMs to reach Running before proceeding with a warning. |
spec.drainTimeoutSeconds | integer | No | 300 | Accepted 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.
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
spec.protectionGroupRef.name | string | Yes | — | ProtectionGroup to test |
spec.protectionGroupRef.namespace | string | No | CR's namespace | |
spec.cleanupPolicy | string | No | Automatic | Automatic — cleans up retentionTime after completion; Manual — resources remain until you delete the CR |
spec.retentionTime | string | No | 2h | Pattern: ^[0-9]+(s|m|h|d)$. Only meaningful when cleanupPolicy: Automatic. |
spec.timeout | string | No | (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.batchBootTimeoutSeconds | integer | No | 300 | Per-batch boot timeout during StartingVMs phase. |
spec.verification.reachability | array | No | — | Optional per-VM network probes. Boot/agent/filesystem checks are automatic and always run. |
spec.verification.reachability[].type | string | Yes (per probe) | — | tcp or http |
spec.verification.reachability[].port | integer | Yes (per probe) | — | 1–65535 |
spec.verification.reachability[].vmName | string | No | — | Restrict probe to this VM (source VM name from the ProtectionGroup). Empty = probe all VMs. |
spec.verification.reachability[].endpoint | string | No | / | HTTP path. http type only. |
spec.verification.reachability[].expectedStatus | integer | No | 200 | Expected HTTP status code (100–599). http type only. |
spec.verification.reachability[].timeout | string | No | 30s | Pattern: ^[0-9]+(s|m|h)$ |
status.phase values (exact): Queued → PreparingEnvironment → CreatingSnapshots → CreatingVolumes → CreatingVMs → StartingVMs → Verifying → Succeeded | Failed → CleaningUp → Cleaned
Key status fields:
| Field | Meaning |
|---|---|
status.testNamespace | Generated namespace for test VMs: site-recovery-<protection-group>-<random>. Set by the controller; never user-provided. |
status.targetCluster | cluster1 or cluster2 — where test VMs are running |
status.targetClusterReason | Why the controller selected that cluster: vm_runstrategy, pg_status, or pg_spec_sourceCluster |
status.verificationResults.passed | Overall 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.testNamespaceonly. 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.
| Field | Type | Default | Notes |
|---|---|---|---|
spec.protectionGroupRef.name | string | — | Required; references the ProtectionGroup being monitored |
spec.pollingIntervalSeconds | integer | 15 | How often DRBD metrics are polled (5–300) |
spec.replicationProtocol | string | C | |
spec.rpoObjectiveSeconds | integer | 0 | Not consumed in Tech Preview. |
Key status fields:
| Field | Meaning |
|---|---|
status.rpoStatus | Met, Degraded, Violated, or Unknown |
status.aggregated.outOfSyncBytes | Total out-of-sync bytes across all VMs in the group |
status.aggregated.totalVolumeCount | Number of DRBD resources (= number of VMs) in the group; one per VM since TSR-90, not one per disk |
status.aggregated.syncedVolumeCount | Number of VMs fully in sync (all disks in sync) |
status.aggregated.connectionState | Worst-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.lastFullySyncedAt | When 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.
| Field | Type | Required | Notes |
|---|---|---|---|
spec.protectionGroupRef.name | string | Yes | |
spec.eventType | string | Yes | SyncCompleted, RPODegraded, RPOViolated, ConnectionLost, ConnectionRestored, ResyncStarted, ResyncCompleted, VolumeAdded, VolumeRemoved, MetadataUndersized, ResizePending, ResizeComplete |
spec.timestamp | string | Yes | RFC 3339 timestamp of the event |
spec.rpoAtEvent | integer | No | Estimated RPO in seconds at event time |
spec.outOfSyncBytesAtEvent | integer | No | Total out-of-sync bytes at event time |
spec.previousRPOStatus | string | No | Met, Degraded, Violated, or Unknown |
spec.newRPOStatus | string | No | Same enum |
spec.violationReason | string | No | ConnectionLost, NetworkTimeout, SyncLagExceeded, ProtocolCOutOfSync, ResyncInProgress, Unknown |
spec.message | string | No | Human-readable description |
spec.affectedVolumes[] | array | No | Per-VM replication state at event time |
spec.connectionDetails | object | No | Connectivity detail for connection events |
spec.recoveryEstimate | object | No | Estimated 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
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"}}}'
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}'
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
sourceClusterfield references aclusterIDthat does not match eithercluster1orcluster2.
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:
- Check per-VM detail in
status.vmStatuses:
oc get failoverrequest <name> -n dr-production \
--context=quorum-cluster \
-o jsonpath='{.status.vmStatuses}'
- 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
- 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
- If the primary cluster is unreachable and you used
failoverType: planned, re-apply the request withfailoverType: unplannedto 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:NoScheduletaint on nodes). - A worker node hosting a protected VM was lost.
Fix:
- Check
RPOEventrecords for the affected group:
oc get rpoevent -n <vm-namespace> --context=primary-cluster
- 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
- Check for the node taint:
oc get nodes --context=primary-cluster \
-o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.taints}{"\n"}{end}'
- 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.