REST Endpoints
All HTTP endpoints with request/response schemas
This page documents every HTTP endpoint exposed by the Trilio Site Recovery protector-api service, running on port 8788. It covers request formats, required headers, response schemas, and error codes for all resources — Sites, Protection Groups, Members, DR Operations, Tenant Mappings, Resource Mappings, Replication Policies, Usage Reports, and the version discovery root. Use these endpoints directly when building integrations or automation that goes beyond the openstack dr CLI.
All versioned endpoints require the /v1/{tenant_id}/ prefix; requests that omit the /v1/ segment return HTTP 404. Authentication is performed by Keystone on port 5000 — obtain a token there and pass it as X-Auth-Token on every authenticated call. The OpenStack-API-Version: protector <version> header is optional; when omitted the API defaults to microversion 1.0.
Before calling these endpoints you need:
- Trilio Site Recovery deployed via the Juju charm on both the primary and secondary OpenStack sites.
- python-openstackclient installed via
pip(not via snap — the snap is incompatible with thetriliositerecoveryplugin). - Keystone token — obtain one with
openstack token issueagainst port 5000 of the target site. Pass it asX-Auth-Tokenon every authenticated request. - Tenant (project) UUID — all versioned URLs include
{tenant_id}, your OpenStack project UUID. - Network connectivity — TCP port 8788 must be reachable from the client to both sites, and must be open bidirectionally between the two sites for metadata sync and tenant mapping propagation.
- Keystone roles — your user needs
member+dr_operatoron the DR-enabled project. Site-level operations (site failover, site operations) additionally requiredr_site_admin. Both roles must exist on both sites before any cross-site operation. - Supported API microversions:
1.0(default) and1.1. PassOpenStack-API-Version: protector 1.1to opt into 1.1 behaviour; omitting the header defaults to 1.0. A malformed value or a version outside1.0–1.1returns HTTP 400.
The protector-api service is deployed by the Juju charm; no separate installation step is needed for the server. To call the REST API from a script or integration you only need an HTTP client and a Keystone token.
To install the openstack dr CLI plugin (required for the CLI examples throughout this page):
-
Install python-openstackclient via pip (do not use the snap):
pip install python-openstackclient -
Install the Trilio Site Recovery CLI plugin from the repository's
client/subdirectory:pip install -e ./clientThe package name is
triliositerecovery. Runningpip install -e .from the repository root installs only the server binaries (protector-api,protector-engine,protector-manage) and does not install the CLI. -
Verify the plugin is registered:
openstack dr --helpYou should see the
drcommand group listed. -
Source your OpenStack credentials or set the environment variables your clouds.yaml provides, then obtain a token:
export OS_AUTH_URL=https://keystone-primary.example.com:5000/v3 export OS_PROJECT_ID=<your-tenant-uuid> export OS_USERNAME=<user> export OS_PASSWORD=<password> export OS_USER_DOMAIN_NAME=Default export OS_PROJECT_DOMAIN_NAME=Default TOKEN=$(openstack token issue -f value -c id)Use
$TOKENas the value ofX-Auth-Tokenin every direct HTTP call shown on this page.
The protector-api process is configured via /etc/protector/protector.conf. In a Juju charm deployment do not edit this file directly — the charm overwrites it on every config-changed event. Set options via juju config trilio-site-recovery <key>=<value> using hyphenated option names.
The sections and options that affect API behaviour are:
[api]
| Option | Default | Effect |
|---|---|---|
bind_host | 0.0.0.0 | Interface the API listens on. |
bind_port | 8788 | TCP port. |
workers | 4 | WSGI worker processes. The Juju charm renders this as 2 unless you override with juju config trilio-site-recovery api-workers=<n>. |
[database]
| Option | Default | Effect |
|---|---|---|
connection | mysql+pymysql://protector:password@localhost/protector | SQLAlchemy connection string. In a Juju deployment the charm sets this automatically via the shared-db relation — no manual configuration needed. |
[engine]
| Option | Default | Effect |
|---|---|---|
topic | protector-engine | RabbitMQ topic for API→engine RPC calls. |
cross_site_timeout | 30 | Seconds allowed for any single cross-site API call (Keystone token exchange, remote protector-api). Increase for slow inter-site links. |
auto_create_security_groups | false | When true, security groups missing on the secondary site are created during failover. When false (default), missing groups are reported as warnings. |
recovery_point_interval_minutes | 0 | Interval between scheduled recovery point snapshots for ActiveDR PGs. 0 disables scheduled snapshots. |
recovery_point_max_count | 20 | Maximum on-demand recovery point snapshots retained per PG. |
recovery_point_ttl_hours | 24 | Recovery point snapshots older than this are automatically deleted. |
[service_credentials]
| Option | Default | Effect |
|---|---|---|
default_service_project | service | Default project for service accounts when not set per-site. |
default_service_user_domain | Default | Default user domain for service accounts. |
default_service_project_domain | Default | Default project domain for service accounts. |
default_trust_roles | member,dr_operator | Roles delegated in Keystone trusts. Both must exist on both sites before any cross-site operation. |
cafile | (none) | Path to a PEM CA bundle for verifying TLS on cross-site calls. |
insecure | false | Disable TLS verification. Set true only in development environments. |
[storage]
| Option | Default | Effect |
|---|---|---|
storage_driver | mock | Active storage driver. Must be set to pure for production to activate the Pure Storage FlashArray driver. The mock default is for development and testing only. |
[pure_storage]
| Option | Default | Effect |
|---|---|---|
primary_management_ip | (required) | Management IP of the primary FlashArray. |
primary_api_token | (required) | API token for the primary FlashArray. |
secondary_management_ip | (required) | Management IP of the secondary FlashArray. |
secondary_api_token | (required) | API token for the secondary FlashArray. |
verify_ssl | false | Verify FlashArray TLS certificates. |
default_rpo_seconds | 900 | Default RPO target in seconds. |
snapshot_interval_seconds | 300 | Interval between async replication snapshots. |
[DEFAULT]
| Option | Default | Effect |
|---|---|---|
host | localhost | Hostname of this node. Used to identify the local site. |
use_mock_storage | false | Use mock storage backend. Development only. |
use_mock_cinder | false | Use mock Cinder calls. Development only. |
rpc_conn_pool_size | 30 | RabbitMQ RPC connection pool size. |
Authentication
Every versioned API call requires an X-Auth-Token obtained from Keystone on port 5000. The protector-api itself has no authentication endpoint.
TOKEN=$(openstack token issue -f value -c id)
TENANT_ID=$(openstack token issue -f value -c project_id)
API=http://protector-primary.example.com:8788
Pass X-Auth-Token: $TOKEN on every request. To opt into microversion 1.1, also pass OpenStack-API-Version: protector 1.1.
Version discovery
The root endpoint GET / requires no authentication and returns the supported microversion range:
curl -s $API/
Response:
{
"versions": [
{
"id": "v1.0",
"status": "CURRENT",
"version": "1.1",
"min_version": "1.0",
"links": [
{ "rel": "self", "href": "http://protector-primary.example.com:8788/v1" }
]
}
]
}
Sites
Sites represent registered OpenStack deployments. Registering both the primary and secondary sites is the first step before any DR operation.
List sites
curl -s -H "X-Auth-Token: $TOKEN" \
$API/v1/$TENANT_ID/sites
Create a site
curl -s -X POST -H "X-Auth-Token: $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"site": {
"name": "primary-site",
"site_type": "PRIMARY",
"auth_url": "https://keystone-primary.example.com:5000/v3",
"service_username": "protector",
"service_password": "<password>",
"service_project_name": "service",
"service_user_domain_name": "Default",
"service_project_domain_name": "Default"
}
}' \
$API/v1/$TENANT_ID/sites
The site_type field must be one of PRIMARY or SECONDARY. The auth_url, site_type, and name fields are required. Service credentials are validated against the remote Keystone at save time.
Get local site
curl -s -H "X-Auth-Token: $TOKEN" \
$API/v1/$TENANT_ID/sites/local
Protection Groups
A Protection Group is the core DR unit — a named collection of VMs replicated and failed over together.
Create a Protection Group
Required fields: name, primary_site_id, secondary_site_id, replication_type. volume_type_id is required for async replication.
curl -s -X POST -H "X-Auth-Token: $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"protection_group": {
"name": "prod-pg",
"primary_site_id": "<primary-site-uuid>",
"secondary_site_id": "<secondary-site-uuid>",
"replication_type": "async",
"volume_type_id": "<replicated-volume-type-uuid>"
}
}' \
$API/v1/$TENANT_ID/protection-groups
Returns HTTP 202 with status: creating. Poll GET /v1/{tenant_id}/protection-groups/{id} until status reaches active.
Valid replication_type values: async (periodic FlashArray snapshot replication) and active_dr (Pure Storage ActiveDR pod-based continuous replication, RPO ≈ 0, requires Purity//FA 6.0+).
Members
VMs are added to a Protection Group one at a time. Every VM must boot from a Cinder volume (instances using ephemeral boot disks are rejected with InstanceNotCinderBoot, HTTP 422). All attached volumes must use a Cinder volume type with replication_enabled='<is> True' set as an extra-spec; volumes that do not meet this requirement are rejected with VolumeTypeNotReplicated (HTTP 422).
Validate before adding (recommended)
curl -s -X POST -H "X-Auth-Token: $TOKEN" \
-H "Content-Type: application/json" \
-d '{"member": {"instance_id": "<instance-uuid>"}}' \
$API/v1/$TENANT_ID/protection-groups/<pg-uuid>/members/validate
Returns HTTP 200 with a validation.errors array whether or not the instance is eligible — inspect validation.valid to decide whether to proceed.
Add a member
curl -s -X POST -H "X-Auth-Token: $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"member": {
"instance_id": "<instance-uuid>",
"boot_order": 1
}
}' \
$API/v1/$TENANT_ID/protection-groups/<pg-uuid>/members
Returns HTTP 201. boot_order is a non-negative integer; VMs with the same value are processed in parallel during failover; lower-numbered tiers complete before higher-numbered ones begin.
DR Operations (failover, failback, test failover)
All DR actions are submitted as a POST to the single /action endpoint with an action key in the request body. The endpoint returns HTTP 202 immediately with an operation_id; poll GET /v1/{tenant_id}/operations/{operation_id} to track progress.
Planned failover
curl -s -X POST -H "X-Auth-Token: $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"failover": {
"failover_type": "planned",
"network_mapping": {
"<primary-net-uuid>": "<secondary-net-uuid>"
}
}
}' \
$API/v1/$TENANT_ID/protection-groups/<pg-uuid>/action
Unplanned failover — set "failover_type": "unplanned". Skips VM shutdown and accepts data loss up to the RPO.
Failback
curl -s -X POST -H "X-Auth-Token: $TOKEN" \
-H "Content-Type: application/json" \
-d '{"failback": {}}' \
$API/v1/$TENANT_ID/protection-groups/<pg-uuid>/action
Test failover — non-disruptive. Either network_mapping or auto_network is required.
curl -s -X POST -H "X-Auth-Token: $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"test_failover": {
"auto_network": true,
"vm_creation_params": {
"availability_zone": "nova-az-test"
}
}
}' \
$API/v1/$TENANT_ID/protection-groups/<pg-uuid>/action
Test cleanup
curl -s -X POST -H "X-Auth-Token: $TOKEN" \
-H "Content-Type: application/json" \
-d '{"test_cleanup": {}}' \
$API/v1/$TENANT_ID/protection-groups/<pg-uuid>/action
Poll operation status
curl -s -H "X-Auth-Token: $TOKEN" \
$API/v1/$TENANT_ID/operations/<operation-uuid>
Replication Health
curl -s -H "X-Auth-Token: $TOKEN" \
$API/v1/$TENANT_ID/protection-groups/<pg-uuid>/replication-health
The response nests fields under replication_health.health_report.* alongside sibling keys protection_group_id, protection_group_name, failover_validation, and storage_config. Use replication_health.failover_validation.can_failover (boolean) as the programmatic readiness check.
Tenant Mappings
Tenant mappings translate project UUIDs between sites, since each site's Keystone assigns different UUIDs to the same tenant. Creating a mapping on the local site automatically attempts to create the reverse mapping on the remote site.
curl -s -X POST -H "X-Auth-Token: $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"tenant_mapping": {
"local_tenant_id": "<local-project-uuid>",
"remote_site_id": "<remote-site-uuid>",
"remote_tenant_id": "<remote-project-uuid>"
}
}' \
$API/v1/$TENANT_ID/tenant-mappings
If the remote site is unreachable, the mapping is created locally with status: pending_sync and a warning field in the response explaining what to do next.
Resource Mappings
Resource mappings define how networks, flavors, and floating-IP pools on the primary site correspond to resources on the secondary site. Auto-mapping tries to match by name.
Auto-map
curl -s -X POST -H "X-Auth-Token: $TOKEN" \
$API/v1/$TENANT_ID/protection-groups/<pg-uuid>/mappings/auto
Manual mapping
curl -s -X POST -H "X-Auth-Token: $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"mapping": {
"resource_type": "network",
"primary_resource_id": "<primary-net-uuid>",
"secondary_resource_id": "<secondary-net-uuid>"
}
}' \
$API/v1/$TENANT_ID/protection-groups/<pg-uuid>/mappings
Supported resource_type values: network, flavor, floating_ip_pool.
1 — Version discovery (unauthenticated)
curl -s http://protector-primary.example.com:8788/
Expected response (HTTP 200):
{
"versions": [
{
"id": "v1.0",
"status": "CURRENT",
"version": "1.1",
"min_version": "1.0",
"links": [
{
"rel": "self",
"href": "http://protector-primary.example.com:8788/v1"
}
]
}
]
}
2 — Register the primary site
TOKEN=$(openstack token issue -f value -c id)
TENANT_ID=$(openstack token issue -f value -c project_id)
API=http://protector-primary.example.com:8788
curl -s -X POST \
-H "X-Auth-Token: $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"site": {
"name": "site-primary",
"site_type": "PRIMARY",
"auth_url": "https://keystone-primary.example.com:5000/v3",
"service_username": "protector",
"service_password": "s3cur3pass",
"service_project_name": "service",
"service_user_domain_name": "Default",
"service_project_domain_name": "Default"
}
}' \
$API/v1/$TENANT_ID/sites
Expected response (HTTP 201):
{
"site": {
"id": "a1b2c3d4-...",
"name": "site-primary",
"site_type": "PRIMARY",
"auth_url": "https://keystone-primary.example.com:5000/v3",
"status": "ACTIVE"
}
}
3 — Create a Protection Group with async replication
curl -s -X POST \
-H "X-Auth-Token: $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"protection_group": {
"name": "prod-pg",
"primary_site_id": "a1b2c3d4-...",
"secondary_site_id": "e5f6a7b8-...",
"replication_type": "async",
"volume_type_id": "<replicated-vol-type-uuid>"
}
}' \
$API/v1/$TENANT_ID/protection-groups
Expected response (HTTP 202):
{
"protection_group": {
"id": "pg-uuid-...",
"name": "prod-pg",
"status": "creating",
"message": "Protection group creation initiated. Use GET to check status."
}
}
Poll until status is active:
curl -s -H "X-Auth-Token: $TOKEN" \
$API/v1/$TENANT_ID/protection-groups/pg-uuid-...
4 — Validate then add a VM to a Protection Group
# Step 1: validate
curl -s -X POST \
-H "X-Auth-Token: $TOKEN" \
-H "Content-Type: application/json" \
-d '{"member": {"instance_id": "<vm-uuid>"}}' \
$API/v1/$TENANT_ID/protection-groups/pg-uuid-.../members/validate
Expected response (HTTP 200):
{
"validation": {
"valid": true,
"instance_id": "<vm-uuid>",
"instance_name": "web-server-01",
"protection_group_id": "pg-uuid-...",
"errors": [],
"warnings": [],
"details": {
"volume_count": 2,
"boot_volume_id": "<vol-uuid>"
}
}
}
# Step 2: add
curl -s -X POST \
-H "X-Auth-Token: $TOKEN" \
-H "Content-Type: application/json" \
-d '{"member": {"instance_id": "<vm-uuid>", "boot_order": 1}}' \
$API/v1/$TENANT_ID/protection-groups/pg-uuid-.../members
Expected response (HTTP 201):
{
"member": {
"id": "member-uuid-...",
"instance_id": "<vm-uuid>",
"instance_name": "web-server-01",
"boot_order": 1,
"status": "protecting",
"volumes_added": 2
}
}
5 — Execute a planned failover and poll to completion
RESP=$(curl -s -X POST \
-H "X-Auth-Token: $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"failover": {
"failover_type": "planned",
"network_mapping": {
"<primary-net-uuid>": "<secondary-net-uuid>"
},
"vm_creation_params": {
"availability_zone": "nova-az-2"
}
}
}' \
$API/v1/$TENANT_ID/protection-groups/pg-uuid-.../action)
OP_ID=$(echo $RESP | python3 -c "import sys,json; print(json.load(sys.stdin)['operation_id'])")
echo "Operation: $OP_ID"
The initial response (HTTP 202) body includes status: "pending":
{
"message": "failover operation initiated",
"status": "pending",
"protection_group_id": "pg-uuid-...",
"operation_id": "op-uuid-...",
"operation": { ... }
}
Poll:
while true; do
STATUS=$(curl -s -H "X-Auth-Token: $TOKEN" \
$API/v1/$TENANT_ID/operations/$OP_ID \
| python3 -c "import sys,json; d=json.load(sys.stdin)['operation']; print(d['status'], d['progress'])")
echo $STATUS
[[ $STATUS == completed* ]] || [[ $STATUS == failed* ]] && break
sleep 10
done
6 — Check replication health
curl -s -H "X-Auth-Token: $TOKEN" \
$API/v1/$TENANT_ID/protection-groups/pg-uuid-.../replication-health
Expected response (HTTP 200) — key fields:
{
"replication_health": {
"protection_group_id": "pg-uuid-...",
"protection_group_name": "prod-pg",
"failover_validation": {
"can_failover": true,
"warnings": [],
"blockers": [],
"message": "Protection group is ready for failover"
},
"health_report": {
"sync_state": "in_sync",
"lag_seconds": 12,
"rpo_compliant": true,
"recovery_points_available": 4
},
"storage_config": { ... }
}
}
Use replication_health.failover_validation.can_failover as the programmatic readiness gate in automation.
7 — Create a tenant mapping
curl -s -X POST \
-H "X-Auth-Token: $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"tenant_mapping": {
"local_tenant_id": "<local-project-uuid>",
"remote_site_id": "<secondary-site-uuid>",
"remote_tenant_id": "<remote-project-uuid>",
"description": "Production tenant DR mapping"
}
}' \
$API/v1/$TENANT_ID/tenant-mappings
Expected response (HTTP 201):
{
"tenant_mapping": {
"id": "mapping-uuid-...",
"local_tenant_id": "<local-project-uuid>",
"remote_site_id": "<secondary-site-uuid>",
"remote_tenant_id": "<remote-project-uuid>",
"status": "active",
"sync_result": { "success": true }
}
}
If status is pending_sync, the remote site was unreachable. Fix connectivity and re-run, or create the reverse mapping manually on the secondary site.
8 — List all operations for a Protection Group
curl -s -H "X-Auth-Token: $TOKEN" \
"$API/v1/$TENANT_ID/operations?pg_id=pg-uuid-..."
Expected response (HTTP 200):
{
"operations": [
{
"id": "op-uuid-...",
"protection_group_id": "pg-uuid-...",
"operation_type": "failover",
"status": "completed",
"progress": 100,
"started_at": "2024-03-15T10:00:00",
"completed_at": "2024-03-15T10:04:32",
"error_message": null
}
]
}
9 — Auto-map resources then review decisions
curl -s -X POST \
-H "X-Auth-Token: $TOKEN" \
$API/v1/$TENANT_ID/protection-groups/pg-uuid-.../mappings/auto
Expected response (HTTP 200):
{
"auto_mappings": {
"networks": 2,
"flavors": 1,
"floating_ip_pools": 0
},
"decisions": [
{
"resource_type": "network",
"primary_resource_name": "tenant-net",
"outcome": "adopted",
"reason": "Matched by name on secondary site"
},
{
"resource_type": "floating_ip_pool",
"primary_resource_name": "public",
"outcome": "unresolved",
"reason": "No network named 'public' found on secondary site"
}
],
"message": "Auto-mapped 3 resources; 1 left unmapped -- see decisions for why"
}
For any unmapped resource, create a manual mapping with POST /v1/{tenant_id}/protection-groups/{pg_id}/mappings.
HTTP 404 on every versioned endpoint
Symptom: All calls to $API/v1/$TENANT_ID/... return 404.
Likely cause: The /v1/ prefix is missing, or the URL is missing the tenant ID segment.
Fix: Ensure your URL follows the pattern http://<host>:8788/v1/<tenant-id>/<resource>. A URL such as http://<host>:8788/<tenant-id>/sites (missing /v1/) always returns 404.
HTTP 400 — Bad Request on microversion header
Symptom: Requests with OpenStack-API-Version: protector 2.0 (or a typo such as OpenStack-API-Version: protector) return HTTP 400.
Likely cause: The microversion value is outside the supported range 1.0–1.1, or the header value is malformed.
Fix: Use OpenStack-API-Version: protector 1.1 or OpenStack-API-Version: protector 1.0, or omit the header entirely (defaults to 1.0).
HTTP 401 — Unauthorized
Symptom: All authenticated calls return 401.
Likely cause: The X-Auth-Token is expired or missing. Tokens from Keystone are time-limited.
Fix: Re-issue a token with openstack token issue and export the new value. Confirm you are pointing at the correct Keystone endpoint (port 5000, not port 8788).
HTTP 403 — Forbidden on DR operations
Symptom: POST .../action or member operations return 403 with a message referencing dr_operator.
Likely cause: Your user lacks the dr_operator role on the project, or the role does not exist on one of the sites.
Fix:
- Confirm the role exists:
openstack role list | grep dr_operator. Create it if missing:openstack role create dr_operator. - Grant it:
openstack role add --user <user> --project <project> dr_operator. - Repeat on the secondary site — both sites must have the role before any cross-site operation.
Site-level operations (site failover, site-operations endpoints) additionally require dr_site_admin.
HTTP 409 — ProtectionGroupFailedOver
Symptom: Member add, member remove, or mapping operations on a PG return HTTP 409 with error type ProtectionGroupFailedOver.
Likely cause: The Protection Group has been failed over and its workloads are now on the secondary site. Mutating operations are blocked to prevent inconsistency.
Fix: Either execute failback to return the PG to the primary site, or — if the PG state is inconsistent after an interrupted operation — use PUT /v1/{tenant_id}/protection-groups/{id} with {"protection_group": {"reset_to_primary": true, "status": "active"}} to reset state (requires understanding why it was left in that state).
HTTP 422 — InstanceNotCinderBoot when adding a member
Symptom: POST .../members returns 422 with error type InstanceNotCinderBoot.
Likely cause: The Nova instance uses an ephemeral boot disk rather than a Cinder-backed boot volume.
Fix: Rebuild or re-create the instance booting from a Cinder volume. All VMs in a Protection Group must boot from Cinder. Use the validate endpoint first: POST .../members/validate returns detailed eligibility information without side effects.
HTTP 422 — VolumeTypeNotReplicated when adding a member
Symptom: POST .../members returns 422 with error type VolumeTypeNotReplicated.
Likely cause: One or more Cinder volumes attached to the instance use a volume type that does not have replication_enabled='<is> True' set as an extra-spec.
Fix:
- Identify the volume type:
openstack volume type list --long. - Add the extra-spec:
openstack volume type set --property replication_enabled='<is> True' <volume-type>. - For async replication also set:
openstack volume type set --property replication_type='<in> async' <volume-type>. - Migrate existing volumes to the updated type, or re-attach volumes that use the corrected type.
HTTP 503 — SiteUnreachable or RemoteSiteUnreachable
Symptom: Operations that involve the secondary site return 503.
Likely cause: Network connectivity on port 8788 between the two sites is broken, or the protector-api service on the remote site is down.
Fix:
- Verify TCP 8788 is open bidirectionally between sites:
nc -zv <remote-protector-host> 8788. - Check the remote service is running. In a Juju deployment:
juju status trilio-site-recovery. - Review logs at
/var/log/protector/on the remote site for startup errors. - If TLS is in use, ensure
cafilein[service_credentials]points to a valid CA bundle, or setinsecure = truetemporarily to isolate certificate issues (development environments only).
Tenant mapping creates locally but remote sync fails (pending_sync)
Symptom: POST .../tenant-mappings returns 201 but the response contains status: pending_sync and a warning field.
Likely cause: The remote site's protector-api on port 8788 was unreachable at the time of the create. Cross-site metadata sync requires bidirectional connectivity on port 8788.
Fix:
- Restore connectivity to port 8788 on the remote site.
- Re-run
POST .../tenant-mappingswith the same parameters — it will detect the existing local half and attempt the remote sync again. - Alternatively, log into the secondary site and create the reverse mapping there manually.
openstack dr commands not found after installing the server package
Symptom: openstack dr --help shows no DR commands after pip install -e . from the repo root.
Likely cause: The server package (openstack-protector) does not include the CLI. The CLI is a separate package.
Fix: Install the CLI plugin separately:
pip install -e ./client
The package name is triliositerecovery. Do not install via the openstackclient snap — it is incompatible with this plugin.
Protection Group stuck in creating status
Symptom: GET /v1/{tenant_id}/protection-groups/{id} keeps returning status: creating and never transitions to active.
Likely cause: The protector-engine service is not running, cannot connect to RabbitMQ, or the storage driver is misconfigured.
Fix:
- In a Juju deployment, check unit status:
juju status trilio-site-recovery. - Verify RabbitMQ connectivity — the
[DEFAULT]transport_url(set via theamqprelation in Juju) must be reachable. - Confirm
storage_driver = purein[storage]and that[pure_storage]credentials are correct. The defaultmockdriver does not perform real replication. - Review engine logs at
/var/log/protector/for the specific failure. - If the PG is in an unrecoverable state, use
PUT /v1/{tenant_id}/protection-groups/{id}with{"protection_group": {"status": "error"}}to mark it, then delete and recreate.