Recovery Operation Failures
Diagnosing failed or partially completed recovery operations, rollback behavior
This page helps you diagnose and recover from failed or partially completed disaster recovery operations in Trilio Site Recovery. Recovery operations—planned failovers, unplanned failovers, test failovers, and failback—are stateful, multi-step workflows coordinated across two or three clusters by the failover-controller and related operators running on the quorum cluster. When a failure occurs mid-workflow, the system leaves a persistent audit trail in FailoverRequest, TestFailover, and ReplicationGroupStatus custom resources, giving you a precise starting point for diagnosis. This guide walks through reading that state, identifying root causes, understanding rollback behavior, and recovering to a consistent operational state.
Before using this guide, ensure you have:
- OpenShift 4.14 or later on all clusters (quorum, primary, DR)
- OpenShift Virtualization (CNV) 1.0 or later
kubectlorocCLI installed and configured- Kubeconfig files for all clusters in your DR estate (quorum, primary, and DR clusters)
- Read access to the DR namespace (
dr-<name>) on the quorum cluster - Sufficient RBAC permissions to read and patch
FailoverRequest,TestFailover,ProtectionGroup, andDRBDResourcecustom resources across clusters - The
tsr-gathertool available if you need to collect a full diagnostic bundle for support
All commands that target a specific cluster use --kubeconfig flags explicitly. Substitute <quorum-kubeconfig>, <primary-kubeconfig>, and <dr-kubeconfig> with the actual paths to your kubeconfig files, and dr-<name> with your actual DR namespace.
No additional installation is required to diagnose recovery operation failures. The diagnostic tooling ships as part of the Site Recovery components you already deployed. If you need to collect a full support bundle, tsr-gather is available on the quorum cluster.
Step 1 — Verify controller health on the quorum cluster
Before diagnosing individual operation failures, confirm that the controllers responsible for orchestrating recovery are running:
kubectl --kubeconfig <quorum-kubeconfig> get pods \
-n dr-<name> \
-l app.kubernetes.io/part-of=site-recovery-quorum-control-plane
Expected output shows all pods in Running state with a low restart count:
NAME READY STATUS RESTARTS AGE
site-recovery-quorum-control-plane-7d9f6c-xkp2l 3/3 Running 0 4d
If any pod is in CrashLoopBackOff or Error, retrieve its logs before proceeding with operation-level diagnosis:
kubectl --kubeconfig <quorum-kubeconfig> logs \
-n dr-<name> \
-l app.kubernetes.io/part-of=site-recovery-quorum-control-plane \
--all-containers \
--tail=200
Step 2 — Verify workload control plane health on primary and DR clusters
Run the following on both the primary and DR clusters:
kubectl --kubeconfig <primary-kubeconfig> get pods \
-n dr-<name> \
-l app.kubernetes.io/part-of=site-recovery-workload-control-plane
kubectl --kubeconfig <dr-kubeconfig> get pods \
-n dr-<name> \
-l app.kubernetes.io/part-of=site-recovery-workload-control-plane
The drbd-node-agent DaemonSet must have one pod per worker node in Running state.
Step 3 — Collect a diagnostic bundle if needed
To collect logs, CRD state, and configuration from all clusters in a single bundle:
oc adm must-gather \
--image=<tsr-gather-image> \
-- /usr/bin/tsr-gather \
--kubeconfig-quorum <quorum-kubeconfig> \
--kubeconfig-primary <primary-kubeconfig> \
--kubeconfig-dr <dr-kubeconfig>
The bundle is written to the current directory and contains no secrets or credentials.
Recovery operation behavior is governed by fields on the FailoverRequest and TestFailover custom resources. Understanding these fields helps you determine whether a failure was caused by a timeout, a misconfigured target, or an underlying infrastructure problem.
FailoverRequest spec fields relevant to failure diagnosis
| Field | Type | Effect |
|---|---|---|
spec.failoverType | string | Planned or Unplanned. Planned failovers gracefully stop VMs on the source before promoting DRBD volumes; unplanned force-promotes without waiting for the source. A failure during a planned failover may leave VMs partially stopped. |
spec.targetCluster | string (required) | The cluster to which VMs should be promoted. If this value does not match an active cluster registered in the ProtectionZone, the failover-controller will reject the request immediately. |
spec.drainTimeoutSeconds | integer | Maximum seconds the failover-controller waits for VMs to stop on the source cluster before declaring a drain failure. Default behavior if unset: the controller uses an internal default. Increase this value if your VMs take longer than expected to shut down gracefully. |
spec.batchBootTimeoutSeconds | integer | Maximum seconds the failover-controller waits for VMs to reach Running state on the target cluster after DRBD volume promotion. Increase this value for large VMs with slow boot times. |
spec.protectionGroupRef | object (required) | Reference to the ProtectionGroup being failed over. A mismatch here (wrong name or namespace) causes an immediate Failed status. |
TestFailover spec fields relevant to failure diagnosis
| Field | Type | Effect |
|---|---|---|
spec.cleanupPolicy | string | Controls what happens to test resources if the test fails. If set to Retain, failed test VMs and PVCs remain in the isolated namespace for manual inspection. |
spec.timeout | string | Overall timeout for the test failover workflow. If exceeded, the TestFailover moves to Failed and then CleaningUp. |
spec.retentionTime | string | How long succeeded test resources are retained before cleanup. Not relevant to failure diagnosis but affects how long you have to inspect a passing test. |
spec.verification | object | Defines the checks the test-failover-controller runs against test VMs. A verification failure causes the CR to move to Failed before cleanup. |
spec.protectionGroupRef | object (required) | Reference to the ProtectionGroup under test. |
DRBDReplicationPolicy fields that affect recovery
| Field | Type | Effect |
|---|---|---|
spec.drbdProtocol | string | A (asynchronous) or C (synchronous). Protocol A allows a small RPO window; if an unplanned failover occurs with Protocol A, some data may not have replicated to the DR side. |
spec.replicationMode | string | Controls replication behavior at steady state. A misconfigured replication mode can leave DRBD resources in a Degraded state that causes failover failures. |
spec.rpo | object | RPO objective configuration. Violations are recorded as RPOEvent CRs and surfaced in ReplicationGroupStatus. |
ProtectionGroup spec fields that affect failover state
| Field | Type | Effect |
|---|---|---|
spec.desiredState | string | running or stopped. The failover-controller patches this field to orchestrate VM lifecycle. If a failover fails mid-way, this field may be left in an inconsistent state relative to actual VM state. |
spec.virtualMachines | array (required) | The set of VMs that must fail over together. A VM that no longer exists or is in a bad state will cause the failover-controller to report a failure for the entire group. |
Reading FailoverRequest status after a failure
The FailoverRequest CR is the primary diagnostic artifact for failed planned or unplanned failovers. Its status subresource records the current phase and any error messages from the failover-controller.
Check overall status:
kubectl --kubeconfig <quorum-kubeconfig> get failoverrequest \
-n dr-<name>
Example output showing a failed operation:
NAME TYPE STATUS PHASE AGE
prod-failover-2025 Planned Failed DrainFailed 8m
Inspect the full status block:
kubectl --kubeconfig <quorum-kubeconfig> get failoverrequest prod-failover-2025 \
-n dr-<name> \
-o yaml
Look for the status.conditions array and status.message fields. The failover-controller populates these with the step that failed, the cluster it was operating on, and the underlying error.
Examine controller events on the FailoverRequest:
kubectl --kubeconfig <quorum-kubeconfig> describe failoverrequest prod-failover-2025 \
-n dr-<name>
The Events section at the bottom shows the timeline of controller actions, which is useful for understanding how far the failover progressed before failing.
Reading TestFailover status after a failure
The TestFailover CR tracks the test workflow through phases: CreatingSnapshots, VerifyingData, Succeeded, Failed, and CleaningUp.
Check test failover status:
kubectl --kubeconfig <quorum-kubeconfig> get testfailover \
-n dr-<name>
Inspect details:
kubectl --kubeconfig <quorum-kubeconfig> describe testfailover <test-name> \
-n dr-<name>
If cleanupPolicy: Retain was set, the isolated test namespace and its VMs and PVCs remain available for inspection after failure:
kubectl --kubeconfig <dr-kubeconfig> get ns | grep testfailover
kubectl --kubeconfig <dr-kubeconfig> get vm,vmi,pvc -n <test-namespace>
Checking replication health before or after a failed operation
Many failover failures originate from a degraded replication state. Check the ReplicationGroupStatus CR on the quorum cluster:
kubectl --kubeconfig <quorum-kubeconfig> get replicationgroupstatus \
-n dr-<name>
A Critical or Degraded status means the ProtectionGroup was not in a Consistent replication state when the failover was attempted. Check per-volume detail:
kubectl --kubeconfig <quorum-kubeconfig> get replicationgroupstatus <name> \
-n dr-<name> \
-o yaml
Also check for recent RPO violations that preceded the failure:
kubectl --kubeconfig <quorum-kubeconfig> get rpoevent \
-n dr-<name> \
--sort-by=.spec.timestamp
Checking DRBDResource state on both clusters
For DRBD Operator deployments, the DRBDResource CR on each cluster shows per-volume sync progress and connection state:
kubectl --kubeconfig <primary-kubeconfig> get drbdresource \
-n <vm-namespace>
kubectl --kubeconfig <dr-kubeconfig> get drbdresource \
-n <vm-namespace>
A resource stuck in Connecting or showing a stale sync percentage is a sign that the DRBD replication link between worker nodes has been interrupted. Verify that TCP ports 7000–7999 are open between primary and DR worker nodes.
Understanding rollback behavior
Trilio Site Recovery does not perform automatic rollback of a partially completed failover. The failover-controller is designed to be re-entrant: if you delete and recreate a FailoverRequest CR with the same parameters, the controller will re-evaluate the current actual state of VMs and DRBD resources on both clusters and continue from where it can safely proceed.
For a planned failover that failed during the drain phase (VMs not fully stopped on source), the VMs on the primary cluster may still be running. You can safely attempt the failover again after resolving the underlying cause—the controller checks actual VM state before taking action.
For an unplanned failover that failed after DRBD volume promotion on the DR cluster, the DR-side volumes may already be in Primary role. Do not attempt to start VMs on the primary cluster in this state. Verify DRBD resource roles before any manual intervention:
kubectl --kubeconfig <dr-kubeconfig> get drbdresource \
-n <vm-namespace> \
-o jsonpath='{range .items[*]}{.metadata.name}{" "}{.status.role}{"\n"}{end}'
Example 1 — Planned failover stuck in DrainFailed
A planned failover was created but reported DrainFailed after 8 minutes.
kubectl --kubeconfig <quorum-kubeconfig> get failoverrequest prod-pg-failover \
-n dr-production \
-o yaml
Expected output (abbreviated):
apiVersion: siterecovery.trilio.io/v1alpha1
kind: FailoverRequest
metadata:
name: prod-pg-failover
namespace: dr-production
spec:
failoverType: Planned
drainTimeoutSeconds: 300
protectionGroupRef:
name: production-pg
namespace: vm-workloads
targetCluster: dr-cluster
status:
phase: DrainFailed
message: "VM prod-vm-3 did not stop within 300 seconds on primary-cluster"
conditions:
- type: DrainComplete
status: "False"
reason: DrainTimeout
message: "VM prod-vm-3 failed to stop within drainTimeoutSeconds=300"
The failover-controller stopped all VMs except prod-vm-3, which failed to stop within the configured drain timeout. Check the VM's state on the primary cluster:
kubectl --kubeconfig <primary-kubeconfig> get vm prod-vm-3 \
-n vm-workloads \
-o jsonpath='{.spec.running} {.status.printableStatus}'
If the VM is stuck in a bad state, investigate the VirtualMachineInstance:
kubectl --kubeconfig <primary-kubeconfig> describe vmi prod-vm-3 \
-n vm-workloads
After resolving the VM issue, delete the failed FailoverRequest and create a new one. If the VM cannot be stopped gracefully, consider switching to failoverType: Unplanned:
apiVersion: siterecovery.trilio.io/v1alpha1
kind: FailoverRequest
metadata:
name: prod-pg-failover-retry
namespace: dr-production
spec:
failoverType: Unplanned
protectionGroupRef:
name: production-pg
namespace: vm-workloads
targetCluster: dr-cluster
kubectl --kubeconfig <quorum-kubeconfig> apply -f failoverrequest-retry.yaml
Example 2 — TestFailover stuck in Failed with snapshots retained
A TestFailover was configured with cleanupPolicy: Retain and failed during VerifyingData.
kubectl --kubeconfig <quorum-kubeconfig> describe testfailover prod-pg-test-1 \
-n dr-production
Expected output (abbreviated):
Name: prod-pg-test-1
Namespace: dr-production
Status:
Phase: Failed
Message: Verification check failed: VM prod-vm-1 did not reach Running state within batchBootTimeoutSeconds=600
Events:
Type Reason Age Message
---- ------ --- -------
Normal CreatingSnapshots 12m Created volume snapshots for 3 PVCs
Normal ProvisioningPVCs 11m Provisioned 3 test PVCs from snapshots
Normal StartingTestVMs 10m Started test VMs in namespace testfailover-prod-pg-test-1
Warning VerificationFailed 2m VM prod-vm-1 did not reach Running state
Because cleanupPolicy: Retain was set, the test namespace still exists. Inspect the test VM:
kubectl --kubeconfig <dr-kubeconfig> get vm,vmi,pvc \
-n testfailover-prod-pg-test-1
kubectl --kubeconfig <dr-kubeconfig> describe vmi prod-vm-1 \
-n testfailover-prod-pg-test-1
Common causes include a missing image, insufficient resource quota on the DR cluster, or a node selector that cannot be satisfied. After investigation, delete the TestFailover CR to trigger cleanup:
kubectl --kubeconfig <quorum-kubeconfig> delete testfailover prod-pg-test-1 \
-n dr-production
The test-failover-controller will move to CleaningUp and remove the isolated namespace and all test resources.
Example 3 — Replication degraded before failover attempt
A FailoverRequest failed immediately with a replication-related message. Check the ReplicationGroupStatus:
kubectl --kubeconfig <quorum-kubeconfig> get replicationgroupstatus \
-n dr-production \
-o yaml
Expected output (abbreviated):
status:
health: Degraded
lastSyncTimestamp: "2025-11-01T03:14:00Z"
volumes:
- pvcName: prod-vm-1-disk
syncState: Syncing
syncPercent: 67
- pvcName: prod-vm-2-disk
syncState: Connected
syncPercent: 100
prod-vm-1-disk is mid-resync. Check for accompanying RPO events:
kubectl --kubeconfig <quorum-kubeconfig> get rpoevent \
-n dr-production \
--sort-by=.spec.timestamp \
-o custom-columns=NAME:.metadata.name,SEVERITY:.spec.eventType,LAG:.spec.rpoAtEvent,TIME:.spec.timestamp
Wait for the resync to complete (the syncPercent reaches 100 and health returns to Healthy) before retrying the failover. Monitor progress:
watch kubectl --kubeconfig <quorum-kubeconfig> get replicationgroupstatus \
-n dr-production
Example 4 — Unplanned failover with partially promoted DRBD resources
An unplanned failover failed after DRBD volume promotion began. Check resource roles on the DR cluster:
kubectl --kubeconfig <dr-kubeconfig> get drbdresource \
-n vm-workloads \
-o custom-columns=NAME:.metadata.name,ROLE:.status.role,SYNC:.status.syncPercent
Expected output showing partial promotion:
NAME ROLE SYNC
prod-vm-1-disk-dr Primary 100
prod-vm-2-disk-dr Secondary 100
prod-vm-3-disk-dr Primary 100
prod-vm-2-disk-dr was not promoted. Do not start any VMs on the primary cluster. Delete the failed FailoverRequest and create a new one:
kubectl --kubeconfig <quorum-kubeconfig> delete failoverrequest prod-pg-failover-failed \
-n dr-production
kubectl --kubeconfig <quorum-kubeconfig> apply -f - <<EOF
apiVersion: siterecovery.trilio.io/v1alpha1
kind: FailoverRequest
metadata:
name: prod-pg-failover-retry
namespace: dr-production
spec:
failoverType: Unplanned
protectionGroupRef:
name: production-pg
namespace: vm-workloads
targetCluster: dr-cluster
EOF
The failover-controller will re-evaluate resource roles and complete the promotion for prod-vm-2-disk-dr.
Use the following reference to match the symptom you observe to its likely cause and resolution. Each issue uses a consistent format: Symptom, Likely cause, Fix.
FailoverRequest remains in Pending indefinitely
Symptom: You applied a FailoverRequest CR and it shows Pending phase for more than two minutes with no events.
Likely cause: The failover-controller pod on the quorum cluster is not running, or it cannot reach the API server of the primary or DR cluster.
Fix:
- Verify the quorum control plane pods are running:
kubectl --kubeconfig <quorum-kubeconfig> get pods \ -n dr-<name> \ -l app.kubernetes.io/part-of=site-recovery-quorum-control-plane - Check controller logs for API connectivity errors:
kubectl --kubeconfig <quorum-kubeconfig> logs \ -n dr-<name> \ -l app.kubernetes.io/part-of=site-recovery-quorum-control-plane \ --all-containers --tail=100 - Verify TCP port 6443 is reachable from the quorum cluster to both primary and DR API servers.
- Confirm the
ProtectionZoneCR references valid cluster endpoints:kubectl --kubeconfig <quorum-kubeconfig> get protectionzone \ -n dr-<name> -o yaml
FailoverRequest fails with DrainFailed
Symptom: A planned FailoverRequest reaches DrainFailed phase. The status message names a specific VM.
Likely cause: The named VM did not stop within spec.drainTimeoutSeconds. This can happen if the VM's guest OS is hung, if the VMI pod is stuck in Terminating, or if the VM is subject to a PodDisruptionBudget that prevents eviction.
Fix:
- Check the VM and VMI state on the primary cluster:
kubectl --kubeconfig <primary-kubeconfig> get vm,vmi \ -n <vm-namespace> - Describe the VMI to find scheduling or pod-level issues:
kubectl --kubeconfig <primary-kubeconfig> describe vmi <vm-name> \ -n <vm-namespace> - If the VM is genuinely stuck, increase
drainTimeoutSecondsin a newFailoverRequest, or switchfailoverTypetoUnplannedif data loss is acceptable. - Delete the failed
FailoverRequestbefore creating a new one.
FailoverRequest fails with VolumePromotionFailed
Symptom: An unplanned FailoverRequest fails during DRBD volume promotion on the DR cluster.
Likely cause: The drbd-node-agent DaemonSet on the DR cluster cannot promote one or more volumes because the DRBD connection is still open from the primary side (split-brain prevention), or the drbd-node-agent pod on the relevant worker node is not running.
Fix:
- Check drbd-node-agent pods on the DR cluster:
kubectl --kubeconfig <dr-kubeconfig> get pods \ -n dr-<name> \ -l app.kubernetes.io/component=drbd-node-agent - Check
DRBDResourcestatus on the DR cluster:kubectl --kubeconfig <dr-kubeconfig> get drbdresource \ -n <vm-namespace> -o yaml - If the primary cluster is genuinely unreachable and DRBD is blocking promotion due to a stale connection, verify network isolation between clusters. For unplanned failovers where the primary is down, the controller is designed to force-promote; if it is not doing so, check the failover-controller logs for the specific rejection reason.
- Do not manually start VMs on the primary cluster while the unplanned failover is in progress.
TestFailover stuck in CreatingSnapshots
Symptom: A TestFailover CR stays in CreatingSnapshots phase for more than five minutes.
Likely cause: The volume snapshot class is not configured on the DR cluster, or the CSI driver does not support snapshots for the storage class backing the protected PVCs.
Fix:
- Check VolumeSnapshot resources on the DR cluster in the DR namespace:
kubectl --kubeconfig <dr-kubeconfig> get volumesnapshot \ -n dr-<name> - Check VolumeSnapshotContent resources for errors:
kubectl --kubeconfig <dr-kubeconfig> get volumesnapshotcontent - Verify a VolumeSnapshotClass is configured and set as default on the DR cluster:
kubectl --kubeconfig <dr-kubeconfig> get volumesnapshotclass - Check test-failover-controller logs for specific snapshot errors:
kubectl --kubeconfig <quorum-kubeconfig> logs \ -n dr-<name> \ -l app.kubernetes.io/part-of=site-recovery-quorum-control-plane \ --all-containers | grep -i testfailover
TestFailover fails in VerifyingData — test VMs do not start
Symptom: The TestFailover reaches VerifyingData but fails because test VMs in the isolated namespace do not reach Running state.
Likely cause: Insufficient compute resources or node selectors on the DR cluster that cannot be satisfied in the isolated test namespace, or a missing image pull secret.
Fix:
- If
cleanupPolicy: Retainwas set, inspect the test namespace directly:kubectl --kubeconfig <dr-kubeconfig> get vm,vmi,pvc \ -n testfailover-<test-name> kubectl --kubeconfig <dr-kubeconfig> describe vmi <vm-name> \ -n testfailover-<test-name> - Look for events indicating resource pressure or unschedulable pods:
kubectl --kubeconfig <dr-kubeconfig> get events \ -n testfailover-<test-name> \ --sort-by=.lastTimestamp - Increase
spec.batchBootTimeoutSecondsif VMs are starting but slowly. - After investigation, delete the
TestFailoverCR to trigger cleanup:kubectl --kubeconfig <quorum-kubeconfig> delete testfailover <test-name> \ -n dr-<name>
ProtectionGroup shows Degraded replication health after failover
Symptom: After a completed failover, the ProtectionGroup status shows Degraded rather than Consistent, and ReplicationGroupStatus shows one or more volumes not yet synchronized.
Likely cause: After a failover, DRBD replication runs in reverse (DR cluster is now primary). Replication may not have been re-established because the original primary cluster is still unreachable, or because the DRBDReplicationPolicy was not updated to reflect the new primary.
Fix:
- Check the
ProtectionGroupstatus on the quorum cluster:kubectl --kubeconfig <quorum-kubeconfig> get protectiongroup \ -n <vm-namespace> -o yaml - Check the
ReplicationGroupStatusfor per-volume detail:kubectl --kubeconfig <quorum-kubeconfig> get replicationgroupstatus \ -n dr-<name> -o yaml - Verify that the drbd-node-agent on both clusters reports a connected state. If the original primary cluster is still offline, replication will remain
Degradeduntil connectivity is restored—this is expected behavior. - Once the original primary is restored and network connectivity between clusters is re-established, the DRBD replication link should reconnect automatically and begin resync. Monitor progress:
watch kubectl --kubeconfig <quorum-kubeconfig> get replicationgroupstatus \ -n dr-<name>
FailoverRequest fails immediately with InvalidTarget
Symptom: A FailoverRequest moves to Failed within seconds with a message referencing an invalid or unknown target cluster.
Likely cause: The value of spec.targetCluster does not match any cluster registered in the ProtectionZone CR on the quorum cluster.
Fix:
- Check the
ProtectionZoneto confirm valid cluster names:kubectl --kubeconfig <quorum-kubeconfig> get protectionzone \ -n dr-<name> \ -o jsonpath='{.spec.clusters[*].name}' - Correct the
spec.targetClustervalue in yourFailoverRequestto exactly match one of the registered cluster names. - Delete the failed
FailoverRequestand apply the corrected version.
Controller logs show repeated reconciliation errors but no CR status update
Symptom: The failover-controller or test-failover-controller logs show repeated errors, but the FailoverRequest or TestFailover CR status does not update.
Likely cause: The controller is losing its leader election lease, or it lacks RBAC permission to update the status subresource of the CR.
Fix:
- Check for leader election errors in the controller logs:
kubectl --kubeconfig <quorum-kubeconfig> logs \ -n dr-<name> \ -l app.kubernetes.io/part-of=site-recovery-quorum-control-plane \ --all-containers | grep -i 'leader\|election\|lease' - Verify RBAC for the controller's service account:
kubectl --kubeconfig <quorum-kubeconfig> auth can-i update \ failoverrequests/status \ --as=system:serviceaccount:dr-<name>:<controller-service-account> \ -n dr-<name> - If RBAC is missing, re-run the
helm upgradefor thesite-recovery-quorum-control-planechart with the current values to restore correct RBAC bindings:helm upgrade site-recovery-quorum-control-plane \ trilio/site-recovery-quorum-control-plane \ -n dr-<name> \ -f values.yaml