Components
API service, workers/agents, scheduler/conductor, RPC, database
This page describes the internal architecture of Trilio Site Recovery for OpenStack: the two service processes that make up the product, how they communicate, what each one does, and how they relate to the database and external systems. Understanding this architecture helps you diagnose failures, tune deployments, and reason about what happens during a DR operation. The service is split into two cooperating processes — protector-api and protector-engine — that communicate over RabbitMQ RPC, share a MariaDB/MySQL database, and delegate storage operations to a pluggable backend driver.
This page assumes you are familiar with the following:
- Basic OpenStack service architecture (WSGI API process, background worker, oslo.messaging)
- The Trilio Site Recovery deployment model for your target platform (Juju, Kolla-Ansible, or OpenShift)
- The
openstack drCLI installed from thepython3-triliositerecoveryDebian package
Before the service can participate in any DR operation:
- Both DR sites must be registered and reachable on port 8788 (protector API), 5000 (Keystone), 8774 (Nova), and 8776 (Cinder)
- The
local_siteoption in the[DEFAULT]section of/etc/protector/protector.confmust be set to the name of the DR site this instance belongs to - The
dr_operatoranddr_site_adminKeystone roles must exist on both sites - The MariaDB/MySQL
protectordatabase must exist and be at schema head021_add_member_status_reason
The service is not installed in isolation — each deployment tool installs and wires up both the API and engine together. Choose the path that matches your platform.
Juju (Charmed OpenStack)
The trilio-site-recovery Juju charm installs and manages both service processes on Ubuntu Jammy or Noble machines.
-
Configure the Trilio apt repository. The charm reads its package source from the
trilio-pkg-sourceconfig key, which defaults to the Trilio apt repository:deb [trusted=yes] https://apt.fury.io/trilio-os-site-recovery-master/ /This is written to
/etc/apt/sources.list.d/trilio-site-recovery.liston each managed machine. -
Deploy the charm:
juju deploy trilio-site-recovery-for-openstack --channel latest/stable -
Add the required relations:
juju integrate trilio-site-recovery-for-openstack:shared-db mysql-innodb-cluster:db-router juju integrate trilio-site-recovery-for-openstack:amqp rabbitmq-server:amqp juju integrate trilio-site-recovery-for-openstack:identity-credentials keystone:identity-credentialsNote: The charm's relation endpoint names are
shared-db,amqp, andidentity-credentials— use these exact names. -
Set the required configuration (the unit will remain in BLOCKED state until these are provided):
juju config trilio-site-recovery-for-openstack local-site=site-primaryIf you are using Pure Storage, also set:
juju config trilio-site-recovery-for-openstack \ pure-primary-site-name=site-primary \ pure-secondary-site-name=site-secondary -
Run the database migration:
juju run trilio-site-recovery-for-openstack/leader db-sync -
Register the service in Keystone:
juju run trilio-site-recovery-for-openstack/leader register-service -
(Optional) Install the Horizon dashboard plugin:
juju deploy trilio-site-recovery-for-openstack-dashboard juju integrate trilio-site-recovery-for-openstack-dashboard:dashboard-plugin openstack-dashboard:dashboard-plugin juju run trilio-site-recovery-for-openstack-dashboard/leader install-plugin
Kolla-Ansible
The Kolla install script deploys the protector_api and protector_engine containers using the protector:latest Docker image alongside a Kolla-managed OpenStack deployment.
-
Run the installer from the deploy directory:
bash deploy/kolla/install.shThe script creates and starts both containers, runs the database migration, and registers the service in Keystone. It polls
/healthzon port 8788 to confirm the API is serving before returning. -
Set
local_sitein/etc/protector/protector.confbefore starting the service if the installer did not set it for your deployment.
OpenShift (RHOSO18)
On Red Hat OpenStack Services on OpenShift, the service is deployed by the deploy/openshift/scripts/install.sh script, which applies all manifests in the correct order including init Jobs for database creation, RabbitMQ setup, Keystone registration, and schema migration.
-
Run the install script:
bash deploy/openshift/scripts/install.shThe script creates the
trilio-site-recoverynamespace, applies the ConfigMap (which contains the fullprotector.conf), adds credentials to the sharedtrilio-openstack-secretin thetrilio-openstacknamespace, runs four init Jobs with completion gates, then deploys thetrilio-site-recovery-apiandtrilio-site-recovery-engineDeployments.Important: Do not apply the manifests from
deploy/openshift/manifests/individually. The init Jobs —job-db-init,job-rabbitmq-init,job-keystone-init, andjob-db-sync— must complete in order before the Deployments are applied. The script handles this sequencing. -
Verify both Deployments are Running:
oc -n trilio-site-recovery get pods \ -l 'app.kubernetes.io/name=trilio-site-recovery,app.kubernetes.io/component=api' oc -n trilio-site-recovery get pods \ -l 'app.kubernetes.io/name=trilio-site-recovery,app.kubernetes.io/component=engine'
Installing the CLI separately
On any machine that runs openstack dr commands but is not managed by the charm, install the client package from the Trilio apt repository:
-
Add the repository:
echo "deb [trusted=yes] https://apt.fury.io/trilio-os-site-recovery-master/ /" \ | sudo tee /etc/apt/sources.list.d/trilio-site-recovery.list sudo apt-get update -
Install the client package:
sudo apt-get install python3-triliositerecovery -
To upgrade the CLI independently (the charm's
upgrade-packagesaction upgrades the service unit only):sudo apt-get install --only-upgrade python3-triliositerecovery
Configuration is managed by oslo.config. The file is written to /etc/protector/protector.conf by the Juju charm or Kolla install script. Do not edit this file by hand on charm-managed deployments — the charm rewrites it on every hook run. Use juju config for options the charm models, and config-flags for anything else.
[DEFAULT]
| Option | Type | Default | Purpose |
|---|---|---|---|
local_site | string | (required) | The registered name of the DR site this instance belongs to. Must be set before any DR operation. The engine uses this to determine which site is local when routing failover, failback, and metadata sync. |
host | string | localhost | The hostname this service reports as. |
rpc_conn_pool_size | integer | 30 | Size of the RPC connection pool shared between API workers. |
use_mock_storage | boolean | false | When true, the mock in-memory storage driver is used instead of the Pure Storage driver. Use for initial validation without hardware. |
use_mock_cinder | boolean | false | When true, Cinder volume operations are simulated. |
cinder_backend_names | list | (empty) | Restrict volume management to named Cinder backends. |
[api]
| Option | Type | Default | Purpose |
|---|---|---|---|
bind_host | string | 0.0.0.0 | Address the API WSGI server binds to. |
bind_port | integer | 8788 | Port the API listens on. The Keystone catalogue and cross-site health checks use this port. |
workers | integer | 4 | Number of gunicorn worker processes. Scale with available CPU cores and expected request concurrency. |
[engine]
| Option | Type | Default | Purpose |
|---|---|---|---|
topic | string | protector-engine | The RabbitMQ topic the engine consumes from and the API dispatches to. Must match on both the API and engine. |
recovery_point_interval_minutes | integer | 0 | Interval between scheduled automatic recovery point creation. 0 disables the scheduler. |
recovery_point_max_count | integer | 20 | Maximum number of recovery points retained per protection group. Older points are pruned. |
recovery_point_ttl_hours | integer | 24 | Time-to-live for recovery points in hours. |
auto_create_security_groups | boolean | false | When true, the engine creates missing security groups on the recovery site during failover instead of failing. |
cross_site_timeout | integer | 30 | Timeout in seconds for cross-site API calls. Increase on high-latency inter-site links. |
To set cross_site_timeout on a Juju-managed deployment (the charm has no dedicated option for it):
juju config trilio-site-recovery-for-openstack \
config-flags="engine.cross_site_timeout=60"
[pure_storage]
Required when storage_driver = pure. Both nodes in a deployment can use an identical [pure_storage] section — the driver resolves which array to talk to by matching the protection group's primary site name against primary_site_name and secondary_site_name.
| Option | Type | Default | Purpose |
|---|---|---|---|
primary_management_ip | string | (required) | Management IP of the Pure FlashArray paired with primary_site_name. |
primary_api_token | string | (required) | API token for the primary array. |
secondary_management_ip | string | (required) | Management IP of the Pure FlashArray paired with secondary_site_name. |
secondary_api_token | string | (required) | API token for the secondary array. |
primary_site_name | string | (required) | The OpenStack site name whose workloads the primary array serves. Must match the registered site name exactly. |
secondary_site_name | string | (required) | The OpenStack site name whose workloads the secondary array serves. |
default_rpo_seconds | integer | 900 | Default RPO target in seconds. Replication intervals that would leave data older than this value trigger a health warning. |
snapshot_interval_seconds | integer | 300 | How often the Pure array takes crash-consistent snapshots of each protection group. |
verify_ssl | boolean | false | Verify TLS certificates when connecting to Pure arrays. |
additional_arrays | list | (empty) | Additional array IPs for multi-array deployments. |
Pairing invariant:
primary_site_namemust name the site whose array is atprimary_management_ip, andsecondary_site_namemust name the site whose array is atsecondary_management_ip. Swapping the names causes failover to read volumes from the wrong array.
[mock_storage]
Used when use_mock_storage = true. State is stored under /var/lib/protector.
| Option | Type | Default | Purpose |
|---|---|---|---|
storage_base_path | string | /var/lib/protector/mock_storage | Directory for mock storage state. |
primary_array_name | string | primary-array | Simulated name for the primary array. |
secondary_array_name | string | secondary-array | Simulated name for the secondary array. |
[storage]
| Option | Type | Default | Purpose |
|---|---|---|---|
storage_driver | string | mock | The replication backend driver. mock for evaluation without hardware; pure for Pure Storage FlashArray in production. |
[service_credentials]
Controls the Keystone trust delegation the engine uses when performing cross-site operations on behalf of tenants.
| Option | Type | Default | Purpose |
|---|---|---|---|
default_trust_roles | string | member,dr_operator | Roles delegated by every per-tenant Keystone trust. Both roles must exist in Keystone before the first DR operation; trust creation fails naming a missing role. Do not remove member — it grants the Nova and Cinder access the trust needs. |
username | string | (set by charm) | The Keystone service account username (protector). |
project_name | string | (set by charm) | The project the service account belongs to. |
cafile | string | (optional) | Path to CA bundle for verifying Keystone TLS. |
insecure | boolean | false | Disable TLS verification for Keystone calls. |
[keystone_authtoken]
| Option | Type | Purpose |
|---|---|---|
auth_url | string | Keystone authentication URL for token validation. |
www_authenticate_uri | string | Public Keystone endpoint advertised in WWW-Authenticate headers on 401 responses. |
[database]
| Option | Type | Purpose |
|---|---|---|
connection | string | SQLAlchemy connection URL for the protector MariaDB/MySQL database (e.g. mysql+pymysql://protector:PASSWORD@HOST/protector). |
Annotated sample configuration
[DEFAULT]
# REQUIRED: the registered name of this DR site
local_site = site-primary
# Set to 'pure' for production, 'mock' for evaluation without hardware
use_mock_storage = false
[api]
bind_host = 0.0.0.0
bind_port = 8788
workers = 4
[engine]
topic = protector-engine
recovery_point_interval_minutes = 60
recovery_point_max_count = 20
recovery_point_ttl_hours = 24
auto_create_security_groups = false
cross_site_timeout = 30
[database]
connection = mysql+pymysql://protector:PASSWORD@db-host/protector
[keystone_authtoken]
auth_url = https://keystone.primary.example.com:5000
www_authenticate_uri = https://keystone.primary.example.com:5000
[service_credentials]
username = protector
project_name = service
# Do not change this; both roles must exist in Keystone
default_trust_roles = member,dr_operator
[storage]
storage_driver = pure
[pure_storage]
primary_management_ip = 10.10.1.10
# primary_api_token contains a secret — inspect with grep, not section anchors
primary_api_token = T-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
secondary_management_ip = 10.20.1.10
secondary_api_token = T-yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy
primary_site_name = site-primary
secondary_site_name = site-secondary
default_rpo_seconds = 900
snapshot_interval_seconds = 300
verify_ssl = false
To inspect a specific option on a live deployment without exposing secrets:
grep '^local_site' /etc/protector/protector.conf
grep '^default_trust_roles' /etc/protector/protector.conf
grep '^storage_driver' /etc/protector/protector.conf
Service health
The API exposes an unauthenticated health endpoint at /healthz on port 8788. It returns HTTP 200 when the API can serve requests and HTTP 503 when it cannot (for example, when the database is unreachable). This endpoint is used by the Kolla container HEALTHCHECK, the installer, and cross-site peer checks.
curl -s -o /dev/null -w "%{http_code}" http://protector-api.primary.example.com:8788/healthz
You can also run the charm health check action:
juju run trilio-site-recovery-for-openstack/leader health-check
Checking service status
The observability surface for Trilio Site Recovery is polled, not event-driven. The service does not emit oslo.messaging notifications. Monitor it using the CLI commands below.
Replication health for a specific protection group:
openstack dr replication health my-protection-group
Add --probe to ask the recovery site whether the group's networks, security groups, and flavors resolve there as the project it recovers into. This talks to the recovery site's Neutron and Nova through a trust-scoped session — the same session a real recovery would use — and is slower than the plain command. Run it before a planned failover window:
openstack dr replication health my-protection-group --probe
Audit replication across all protection groups:
openstack dr replication audit
List available recovery points:
openstack dr recovery point list my-protection-group
List recent DR operations:
openstack dr operation list
openstack dr operation list --protection-group my-protection-group
openstack dr operation list --status failed
Show details of a specific operation:
openstack dr operation show OPERATION-ID
The operation record includes a progress field (0–100) and the Source Site and Target Site by name. There is no estimated completion time — the service does not compute one.
Database migration
The supported way to run schema migrations is protector-manage db sync. It reads /etc/protector/protector.conf by default.
On Juju deployments:
juju run trilio-site-recovery-for-openstack/leader db-sync
On Kolla and manual deployments:
protector-manage db sync
# Or with an explicit config path:
protector-manage --config-file /etc/protector/protector.conf db sync
Migrations must run before starting a new version of the service. The current schema head is 021_add_member_status_reason.
To check the current schema revision:
protector-manage db current
Log access
Juju and Kolla deployments write logs to /var/log/protector/. View them with:
tail -f /var/log/protector/protector-api.log
tail -f /var/log/protector/protector-engine.log
Or via Juju:
juju debug-log --include trilio-site-recovery-for-openstack
OpenShift deployments log to stdout only. Use the label selectors to target the correct pods:
# API pods
oc -n trilio-site-recovery logs -f \
-l 'app.kubernetes.io/name=trilio-site-recovery,app.kubernetes.io/component=api'
# Engine pods
oc -n trilio-site-recovery logs -f \
-l 'app.kubernetes.io/name=trilio-site-recovery,app.kubernetes.io/component=engine'
Python client library
The python3-triliositerecovery package includes the triliositerecovery Python client library for programmatic access. Import it from triliositerecovery.v1.client:
from keystoneauth1 import identity, session
from keystoneauth1.adapter import Adapter
from triliositerecovery.v1.client import Client
auth = identity.v3.Password(
auth_url='https://keystone.primary.example.com:5000/v3',
username='myuser',
password='mypassword',
project_name='myproject',
user_domain_name='Default',
project_domain_name='Default',
)
ks_session = session.Session(auth=auth)
adapter = Adapter(
session=ks_session,
service_type='disaster-recovery',
endpoint_override='http://protector-api.primary.example.com:8788',
region_name='RegionOne',
)
client = Client(session=adapter, endpoint='http://protector-api.primary.example.com:8788')
Key conventions when using the client:
- Return shape: Every manager method returns parsed JSON — plain Python dicts and lists. Use
site['id'], notsite.id. - Resource ID is positional: Pass the protection group ID as the first positional argument, not as a keyword. For example:
client.protection_groups.failover(pg_id, failover_type='planned'), notclient.protection_groups.failover(protection_group_id=pg_id). - Mappings are dicts: Pass network and flavor mappings as Python dicts (
network_mapping={'src-net-id': 'dst-net-id'}), not as'src=dst'strings. Thesrc=dstformat is argparse-only and is parsed into a dict by the CLI before it reaches the library. endpointis required: Omitting it causes all request URLs to begin withNone/and fail at the first call.
List all registered DR sites
openstack dr site list
Register a secondary DR site
openstack dr site create site-secondary \
--auth-url https://keystone.secondary.example.com:5000 \
--region RegionTwo \
--service-username protector \
--service-password SECRET \
--service-project-name service \
--service-user-domain-name Default \
--service-project-domain-name Default
Create a protection group
Both --primary-site and --secondary-site are required — the secondary site is never inferred. When --replication-type async is used (the default), --volume-type is also required:
openstack dr protection group create my-app-pg \
--primary-site site-primary \
--secondary-site site-secondary \
--replication-type async \
--volume-type ceph-replicated
Add a VM to a protection group
openstack dr protection group member add my-app-pg \
--instance db-server-uuid \
--boot-order 1
openstack dr protection group member add my-app-pg \
--instance app-server-uuid \
--boot-order 2
Check replication health before a failover window
The plain command checks locally cached replication state:
openstack dr replication health my-app-pg
Add --probe to verify that networks, security groups, and flavors resolve on the recovery site as the project the group recovers into:
openstack dr replication health my-app-pg --probe
Create an on-demand recovery point
openstack dr recovery point create my-app-pg --suffix pre-upgrade
Execute a test failover
Test failover is non-disruptive and must be run from the secondary site. It clones replicated data and boots test VMs in isolation:
openstack dr test failover my-app-pg \
--network-mapping src-net-id=dst-net-id \
--instance-prefix test- \
--wait
Note: The separator in
--network-mappingis=, not:. A colon-separated value is silently accepted by argparse as one string and the mapping does not apply.
Clean up after a test failover
openstack dr test failover cleanup my-app-pg
Execute a planned failover
openstack dr failover my-app-pg \
--failover-type planned \
--network-mapping src-net-id=dst-net-id \
--flavor-mapping src-flavor-id=dst-flavor-id \
--wait
After a successful failover, recovered instances are named <original-name>-failover.
Execute an unplanned failover
openstack dr failover my-app-pg \
--failover-type unplanned \
--force \
--wait
Fail back to the primary site
openstack dr failback my-app-pg \
--network-mapping dst-net-id=src-net-id \
--wait
After a successful failback, instances on the primary site are named <original-name>-restored.
Monitor a running operation
openstack dr operation list --protection-group my-app-pg --status running
openstack dr operation show OPERATION-ID
The progress field reports 0–100. There is no estimated completion time.
Run the replication audit
openstack dr replication audit
Create a tenant mapping (admin operation)
Tenant mappings are required when primary and secondary sites use independent Keystone deployments that assign different UUIDs to the same project. Without a mapping, the engine assumes the project UUID is identical on the peer site.
openstack dr tenant mapping create \
--local-tenant PRIMARY-PROJECT-UUID \
--remote-site site-secondary \
--remote-tenant SECONDARY-PROJECT-UUID
Execute a site-level failover (cloud admin operation)
Site-level failover requires the dr_site_admin role. It fails over all protection groups at the named site:
openstack dr site failover site-primary --failover-type unplanned
Alternatively, from the recovery site:
openstack dr failover to this site --failover-type unplanned
Python client: list protection groups and check health
from keystoneauth1 import identity, session
from keystoneauth1.adapter import Adapter
from triliositerecovery.v1.client import Client
auth = identity.v3.Password(
auth_url='https://keystone.primary.example.com:5000/v3',
username='myuser',
password='mypassword',
project_name='myproject',
user_domain_name='Default',
project_domain_name='Default',
)
ks_session = session.Session(auth=auth)
adapter = Adapter(
session=ks_session,
service_type='disaster-recovery',
endpoint_override='http://protector-api.primary.example.com:8788',
region_name='RegionOne',
)
client = Client(
session=adapter,
endpoint='http://protector-api.primary.example.com:8788',
)
# Managers return lists of plain dicts — use dict indexing, not attribute access
groups = client.protection_groups.list()
for pg in groups:
health = client.protection_groups.replication_health(pg['id'])
print(pg['name'], health)
# cleanup_test_failover is the correct method name
client.protection_groups.cleanup_test_failover(pg_id)
Run database migration directly
# On Juju
juju run trilio-site-recovery-for-openstack/leader db-sync
# On Kolla or manual deployments
protector-manage db sync
# With an explicit config path
protector-manage --config-file /etc/protector/protector.conf db sync
# Check current schema revision
protector-manage db current
The unit is in BLOCKED state after charm deployment or upgrade
Symptom: juju status shows the trilio-site-recovery-for-openstack unit in BLOCKED with the message set local-site, pure-primary-site-name, pure-secondary-site-name.
Cause: The charm requires local-site to be configured before it can start the service. If you are using Pure Storage, pure-primary-site-name and pure-secondary-site-name must also be set. This is expected behaviour after a fresh deploy or upgrade — it is not a failed deployment.
Fix:
juju config trilio-site-recovery-for-openstack local-site=site-primary
# If using Pure Storage:
juju config trilio-site-recovery-for-openstack \
pure-primary-site-name=site-primary \
pure-secondary-site-name=site-secondary
Failover fails with HTTP 403 "Policy doesn't allow volume_extension:volume_manage"
Symptom: The service deploys and reports healthy. The first failover attempt stops partway through with a Cinder 403 error referencing volume_extension:volume_manage or a similar extension.
Cause: Cinder's default policy restricts the volume manage, unmanage, and related APIs to admin. The service runs as a tenant through a Keystone trust, so it is blocked. All five Cinder policy rules must be relaxed on both sites:
"volume_extension:volume_manage": "rule:admin_or_owner"
"volume_extension:volume_unmanage": "rule:admin_or_owner"
"volume_extension:services:index": "rule:admin_or_owner"
"volume_extension:type_get": "rule:admin_or_owner"
"volume_extension:type_get_all": "rule:admin_or_owner"
Fix: Apply these rules to the Cinder policy on both the primary and secondary sites. On Juju-managed Cinder, use the policyd-override resource (not policyd):
juju attach-resource cinder policyd-override=./cinder-policy-override.yaml
DR operations fail with "Trust-based authentication failed" or "No trust found for tenant"
Symptom: Operations that require cross-site access fail with TrustAuthFailed (HTTP 401) or TrustNotFoundForTenant (HTTP 404).
Cause: The most common causes are:
- The
dr_operatorrole does not exist in Keystone on one or both sites. Trust creation fails naming the missing role rather than skipping it. default_trust_roleshas been changed to omitmember, removing the Nova and Cinder access the trust needs.- No tenant mapping exists for a deployment with independent Keystone instances, so the engine is scoping its trust to a project UUID that does not exist on the peer site.
Fix:
-
Verify both roles exist on both sites:
openstack role list | grep -E 'dr_operator|dr_site_admin' -
Check
default_trust_roles:grep '^default_trust_roles' /etc/protector/protector.confThe value must be
member,dr_operator. -
If your sites use independent Keystone deployments with different project UUIDs, create a tenant mapping:
openstack dr tenant mapping create \ --local-tenant PRIMARY-PROJECT-UUID \ --remote-site site-secondary \ --remote-tenant SECONDARY-PROJECT-UUID
openstack dr commands fail with "unrecognized arguments: --site-type"
Symptom: A script or documentation example calling openstack dr site create with a --site-type flag fails.
Cause: The --site-type flag was removed. A site is registered by name only; primary and secondary are roles of a protection group, not a site property.
Fix: Remove --site-type from the command. The correct form is:
openstack dr site create site-secondary --auth-url https://keystone.secondary.example.com:5000
Failover reports "2 of 2 volumes missing or destroyed"
Symptom: Failover completes with an error indicating all volumes are missing or destroyed on the target site, even though replication appeared healthy.
Cause: The [pure_storage] section on this node has primary_site_name and secondary_site_name swapped relative to the primary_management_ip and secondary_management_ip they are paired with. The driver resolves which array to read volumes from by matching the protection group's primary site name against the configured site names. A mismatch causes it to read from the wrong array.
Fix: Verify that primary_site_name names the site whose array is at primary_management_ip, and secondary_site_name names the site at secondary_management_ip. An identical [pure_storage] section works correctly on both nodes:
grep '^primary_site_name\|^secondary_site_name\|^primary_management_ip\|^secondary_management_ip' \
/etc/protector/protector.conf
The service reports HTTP 409 instead of HTTP 400 on invalid state transitions
Symptom: An attempt to perform a DR operation on a protection group in the wrong state (for example, failing over a group that is already failed_over) returns HTTP 409, not 400.
Cause: InvalidState is defined as HTTP 409 on all endpoints, including /action. This is correct and intentional.
Operations left in running or pending state after engine restart
Symptom: After an engine process restart, some operations remain in running or pending state and the protection group appears stuck.
Cause: When the engine restarts, it recovers orphaned operations from the previous run. It uses the checkpoint phase saved in the operation's result_data to determine the correct terminal state for both the operation and the protection group.
Fix: The engine handles this automatically on startup. If a group remains stuck after the engine has fully started, use the reconcile command to inspect live infrastructure and reset state:
openstack dr protection group reconcile my-app-pg
No logs visible on an OpenShift deployment
Symptom: oc logs returns "No resources found" or no output.
Cause: The pod label selector is wrong. There is no bare app label on any pod in the trilio-site-recovery namespace. Both app.kubernetes.io/name and app.kubernetes.io/component are required.
Fix: Use the correct label selectors:
# API logs
oc -n trilio-site-recovery logs -f \
-l 'app.kubernetes.io/name=trilio-site-recovery,app.kubernetes.io/component=api'
# Engine logs
oc -n trilio-site-recovery logs -f \
-l 'app.kubernetes.io/name=trilio-site-recovery,app.kubernetes.io/component=engine'
On OpenShift, the service logs to stdout only. There is no /var/log/protector/ directory in the containers.
Adding a member fails with "Instance is not a Cinder boot instance"
Symptom: openstack dr protection group member add fails with InstanceNotCinderBoot (HTTP 422).
Cause: All VMs in a protection group must boot from a Cinder volume. Instances that boot from ephemeral or image-backed storage cannot be replicated by this service.
Fix: Migrate the instance to boot from a Cinder volume before adding it to a protection group. LUKS-encrypted volumes are also unsupported — use array-side encryption (Pure data-at-rest) instead.
config-flags setting is silently ignored or causes DuplicateSectionError
Symptom: A setting applied via config-flags has no effect, or the service fails to start with a DuplicateSectionError.
Cause: If config-flags names a config key that the charm template already renders (in a section the template also writes), the charm appends a second copy of that section and configparser raises DuplicateSectionError. Keys that the charm does not model — such as engine.cross_site_timeout — are safe to set this way.
Fix: Only use config-flags for keys the charm does not have a dedicated option for. Check the list of 47 charm config options with juju config trilio-site-recovery-for-openstack before adding a flag. For options the charm already models, use the corresponding charm config key instead.