Custom Resource Reference
Complete API reference for all site recovery custom resources
This page is the complete API reference for all Custom Resource Definitions (CRDs) in the siterecovery.trilio.io/v1alpha1 API group. Trilio Site Recovery is a Kubernetes operator whose entire operational surface — protecting VMs, triggering failovers, running non-disruptive DR tests, and monitoring replication health — is expressed through these custom resources. Understanding each CRD's spec, status, and admission constraints is essential for authoring GitOps manifests, building automation, and diagnosing problems. All CRDs are namespaced; the cluster on which each kind is registered, the namespace convention, and the short names you can use with kubectl and oc are noted for every resource.
Before working with these custom resources, ensure the following are in place:
- OpenShift 4.17 or later on all three clusters (quorum, primary, and DR). The validated range for Tech Preview is OpenShift 4.17–4.20, as stated in
docs/tech-preview-supported-configurations.md. The OLM bundle annotation indeploy/olm-catalog/site-recovery/metadata/annotations.yamllistscom.redhat.openshift.versions: v4.14-v4.19as the installable range; note that 4.14–4.16 are within the installable range but are not validated for Tech Preview and are not supported. - KubeVirt / OpenShift Virtualization installed on both workload clusters (primary and DR). The quorum cluster does not require KubeVirt.
- DRBD kernel module ≥ 9.x loaded on every worker node of both workload clusters.
- Three clusters: a quorum cluster (management plane only, no VMs, no DRBD), a primary workload cluster, and a DR workload cluster.
ocCLI — required for all operations; kubectl-only is not a supported mode.- Kubeconfig files for all three clusters.
- Network: TCP ports 7000–8000 inclusive open between primary and DR worker nodes (one port per protected VM); TCP port 6443 open from the quorum cluster to the primary and DR API servers.
- Network latency below 50 ms RTT between primary and DR clusters (required for Protocol C synchronous replication).
- LVM thin pools configured on every worker node of both workload clusters, backed by a dedicated block device.
- The Trilio Site Recovery operator installed via OLM on the quorum cluster (package
site-recovery, channeltech-preview, catalogquay.io/triliovault/site-recovery-catalog), and a ProtectionZone CR created in adr-<name>namespace — this automatically deploys the workload control planes and DRBD node agents to both workload clusters.
CRD registration by cluster
| Cluster | Registered kinds |
|---|---|
| Quorum | ProtectionZone, ProtectionGroup, ProtectionRequest, DRBDReplicationPolicy, FailoverRequest, TestFailover, ReplicationGroupStatus, RPOEvent |
| Primary and DR (workload) | DRBDResource |
Running kubectl get drbdresource against the quorum cluster returns server doesn't have a resource type. Running kubectl get protectiongroup against a workload cluster returns the same error.
Custom resources are not installed separately — the CRDs are deployed automatically by the ProtectionZone controller and the OLM operator bundle. You never run helm install directly. The customer-facing install sequence is:
Step 1 — Install the operator via OLM on the quorum cluster
Create a CatalogSource, OperatorGroup, and Subscription in the trilio-site-recovery-system namespace:
apiVersion: operators.coreos.com/v1alpha1
kind: CatalogSource
metadata:
name: trilio-site-recovery-catalog
namespace: trilio-site-recovery-system
spec:
sourceType: grpc
image: quay.io/triliovault/site-recovery-catalog:latest
displayName: Trilio Site Recovery
---
apiVersion: operators.coreos.com/v1
kind: OperatorGroup
metadata:
name: trilio-site-recovery-og
namespace: trilio-site-recovery-system
spec:
targetNamespaces:
- trilio-site-recovery-system
---
apiVersion: operators.coreos.com/v1alpha1
kind: Subscription
metadata:
name: site-recovery
namespace: trilio-site-recovery-system
spec:
channel: tech-preview
name: site-recovery
source: trilio-site-recovery-catalog
sourceNamespace: trilio-site-recovery-system
Apply with:
oc apply -f catalog-source.yaml --context=<quorum-context>
Step 2 — Create kubeconfig Secrets in the zone namespace
All quorum CRs live in a dr-<name> namespace. Create the namespace and the kubeconfig secrets for both workload clusters:
oc create namespace dr-production --context=<quorum-context>
oc create secret generic cluster1-kubeconfig \
--from-file=kubeconfig=/path/to/primary-kubeconfig \
-n dr-production \
--context=<quorum-context>
oc create secret generic cluster2-kubeconfig \
--from-file=kubeconfig=/path/to/dr-kubeconfig \
-n dr-production \
--context=<quorum-context>
Step 3 — Create a ProtectionZone CR
Once the ProtectionZone is created and reaches Available, the controller automatically deploys the site-recovery-quorum-control-plane chart (to the quorum cluster) and the site-recovery-workload-control-plane chart (to both workload clusters), registering all CRDs:
apiVersion: siterecovery.trilio.io/v1alpha1
kind: ProtectionZone
metadata:
name: production
namespace: dr-production
spec:
displayName: "Production DR Zone"
description: "Primary to DR site replication over DRBD."
storageBackendMode: DRBD
clusters:
- name: primary
clusterID: cluster1
distributionName: RHEL-CoreOS-9
kubeconfigSecretRef:
name: cluster1-kubeconfig
key: kubeconfig
- name: dr
clusterID: cluster2
distributionName: RHEL-CoreOS-9
kubeconfigSecretRef:
name: cluster2-kubeconfig
key: kubeconfig
oc apply -f protection-zone.yaml --context=<quorum-context>
Wait for the zone to become available:
oc get pz production -n dr-production --context=<quorum-context> -w
Expected output when ready:
NAME DISPLAY NAME BACKEND CLUSTERS STATUS AGE
production Production DR Zone DRBD primary, dr Available 3m
Once STATUS is Available, all CRDs are registered and the workload control planes and DRBD node agents are running on both workload clusters.
The following describes the spec fields, valid values, and defaults for each Custom Resource. Fields that are accepted by the API but have no effect in Tech Preview are noted explicitly.
ProtectionZone
Cluster: quorum | Namespace: dr-<name> | Short name: pz
A ProtectionZone pairs exactly two workload clusters and drives the automated deployment of all control plane components. The quorum cluster is required; it is not optional.
| Field | Type | Default | Description |
|---|---|---|---|
spec.displayName | string | — | Human-friendly label shown in the TSR web console. Min 5, max 253 characters. |
spec.description | string | — | Optional free-form text. Max 1024 characters. |
spec.storageBackendMode | string | DRBD | Storage backend. Only DRBD is supported; Linstor is accepted by the schema but rejected by the admission webhook with: Linstor backend is not supported yet; use DRBD. |
spec.clusters[].name | string | — | Operator-facing identifier for the cluster. Must be unique within the zone. Max 63 characters. Immutable after creation. |
spec.clusters[].clusterID | string | — | Deployment slot: cluster1 or cluster2. The two clusters must occupy different slots. Immutable after creation. |
spec.clusters[].kubeconfigSecretRef.name | string | — | Name of the Secret containing the kubeconfig. Required and must be non-empty. Mutable (credentials can be rotated). |
spec.clusters[].kubeconfigSecretRef.key | string | — | Key within the Secret. Required and must be non-empty. Mutable. |
spec.clusters[].distributionName | string | RHEL-CoreOS-9 | OS family of the cluster's nodes. Selects the DRBD kernel-module loader image. Values: RHEL-CoreOS-9 (supported), RHEL-CoreOS-10 (not validated in Tech Preview). Mutable to accommodate OpenShift upgrades. |
spec.logging.level | string | — | Runtime log level for all zone components. Values: error, info, debug, trace. Takes effect within ~15 s without pod restarts. |
spec.logging.controlPlaneLevel | string | — | Overrides level for quorum and workload control-plane managers only. |
spec.logging.nodeAgentLevel | string | — | Overrides level for DRBD node agents only. |
Mutable fields after creation: displayName, description, each cluster's kubeconfigSecretRef, each cluster's distributionName, and logging. Everything else is immutable.
Status fields
| Field | Description |
|---|---|
status.phase | Zone lifecycle: Provisioning, Available, Unavailable, Degraded, Deleting. |
status.clusters[].status | Per-cluster reachability: Reachable or Unreachable. |
status.clusters[].message | Explains an Unreachable observation. |
status.conditions | Detailed condition list. |
ProtectionGroup
Cluster: quorum | Namespace: dr-<name> | Short name: pg
Defines a set of VMs that fail over together as a single unit. At least one VM is required.
| Field | Type | Default | Description |
|---|---|---|---|
spec.virtualMachines[].name | string | — | Name of the KubeVirt VirtualMachine. Must be a DNS-1123 label. |
spec.virtualMachines[].namespace | string | — | Namespace of the VM on the workload cluster. |
spec.virtualMachines[].bootOrder | integer | — | Controls failover boot sequencing. Lower values boot first; VMs sharing a value boot in parallel. Optional. |
spec.desiredState | string | running | Whether VMs should be running or stopped on the active cluster. |
spec.sourceCluster | string | — | Deployment-time source cluster (cluster1 or cluster2). |
spec.sla.rtoTargetSeconds | integer | — | Target RTO in seconds. Recorded for compliance; not enforced in Tech Preview. |
spec.sla.rpoTargetSeconds | integer | 0 | Target RPO in seconds. Not enforced in Tech Preview; Protocol C always gives RPO=0. |
Note:
spec.sla.rpoTargetSecondsandspec.sla.rtoTargetSecondsare accepted by the API but not consumed by any controller in Tech Preview. Do not rely on them for enforcement.
Status fields
| Field | Description |
|---|---|
status.state | Overall state: Protected, Syncing, Degraded, Failed. |
status.activeClusterID | Which cluster (cluster1 or cluster2) currently runs the VMs. |
status.protectedVMs[].replicationStatus | Per-VM: Protected, Syncing, Degraded, NotProtected. |
ProtectionRequest
Cluster: quorum | Namespace: dr-<name> | Short names: pr, protect
Requests block-level replication protection for a single VM. One ProtectionRequest exists per protected VM.
| Field | Type | Default | Description |
|---|---|---|---|
spec.sourceCluster | string | — | Which cluster the VM currently runs on: cluster1 or cluster2. Required. |
spec.virtualMachine.name | string | — | VM name on the workload cluster. Required. |
spec.virtualMachine.namespace | string | — | VM namespace on the workload cluster. Required; cannot be inferred from the PR's namespace. |
spec.replicationConfig.replicationProtocol | string | C | DRBD wire protocol. Only C (synchronous, RPO=0) is supported in Tech Preview. A is rejected by the admission webhook with: ReplicationProtocolA is not supported yet. |
spec.replicationConfig.rpo | integer | 0 | RPO in minutes. Must be 0 when replicationProtocol is C; a non-zero value is rejected: rpo must be 0 when spec.replicationConfig.replicationProtocol is C (synchronous). |
spec.replicationConfig.drbdMetadataConfig.storageClassName | string | — | StorageClass for per-volume DRBD metadata PVCs. Must bind immediately; WaitForFirstConsumer classes are rejected during validation. |
spec.replicationConfig.drbdMetadataConfig.size | quantity | 128Mi | Size of each metadata PVC. Minimum 128Mi. |
Status phases (exact, in order): Pending → Validating → CreatingDRBD → Syncing → ReadyToActivate → Activating → Protected. On teardown: Unprotecting. Terminal failure: Failed.
Key status fields
| Field | Description |
|---|---|
status.phase | Current lifecycle phase (see above). |
status.syncProgress | VM-level sync percentage (0–100). Mirrors slowestVolumeSyncProgress.percentage. |
status.slowestVolumeSyncProgress | Progress of the slowest volume. Authoritative for the replication monitor. |
status.drbdResourceRef.name | Shared name of both DRBDResource copies (drbd-<namespace>-<vm>-<hash8>). |
status.drbdResourceRef.port | Allocated replication port (7000–8000). |
status.activeCluster | Cluster currently observed to hold the DRBD Primary for this VM. |
status.consistencySummary | Human-readable consistency summary (e.g. 3/3 volumes consistent). |
Important operational constraints: A protected VM is pinned to one node per cluster for the lifetime of the protection.
nodeNameis immutable once set. Live migration and cold migration of a protected VM are denied. Re-protecting always requires a full unprotect and a full initial resync.
DRBDResource
Cluster: primary and DR (workload) | Namespace: VM's namespace | Short name: dres
Represents all disks of a single VM as one replicated DRBD resource. One CR exists per VM per cluster side. Created and managed by the protection-controller; you do not create these directly. Name pattern: drbd-<namespace>-<vm>-<hash8>.
Do not query DRBDResources against the quorum cluster — kubectl get drbdresource on the quorum cluster returns server doesn't have a resource type.
Key spec fields
| Field | Description |
|---|---|
spec.side | Source or Target. Immutable; does not change on failover. |
spec.role | Desired DRBD role: Primary or Secondary. Set by the protection controller. |
spec.nodeName | Node this copy is pinned to. Immutable once set. |
spec.port | Replication port (7000–8000). |
spec.volumes[].volumeNumber | Stable cross-copy join key (0 = boot disk). |
spec.volumes[].pvcName | Local backing PVC name. |
spec.paused | Suspends convergence (maintenance knob). Does not affect deletion. |
spec.replicationIntent | Enabled (steady state) or Suspended. The webhook forbids Suspended → Enabled in place. |
spec.replication.protocol | A or C. Materialized from the DRBDReplicationPolicy by the protection controller. |
Status fields
| Field | Description |
|---|---|
status.phase | Pending, Provisioning, ReadyToActivate, Syncing, Consistent, Degraded, Failed, Deleting. |
status.connectionState | Connection to peer: Connected, Connecting, Disconnected, StandAlone, Unknown. |
status.role | Observed DRBD role: Primary, Secondary, Unknown. |
status.volumes[].replicationState | Per-volume: Established, SyncSource, SyncTarget, PausedSyncS, PausedSyncT, Off, Unknown. |
status.volumes[].syncProgress | Per-volume sync progress. |
DRBDReplicationPolicy
Cluster: quorum | Namespace: dr-<name> | Short names: drp, drbdpolicy
You do not create or modify this resource. The ProtectionZone controller automatically creates one default policy named
<zoneName>-defaultwithisDefault: truein the zone's namespace when the zone reachesAvailable. Only theisDefaultpolicy in the ProtectionRequest's namespace is ever consulted by the controllers.
This resource defines replication parameters including storage class mappings. It is documented here for reference and diagnostics only.
| Field | Default | Description |
|---|---|---|
spec.drbdProtocol | C | Wire protocol. C = synchronous (RPO=0). |
spec.replicationMode | Synchronous | Synchronous or Asynchronous. Only Synchronous is effective in Tech Preview. |
spec.isDefault | false | Marks the policy as the default for its namespace. |
spec.clusters[].name | — | Cluster slot: cluster1 or cluster2. |
spec.clusters[].storageClassMappings | — | Maps source StorageClass names to target StorageClass names. Falls back to source class if absent. |
spec.networkConfig.portRange | start: 7000, end: 7999 | Not consumed in Tech Preview. Ports are always allocated from the fixed 7000–8000 range regardless of this field. |
Important:
spec.networkConfig.portRange,spec.clusters[].replicationEndpoint.port,spec.clusters[].nodeSelector, andspec.replicationMode: Asynchronousare accepted by the API but not consumed by any controller in Tech Preview. The actual port range is always 7000–8000.
FailoverRequest
Cluster: quorum | Namespace: dr-<name> | Short name: fr
Triggers a planned or unplanned failover for a ProtectionGroup. Create one CR per failover operation; do not reuse or modify an existing one.
| Field | Type | Default | Description |
|---|---|---|---|
spec.protectionGroupRef.name | string | — | Name of the ProtectionGroup to fail over. Required. |
spec.protectionGroupRef.namespace | string | — | Namespace of the ProtectionGroup. |
spec.targetCluster | string | — | Cluster to fail over TO: cluster1 or cluster2. Required. |
spec.failoverType | string | planned | planned (graceful, waits for sync) or unplanned (force-promotes DRBD volumes without waiting for the primary). |
spec.batchBootTimeoutSeconds | integer | 300 | Max seconds to wait per boot-order batch. Relevant only when the ProtectionGroup defines bootOrder. |
spec.drainTimeoutSeconds | integer | 300 | Accepted and range-checked (30–3600) but not consumed by the failover controller in Tech Preview. This field has no effect on drain or failover behavior. Do not include it in manifests expecting timed drain behavior. |
Status fields (read-only)
| Field | Description |
|---|---|
status.state | Overall state: Pending, InProgress, Completed, Failed, Cancelled. |
status.phase | Granular phase: Initializing, Validating, StoppingSource, WaitingForLocks, RemovingTaints, StartingTarget, Monitoring, Completed, Failed. |
status.vmStatuses[] | Per-VM observed status. Use this for per-VM detail — there is no status.failedVMs or status.message field. |
status.vmStatuses[].phase | Per-VM: Pending, Stopping, Stopped, Starting, Running, Failed. |
status.sourceCluster | Cluster the failover originated from. |
Unplanned failover: When
failoverType: unplanned, DRBD volumes are force-promoted on the DR cluster without waiting for the primary to shut down. If replication wasHealthyandConsistentat the time of failure, zero data loss occurs. If replication wasDegraded, minimal data loss may occur.
TestFailover
Cluster: quorum | Namespace: dr-<name> | Short name: tfo
Triggers a non-disruptive DR validation using volume snapshots. Test VMs are started in an isolated, system-generated namespace on the DR cluster and cleaned up afterward. Production VMs are unaffected.
| Field | Type | Default | Description |
|---|---|---|---|
spec.protectionGroupRef.name | string | — | Name of the ProtectionGroup to test. Required. |
spec.protectionGroupRef.namespace | string | — | Namespace of the ProtectionGroup. Required; cannot be inferred. |
spec.cleanupPolicy | string | Automatic | Automatic: test resources are deleted retentionTime after completion. Manual: resources are kept until you delete the CR. |
spec.retentionTime | string | 2h | How long to keep test resources before automatic cleanup. Pattern: ^[0-9]+(s|m|h|d)$. |
spec.timeout | string | — | Maximum duration for the entire test (measured from status.startTime). No default is applied; set only if you need to enforce a duration SLA. Pattern: ^[0-9]+(s|m|h)$. |
spec.batchBootTimeoutSeconds | integer | 300 | Max seconds per boot-order batch. |
spec.verification.reachability[].type | string | — | Probe protocol: tcp or http. Required. |
spec.verification.reachability[].port | integer | — | Port to probe (1–65535). Required. |
spec.verification.reachability[].vmName | string | — | Restricts the probe to a named VM in the ProtectionGroup. When empty, runs against all test VMs. |
spec.verification.reachability[].endpoint | string | — | HTTP path. Valid for http probes only; rejected for tcp. |
spec.verification.reachability[].expectedStatus | integer | — | Expected HTTP status code. Valid for http probes only; rejected for tcp. |
spec.verification.reachability[].timeout | string | 30s | Probe timeout. Maximum 10m; values above 10m are rejected. |
Status phases (exact): Queued → PreparingEnvironment → CreatingSnapshots → CreatingVolumes → CreatingVMs → StartingVMs → Verifying → Succeeded or Failed → CleaningUp → Cleaned.
Key status fields
| Field | Description |
|---|---|
status.testNamespace | System-generated isolated namespace (site-recovery-<protection-group>-<random>). Never user-provided or manually constructed. |
status.targetCluster | Which cluster the test ran on. |
status.verificationResults.passed | Whether all verification checks passed. |
status.verificationResults.checks[] | Per-check results. |
Do not patch finalizers to force deletion of a TestFailover. If the test namespace is stuck, follow the official runbooks.
ReplicationGroupStatus
Cluster: quorum | Namespace: dr-<name> | Short name: rgs
Aggregates replication health for a ProtectionGroup. Created automatically by the replication monitor — one per ProtectionGroup, named <protection-group-name>-replication. You do not create or modify this resource.
Spec fields (reference only)
| Field | Default | Description |
|---|---|---|
spec.pollingIntervalSeconds | 15 | How often the controller polls DRBD metrics (5–300). |
spec.rpoObjectiveSeconds | 0 | Target RPO in seconds. Not enforced in Tech Preview. |
Key status fields
| Field | Description |
|---|---|
status.rpoStatus | Met, Degraded, Violated, or Unknown. |
status.aggregated.connectionState | Worst-case connection state across all VMs: Connected, Degraded, Disconnected. |
status.aggregated.syncedVolumeCount | Number of VMs (not disks) currently fully in sync. |
status.aggregated.totalVolumeCount | Total number of replicated VMs in the group. |
status.aggregated.outOfSyncBytes | Total out-of-sync bytes across all VMs. |
status.volumeReplicationStatuses[] | One entry per VM (not per disk). Each entry covers all disks of that VM. |
status.lastFullySyncedAt | When every disk of every VM was last fully in sync. |
Label for filtering: Filter
ReplicationGroupStatusobjects by labelsiterecovery.trilio.io/protection-group=<pg-name>. Field selectors onspec.*orstatus.*are not supported on custom resources.
RPOEvent
Cluster: quorum | Namespace: dr-<name> | Short name: rpoe
An immutable audit record of a single replication event (connection loss, RPO violation, resync completion, etc.). Created by the replication monitor; all fields are in spec. You do not create or modify these. Updates are rejected by the admission webhook.
Key spec fields
| Field | Description |
|---|---|
spec.eventType | Type of event: SyncCompleted, RPODegraded, RPOViolated, ConnectionLost, ConnectionRestored, ResyncStarted, ResyncCompleted, VolumeAdded, VolumeRemoved, MetadataUndersized, ResizePending, ResizeComplete. |
spec.timestamp | When the event occurred. Required. |
spec.protectionGroupRef | ProtectionGroup this event pertains to. |
spec.rpoAtEvent | Estimated RPO in seconds at event time. |
spec.outOfSyncBytesAtEvent | Total out-of-sync bytes at event time. |
spec.previousRPOStatus / spec.newRPOStatus | RPO status transition: Met, Degraded, Violated, Unknown. |
spec.violationReason | Why the violation occurred: ConnectionLost, NetworkTimeout, SyncLagExceeded, ProtocolCOutOfSync, ResyncInProgress, Unknown. |
spec.affectedVolumes[] | Per-VM replication state at event time. |
spec.recoveryEstimate | Estimated time to recovery from a violation. |
Label for filtering: Filter
RPOEventobjects by labelsiterecovery.trilio.io/protection-group=<pg-name>.
Checking zone and cluster health
All zone-scoped CRs live in the dr-<name> namespace on the quorum cluster.
# List all protection zones
oc get pz -A --context=<quorum-context>
# Describe a specific zone
oc describe pz production -n dr-production --context=<quorum-context>
Protecting a VM
Create a ProtectionRequest in the zone namespace. The VM must be running on the source cluster at the time of protection.
# Create a ProtectionRequest
oc apply -f protection-request.yaml --context=<quorum-context>
# Watch it progress through phases
oc get pr my-vm-protection -n dr-production --context=<quorum-context> -w
The Phase column progresses: Pending → Validating → CreatingDRBD → Syncing → ReadyToActivate → Activating → Protected.
# Check sync progress
oc get pr my-vm-protection -n dr-production --context=<quorum-context> \
-o jsonpath='{.status.syncProgress}'
Monitoring replication health
# Check the ReplicationGroupStatus for a protection group
oc get rgs -n dr-production --context=<quorum-context>
# The auto-created RGS is named <protection-group-name>-replication
oc get rgs my-pg-replication -n dr-production --context=<quorum-context>
# Get detailed replication metrics
oc describe rgs my-pg-replication -n dr-production --context=<quorum-context>
# Filter ReplicationGroupStatus objects by protection group (use labels, not field selectors)
oc get rgs -n dr-production -l siterecovery.trilio.io/protection-group=my-pg \
--context=<quorum-context>
Checking DRBDResources on a workload cluster
DRBDResource CRs live in the VM's namespace on the workload cluster, not on the quorum cluster.
# List all DRBDResources in the VM's namespace on the primary cluster
oc get dres -n my-vm-namespace --context=<primary-context>
# Check the role and phase of a specific DRBDResource
oc get dres drbd-my-vm-namespace-my-vm-a1b2c3d4 -n my-vm-namespace \
--context=<primary-context>
Finding protected VMs
# The siterecovery.trilio.io/protected-vm=true label is stamped on VirtualMachine objects only
oc get vm -A -l siterecovery.trilio.io/protected-vm=true --context=<primary-context>
Note: This label is on
VirtualMachineobjects on the workload cluster only. Queryingoc get proroc get dreswith this label returns nothing.
Running a test failover
oc apply -f test-failover.yaml --context=<quorum-context>
# Watch phases
oc get tfo my-test -n dr-production --context=<quorum-context> -w
# Read the generated test namespace (never construct or guess it)
oc get tfo my-test -n dr-production --context=<quorum-context> \
-o jsonpath='{.status.testNamespace}'
Triggering a planned failover
oc apply -f failover-request-planned.yaml --context=<quorum-context>
# Monitor state and phase
oc get fr my-failover -n dr-production --context=<quorum-context> -w
# Check per-VM status (not status.message or status.failedVMs — those fields do not exist)
oc get fr my-failover -n dr-production --context=<quorum-context> \
-o jsonpath='{.status.vmStatuses}'
Viewing RPO events
# Filter RPOEvents by protection group label (field selectors on spec.* are not supported)
oc get rpoe -n dr-production -l siterecovery.trilio.io/protection-group=my-pg \
--context=<quorum-context>
Adjusting runtime log levels
Log levels take effect within ~15 seconds without pod restarts. Edit the ProtectionZone:
oc patch pz production -n dr-production --type=merge \
-p '{"spec":{"logging":{"level":"debug"}}}' \
--context=<quorum-context>
Example 1 — ProtectionZone with RHEL CoreOS 9 nodes
A minimal zone pairing two RHEL 9-based OpenShift clusters.
apiVersion: siterecovery.trilio.io/v1alpha1
kind: ProtectionZone
metadata:
name: production
namespace: dr-production
spec:
displayName: "Production DR Zone"
description: "Primary to DR site DRBD replication."
storageBackendMode: DRBD
clusters:
- name: primary
clusterID: cluster1
distributionName: RHEL-CoreOS-9
kubeconfigSecretRef:
name: cluster1-kubeconfig
key: kubeconfig
- name: dr
clusterID: cluster2
distributionName: RHEL-CoreOS-9
kubeconfigSecretRef:
name: cluster2-kubeconfig
key: kubeconfig
Expected oc get pz output when healthy:
NAME DISPLAY NAME BACKEND CLUSTERS STATUS AGE
production Production DR Zone DRBD primary, dr Available 5m
Example 2 — ProtectionGroup with boot-order sequencing
A group of three VMs where the database boots first, then the application tier, then the load balancer.
apiVersion: siterecovery.trilio.io/v1alpha1
kind: ProtectionGroup
metadata:
name: ecommerce
namespace: dr-production
spec:
sourceCluster: cluster1
desiredState: running
virtualMachines:
- name: db-primary
namespace: ecommerce
bootOrder: 1
- name: app-server-1
namespace: ecommerce
bootOrder: 2
- name: app-server-2
namespace: ecommerce
bootOrder: 2
- name: load-balancer
namespace: ecommerce
bootOrder: 3
Expected oc get pg output:
NAME VMS STATE ACTIVE AGE
ecommerce db-primary,app-server-1,app-server-2,... Protected cluster1 10m
Example 3 — ProtectionRequest for a single VM
Protecting db-primary with a custom metadata storage class and 256Mi metadata PVCs.
apiVersion: siterecovery.trilio.io/v1alpha1
kind: ProtectionRequest
metadata:
name: db-primary-protection
namespace: dr-production
spec:
sourceCluster: cluster1
virtualMachine:
name: db-primary
namespace: ecommerce
replicationConfig:
replicationProtocol: C
rpo: 0
drbdMetadataConfig:
storageClassName: fast-ssd
size: 256Mi
Watch the phases:
oc get pr db-primary-protection -n dr-production --context=<quorum-context> -w
NAME VM NAMESPACE PHASE SYNC AGE
db-primary-protection db-primary ecommerce Syncing 67 2m
db-primary-protection db-primary ecommerce Protected 100 4m
Example 4 — Planned FailoverRequest
Failing the ecommerce protection group over to cluster2 gracefully.
apiVersion: siterecovery.trilio.io/v1alpha1
kind: FailoverRequest
metadata:
name: planned-failover-20260801
namespace: dr-production
spec:
protectionGroupRef:
name: ecommerce
namespace: dr-production
targetCluster: cluster2
failoverType: planned
batchBootTimeoutSeconds: 300
Monitor progress:
oc get fr planned-failover-20260801 -n dr-production --context=<quorum-context> -w
NAME PROTECTION GROUP TARGET STATE PHASE AGE
planned-failover-20260801 ecommerce cluster2 InProgress StoppingSource 30s
planned-failover-20260801 ecommerce cluster2 Completed Completed 3m
Check per-VM status:
oc get fr planned-failover-20260801 -n dr-production --context=<quorum-context> \
-o jsonpath='{.status.vmStatuses[*]}'
Example 5 — Unplanned FailoverRequest
Emergency failover when the primary cluster is unavailable.
apiVersion: siterecovery.trilio.io/v1alpha1
kind: FailoverRequest
metadata:
name: emergency-failover-20260801
namespace: dr-production
spec:
protectionGroupRef:
name: ecommerce
namespace: dr-production
targetCluster: cluster2
failoverType: unplanned
Example 6 — TestFailover with HTTP reachability probes
A non-disruptive DR validation with automatic cleanup after 4 hours, checking that the web tier responds on port 8080.
apiVersion: siterecovery.trilio.io/v1alpha1
kind: TestFailover
metadata:
name: quarterly-dr-test
namespace: dr-production
spec:
protectionGroupRef:
name: ecommerce
namespace: dr-production
cleanupPolicy: Automatic
retentionTime: 4h
verification:
reachability:
- type: http
vmName: app-server-1
port: 8080
endpoint: /health
expectedStatus: 200
timeout: 30s
- type: tcp
vmName: db-primary
port: 5432
timeout: 10s
Watch the test progress:
oc get tfo quarterly-dr-test -n dr-production --context=<quorum-context> -w
NAME PROTECTION GROUP PHASE PASSED AGE
quarterly-dr-test ecommerce Verifying <none> 8m
quarterly-dr-test ecommerce Succeeded true 12m
Read the generated test namespace:
oc get tfo quarterly-dr-test -n dr-production --context=<quorum-context> \
-o jsonpath='{.status.testNamespace}'
Example 7 — Checking replication health for a protection group
# The RGS is auto-named <pg-name>-replication
oc get rgs ecommerce-replication -n dr-production --context=<quorum-context>
NAME PROTECTION-GROUP RPO-STATUS OUT-OF-SYNC SYNCED CONNECTION LAST-SYNC
ecommerce-replication ecommerce Met 0 4 Connected 2m
# List RPO violation events
oc get rpoe -n dr-production \
-l siterecovery.trilio.io/protection-group=ecommerce \
--context=<quorum-context>
Example 8 — Enabling debug logging for a zone
oc patch pz production -n dr-production --type=merge \
-p '{"spec":{"logging":{"level":"info","controlPlaneLevel":"debug"}}}' \
--context=<quorum-context>
Log changes take effect within ~15 seconds. To revert:
oc patch pz production -n dr-production --type=merge \
-p '{"spec":{"logging":null}}' \
--context=<quorum-context>
Issue 1 — ProtectionZone stuck in Provisioning or Unavailable
Symptom: oc get pz shows STATUS: Provisioning or Unavailable long after creation.
Likely causes:
- A workload cluster's API server is unreachable from the quorum cluster (TCP port 6443 blocked).
- A kubeconfig Secret is missing, has the wrong key, or references an expired credential.
- The
trilio-site-recovery-systemnamespace does not exist on a workload cluster (the reachability probe GETs this namespace by name).
Fix:
# Check per-cluster reachability
oc describe pz <zone-name> -n dr-<name> --context=<quorum-context>
# Look at status.clusters[].status and status.clusters[].message for the failure detail
# Verify the kubeconfig secret resolves
oc get secret <secret-name> -n dr-<name> --context=<quorum-context>
# Verify the workload namespace exists on the workload cluster
oc get namespace trilio-site-recovery-system --context=<workload-context>
Issue 2 — ProtectionRequest stuck in CreatingDRBD or Syncing
Symptom: oc get pr shows Phase: CreatingDRBD or Phase: Syncing and does not advance.
Likely causes:
- DRBD replication ports (7000–8000) are blocked between primary and DR worker nodes. Port 8000 is mandatory; a firewall rule that stops at 7999 silently prevents replication on one volume.
- The DRBD kernel module is not loaded on the target node. Check the node agent DaemonSet.
- The node hosting the protected VM is tainted with
siterecovery.trilio.io/not-ready:NoSchedule; wait for DRBD to converge. - The metadata PVC StorageClass uses
WaitForFirstConsumerbinding — these are rejected during validation.
Fix:
# Check protection request conditions
oc describe pr <pr-name> -n dr-<name> --context=<quorum-context>
# Check the DRBDResource on the primary workload cluster
oc get dres -n <vm-namespace> --context=<primary-context>
oc describe dres drbd-<namespace>-<vm>-<hash> -n <vm-namespace> --context=<primary-context>
# Check node agent pods on the workload cluster
oc get pods -n trilio-site-recovery-system \
-l app.kubernetes.io/component=drbd-node-agent \
--context=<primary-context>
# Get logs for the node agent on a specific node (two-step: get pod name, then logs)
POD=$(oc get pods -n trilio-site-recovery-system --context=<primary-context> \
--field-selector spec.nodeName=<node-name> \
-l app.kubernetes.io/component=drbd-node-agent \
-o jsonpath='{.items[0].metadata.name}')
oc logs $POD -n trilio-site-recovery-system --context=<primary-context>
Issue 3 — ProtectionRequest admission rejected: ReplicationProtocolA is not supported yet
Symptom: Creating a ProtectionRequest fails with this message.
Cause: spec.replicationConfig.replicationProtocol: A (asynchronous) is feature-gated off in Tech Preview. Only Protocol C is supported.
Fix: Remove replicationProtocol from your manifest (it defaults to C) or explicitly set it to C:
spec:
replicationConfig:
replicationProtocol: C
rpo: 0
Issue 4 — ProtectionRequest admission rejected: rpo must be 0 when replicationProtocol is C
Symptom: Creating a ProtectionRequest fails with this message.
Cause: spec.replicationConfig.rpo is set to a non-zero value while using Protocol C. Protocol C is synchronous and always provides RPO=0; a non-zero RPO is contradictory.
Fix: Set rpo: 0 (or omit it, since 0 is the default).
Issue 5 — ProtectionZone rejected: Linstor backend is not supported yet; use DRBD
Symptom: Creating or updating a ProtectionZone with storageBackendMode: Linstor fails.
Cause: The CRD schema accepts Linstor, but the admission webhook rejects it. Only DRBD is supported in Tech Preview.
Fix: Set spec.storageBackendMode: DRBD.
Issue 6 — FailoverRequest stuck in InProgress
Symptom: oc get fr shows STATE: InProgress and does not advance to Completed.
Fix:
# Check the granular phase
oc get fr <fr-name> -n dr-<name> --context=<quorum-context>
# Check per-VM status for stuck VMs
oc get fr <fr-name> -n dr-<name> --context=<quorum-context> \
-o jsonpath='{.status.vmStatuses}'
# Check failover controller logs on the quorum cluster
# The quorum deployment runs a single container named 'manager'
oc logs -n dr-<name> -l app.kubernetes.io/component=quorum-control-plane \
--context=<quorum-context> -c manager
Note: spec.drainTimeoutSeconds is accepted and range-checked but is not consumed by the failover controller in Tech Preview. It has no effect on how long the controller waits for VM shutdown.
Issue 7 — kubectl get drbdresource on the quorum cluster returns server doesn't have a resource type
Symptom: Running oc get dres -n <ns> --context=<quorum-context> fails with this error.
Cause: DRBDResource CRDs are registered only on workload clusters, not on the quorum cluster. This is by design.
Fix: Run the command against the correct workload cluster:
oc get dres -n <vm-namespace> --context=<primary-context>
Issue 8 — oc get pg against a workload cluster returns no results or an error
Symptom: Querying ProtectionGroup on a primary or DR cluster returns empty or server doesn't have a resource type.
Cause: ProtectionGroup (and all other CRDs except DRBDResource) are registered on the quorum cluster only.
Fix: Run against the quorum cluster:
oc get pg -n dr-<name> --context=<quorum-context>
Issue 9 — Live migration of a protected VM is denied
Symptom: Attempting to live-migrate a protected VM produces an admission error: live migration of VM "<name>" is not allowed: it uses DRBD-protected volumes with node-pinned PVs.
Cause: Protected VMs are pinned to one node per cluster for the lifetime of the protection. Live migration and cold migration are both denied. oc adm drain of a node hosting a running protected VM will also not complete.
Fix: To move a protected VM to a different node, you must unprotect it, move it, then re-protect it. Re-protecting costs a full initial resynchronization.
Issue 10 — TestFailover phases show Failed; test namespace is stuck
Symptom: A TestFailover CR is in Failed phase. The test namespace persists.
Fix:
# Read the actual generated test namespace name
oc get tfo <tfo-name> -n dr-<name> --context=<quorum-context> \
-o jsonpath='{.status.testNamespace}'
# Check conditions for the root cause
oc describe tfo <tfo-name> -n dr-<name> --context=<quorum-context>
# Delete the TestFailover CR to trigger cleanup (do NOT patch finalizers manually)
oc delete tfo <tfo-name> -n dr-<name> --context=<quorum-context>
Do not construct or guess the test namespace name, and do not patch finalizers to force deletion.
Issue 11 — ReplicationGroupStatus shows rpoStatus: Violated
Symptom: oc get rgs shows RPO-STATUS: Violated.
Fix:
# Check which VMs are out of sync and why
oc describe rgs <pg-name>-replication -n dr-<name> --context=<quorum-context>
# Check RPO events for the violation reason
oc get rpoe -n dr-<name> \
-l siterecovery.trilio.io/protection-group=<pg-name> \
--context=<quorum-context>
# Common violation reasons and their remediation:
# ConnectionLost / NetworkTimeout -> Check TCP 7000-8000 between worker nodes
# ProtocolCOutOfSync -> Check DRBD status on the workload clusters via DRBDResource
oc get dres -n <vm-namespace> --context=<primary-context>
Issue 12 — Node taint siterecovery.trilio.io/not-ready:NoSchedule blocking workloads
Symptom: New pods cannot schedule on a node after a reboot or node agent restart.
Cause: The DRBD node agent applies this taint at startup and removes it only after every DRBD resource on that node is connected. This is expected behavior during DRBD convergence.
Fix: Wait for DRBD to converge. Monitor:
# Check node agent status on the affected node
POD=$(oc get pods -n trilio-site-recovery-system --context=<workload-context> \
--field-selector spec.nodeName=<node-name> \
-l app.kubernetes.io/component=drbd-node-agent \
-o jsonpath='{.items[0].metadata.name}')
oc logs $POD -n trilio-site-recovery-system --context=<workload-context>
# Check DRBDResources for the node's VMs
oc get dres -n <vm-namespace> --context=<workload-context>
If the node is permanently lost, follow docs/runbooks/drbdresource-node-loss.md for the node-loss recovery procedure. Remove only the agent finalizer — removing the placement finalizer permanently leaks PVC/PV pairs.