Trilio Site Recovery for Kubernetes
Guide

Custom Resource Reference

Complete API reference for all site recovery custom resources


Overview

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.


Prerequisites

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 in deploy/olm-catalog/site-recovery/metadata/annotations.yaml lists com.redhat.openshift.versions: v4.14-v4.19 as 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.
  • oc CLI — 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, channel tech-preview, catalog quay.io/triliovault/site-recovery-catalog), and a ProtectionZone CR created in a dr-<name> namespace — this automatically deploys the workload control planes and DRBD node agents to both workload clusters.

CRD registration by cluster

ClusterRegistered kinds
QuorumProtectionZone, 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.


Installation

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.


Configuration

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.

FieldTypeDefaultDescription
spec.displayNamestring—Human-friendly label shown in the TSR web console. Min 5, max 253 characters.
spec.descriptionstring—Optional free-form text. Max 1024 characters.
spec.storageBackendModestringDRBDStorage 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[].namestring—Operator-facing identifier for the cluster. Must be unique within the zone. Max 63 characters. Immutable after creation.
spec.clusters[].clusterIDstring—Deployment slot: cluster1 or cluster2. The two clusters must occupy different slots. Immutable after creation.
spec.clusters[].kubeconfigSecretRef.namestring—Name of the Secret containing the kubeconfig. Required and must be non-empty. Mutable (credentials can be rotated).
spec.clusters[].kubeconfigSecretRef.keystring—Key within the Secret. Required and must be non-empty. Mutable.
spec.clusters[].distributionNamestringRHEL-CoreOS-9OS 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.levelstring—Runtime log level for all zone components. Values: error, info, debug, trace. Takes effect within ~15 s without pod restarts.
spec.logging.controlPlaneLevelstring—Overrides level for quorum and workload control-plane managers only.
spec.logging.nodeAgentLevelstring—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

FieldDescription
status.phaseZone lifecycle: Provisioning, Available, Unavailable, Degraded, Deleting.
status.clusters[].statusPer-cluster reachability: Reachable or Unreachable.
status.clusters[].messageExplains an Unreachable observation.
status.conditionsDetailed 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.

FieldTypeDefaultDescription
spec.virtualMachines[].namestring—Name of the KubeVirt VirtualMachine. Must be a DNS-1123 label.
spec.virtualMachines[].namespacestring—Namespace of the VM on the workload cluster.
spec.virtualMachines[].bootOrderinteger—Controls failover boot sequencing. Lower values boot first; VMs sharing a value boot in parallel. Optional.
spec.desiredStatestringrunningWhether VMs should be running or stopped on the active cluster.
spec.sourceClusterstring—Deployment-time source cluster (cluster1 or cluster2).
spec.sla.rtoTargetSecondsinteger—Target RTO in seconds. Recorded for compliance; not enforced in Tech Preview.
spec.sla.rpoTargetSecondsinteger0Target RPO in seconds. Not enforced in Tech Preview; Protocol C always gives RPO=0.

Note: spec.sla.rpoTargetSeconds and spec.sla.rtoTargetSeconds are accepted by the API but not consumed by any controller in Tech Preview. Do not rely on them for enforcement.

Status fields

FieldDescription
status.stateOverall state: Protected, Syncing, Degraded, Failed.
status.activeClusterIDWhich cluster (cluster1 or cluster2) currently runs the VMs.
status.protectedVMs[].replicationStatusPer-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.

FieldTypeDefaultDescription
spec.sourceClusterstring—Which cluster the VM currently runs on: cluster1 or cluster2. Required.
spec.virtualMachine.namestring—VM name on the workload cluster. Required.
spec.virtualMachine.namespacestring—VM namespace on the workload cluster. Required; cannot be inferred from the PR's namespace.
spec.replicationConfig.replicationProtocolstringCDRBD 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.rpointeger0RPO 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.storageClassNamestring—StorageClass for per-volume DRBD metadata PVCs. Must bind immediately; WaitForFirstConsumer classes are rejected during validation.
spec.replicationConfig.drbdMetadataConfig.sizequantity128MiSize 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

FieldDescription
status.phaseCurrent lifecycle phase (see above).
status.syncProgressVM-level sync percentage (0–100). Mirrors slowestVolumeSyncProgress.percentage.
status.slowestVolumeSyncProgressProgress of the slowest volume. Authoritative for the replication monitor.
status.drbdResourceRef.nameShared name of both DRBDResource copies (drbd-<namespace>-<vm>-<hash8>).
status.drbdResourceRef.portAllocated replication port (7000–8000).
status.activeClusterCluster currently observed to hold the DRBD Primary for this VM.
status.consistencySummaryHuman-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. nodeName is 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

FieldDescription
spec.sideSource or Target. Immutable; does not change on failover.
spec.roleDesired DRBD role: Primary or Secondary. Set by the protection controller.
spec.nodeNameNode this copy is pinned to. Immutable once set.
spec.portReplication port (7000–8000).
spec.volumes[].volumeNumberStable cross-copy join key (0 = boot disk).
spec.volumes[].pvcNameLocal backing PVC name.
spec.pausedSuspends convergence (maintenance knob). Does not affect deletion.
spec.replicationIntentEnabled (steady state) or Suspended. The webhook forbids Suspended → Enabled in place.
spec.replication.protocolA or C. Materialized from the DRBDReplicationPolicy by the protection controller.

Status fields

FieldDescription
status.phasePending, Provisioning, ReadyToActivate, Syncing, Consistent, Degraded, Failed, Deleting.
status.connectionStateConnection to peer: Connected, Connecting, Disconnected, StandAlone, Unknown.
status.roleObserved DRBD role: Primary, Secondary, Unknown.
status.volumes[].replicationStatePer-volume: Established, SyncSource, SyncTarget, PausedSyncS, PausedSyncT, Off, Unknown.
status.volumes[].syncProgressPer-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>-default with isDefault: true in the zone's namespace when the zone reaches Available. Only the isDefault policy 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.

FieldDefaultDescription
spec.drbdProtocolCWire protocol. C = synchronous (RPO=0).
spec.replicationModeSynchronousSynchronous or Asynchronous. Only Synchronous is effective in Tech Preview.
spec.isDefaultfalseMarks 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.portRangestart: 7000, end: 7999Not 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, and spec.replicationMode: Asynchronous are 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.

FieldTypeDefaultDescription
spec.protectionGroupRef.namestring—Name of the ProtectionGroup to fail over. Required.
spec.protectionGroupRef.namespacestring—Namespace of the ProtectionGroup.
spec.targetClusterstring—Cluster to fail over TO: cluster1 or cluster2. Required.
spec.failoverTypestringplannedplanned (graceful, waits for sync) or unplanned (force-promotes DRBD volumes without waiting for the primary).
spec.batchBootTimeoutSecondsinteger300Max seconds to wait per boot-order batch. Relevant only when the ProtectionGroup defines bootOrder.
spec.drainTimeoutSecondsinteger300Accepted 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)

FieldDescription
status.stateOverall state: Pending, InProgress, Completed, Failed, Cancelled.
status.phaseGranular 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[].phasePer-VM: Pending, Stopping, Stopped, Starting, Running, Failed.
status.sourceClusterCluster 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 was Healthy and Consistent at the time of failure, zero data loss occurs. If replication was Degraded, 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.

FieldTypeDefaultDescription
spec.protectionGroupRef.namestring—Name of the ProtectionGroup to test. Required.
spec.protectionGroupRef.namespacestring—Namespace of the ProtectionGroup. Required; cannot be inferred.
spec.cleanupPolicystringAutomaticAutomatic: test resources are deleted retentionTime after completion. Manual: resources are kept until you delete the CR.
spec.retentionTimestring2hHow long to keep test resources before automatic cleanup. Pattern: ^[0-9]+(s|m|h|d)$.
spec.timeoutstring—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.batchBootTimeoutSecondsinteger300Max seconds per boot-order batch.
spec.verification.reachability[].typestring—Probe protocol: tcp or http. Required.
spec.verification.reachability[].portinteger—Port to probe (1–65535). Required.
spec.verification.reachability[].vmNamestring—Restricts the probe to a named VM in the ProtectionGroup. When empty, runs against all test VMs.
spec.verification.reachability[].endpointstring—HTTP path. Valid for http probes only; rejected for tcp.
spec.verification.reachability[].expectedStatusinteger—Expected HTTP status code. Valid for http probes only; rejected for tcp.
spec.verification.reachability[].timeoutstring30sProbe 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

FieldDescription
status.testNamespaceSystem-generated isolated namespace (site-recovery-<protection-group>-<random>). Never user-provided or manually constructed.
status.targetClusterWhich cluster the test ran on.
status.verificationResults.passedWhether 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)

FieldDefaultDescription
spec.pollingIntervalSeconds15How often the controller polls DRBD metrics (5–300).
spec.rpoObjectiveSeconds0Target RPO in seconds. Not enforced in Tech Preview.

Key status fields

FieldDescription
status.rpoStatusMet, Degraded, Violated, or Unknown.
status.aggregated.connectionStateWorst-case connection state across all VMs: Connected, Degraded, Disconnected.
status.aggregated.syncedVolumeCountNumber of VMs (not disks) currently fully in sync.
status.aggregated.totalVolumeCountTotal number of replicated VMs in the group.
status.aggregated.outOfSyncBytesTotal 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.lastFullySyncedAtWhen every disk of every VM was last fully in sync.

Label for filtering: Filter ReplicationGroupStatus objects by label siterecovery.trilio.io/protection-group=<pg-name>. Field selectors on spec.* or status.* 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

FieldDescription
spec.eventTypeType of event: SyncCompleted, RPODegraded, RPOViolated, ConnectionLost, ConnectionRestored, ResyncStarted, ResyncCompleted, VolumeAdded, VolumeRemoved, MetadataUndersized, ResizePending, ResizeComplete.
spec.timestampWhen the event occurred. Required.
spec.protectionGroupRefProtectionGroup this event pertains to.
spec.rpoAtEventEstimated RPO in seconds at event time.
spec.outOfSyncBytesAtEventTotal out-of-sync bytes at event time.
spec.previousRPOStatus / spec.newRPOStatusRPO status transition: Met, Degraded, Violated, Unknown.
spec.violationReasonWhy the violation occurred: ConnectionLost, NetworkTimeout, SyncLagExceeded, ProtocolCOutOfSync, ResyncInProgress, Unknown.
spec.affectedVolumes[]Per-VM replication state at event time.
spec.recoveryEstimateEstimated time to recovery from a violation.

Label for filtering: Filter RPOEvent objects by label siterecovery.trilio.io/protection-group=<pg-name>.


Usage

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 VirtualMachine objects on the workload cluster only. Querying oc get pr or oc get dres with 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>

Examples

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>

Troubleshooting

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-system namespace 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 WaitForFirstConsumer binding — 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.