API Reference
Complete REST API endpoint documentation
Complete REST API reference for Trilio Site Recovery for OpenStack (protector-api). The API runs on port 8788 on each site and accepts authenticated requests from the openstack dr CLI, the Horizon dashboard panel, or any HTTP client. All resource endpoints are versioned under /v1/{tenant_id}/. Authentication is via an X-Auth-Token header obtained from Keystone on port 5000. Version discovery at GET / requires no authentication.
The maximum supported microversion is 1.1; the minimum is 1.0. The OpenStack-API-Version: protector <version> header is optional — when omitted the service defaults to microversion 1.0 and processes the request normally. A malformed value or a version outside the 1.0–1.1 range returns HTTP 400 Bad Request.
Global request headers
| Header | Required | Description |
|---|---|---|
X-Auth-Token | Yes (all versioned endpoints) | Keystone token obtained from port 5000. Not required for GET /. |
OpenStack-API-Version | No | Format: protector <version>. Valid values: protector 1.0, protector 1.1. Omitting this header is equivalent to protector 1.0. |
Content-Type | Yes (POST/PUT) | Must be application/json. |
URL structure
All versioned resource endpoints share the prefix /v1/{tenant_id}/. URLs that omit the /v1/ segment return 404.
| Path segment | Description |
|---|---|
{tenant_id} | The caller's OpenStack project UUID, taken from the Keystone token scope. |
{id} / {pg_id} | UUID of the resource being addressed. Names are accepted where noted. |
Query parameters (common)
| Parameter | Endpoints | Description |
|---|---|---|
all_projects=true | Protection Groups list/show, action | Admin only. Returns resources across all tenants. |
status=<value> | Operations list, Site Operations list | Filter by status string. |
operation_type=<value> | Operations list | Filter by operation type. |
pg_id=<uuid> | Operations list | Filter operations for a specific Protection Group. |
Version discovery
GET / requires no authentication and returns HTTP 200 with supported API versions:
{
"versions": [
{
"id": "v1.0",
"status": "CURRENT",
"version": "1.1",
"min_version": "1.0",
"links": [
{
"rel": "self",
"href": "http://<host>:8788/v1/"
}
]
}
]
}
Field reference: id identifies the version family (v1.0); version is the maximum supported microversion (1.1); min_version is the minimum (1.0); status is CURRENT for the only shipped version.
Health probe
GET /healthz requires no authentication. Returns 200 OK when the service and its database connection are healthy:
{"status": "ok", "checks": {"database": "ok"}}
Returns 503 Service Unavailable when the database is unreachable:
{"status": "unhealthy", "checks": {"database": "unavailable (<ExceptionClassName>)"}}
Accepted responses by resource type
| Resource | Create | Show/List | Update | Delete | Action |
|---|---|---|---|---|---|
| Site | 201 Created | 200 OK | 200 OK | 204 No Content | 202 Accepted |
| Protection Group | 202 Accepted | 200 OK | 200 OK | 204 No Content (clean) / 200 OK (with leftovers) | 202 Accepted |
| Member | 201 Created | 200 OK | 200 OK | 202 Accepted | — |
| Tenant Mapping | 201 Created | 200 OK | — | 204 No Content | — |
| Resource Mapping | 201 Created | 200 OK | — | 204 No Content | — |
| Operation | — | 200 OK | — | — | — |
| Policy | 200 OK | 200 OK | — | — | — |
Action endpoint (POST /v1/{tenant_id}/protection-groups/{pg_id}/action)
All DR operations — failover, failback, test failover, test cleanup, and reconcile — are triggered through the single /action endpoint using a typed request body. A successful dispatch returns 202 Accepted:
{
"message": "failover operation initiated",
"status": "pending",
"protection_group_id": "<pg-uuid>",
"operation_id": "<operation-uuid>",
"operation": { ... }
}
The status field in the immediate 202 response is always "pending". Poll GET /v1/{tenant_id}/operations/{operation_id} to track progress.
Operation response
GET /v1/{tenant_id}/operations/{id} returns:
{
"operation": {
"id": "<uuid>",
"protection_group_id": "<uuid>",
"operation_type": "failover",
"failover_type": "planned",
"status": "running",
"progress": 45,
"source_site_id": "<uuid>",
"target_site_id": "<uuid>",
"execution_site_id": "<uuid>",
"started_at": "2024-03-15T10:00:00",
"completed_at": null,
"error_message": null,
"instances_created": [],
"volumes_managed": [],
"networks_configured": [],
"floating_ips_allocated": [],
"security_groups_applied": [],
"vm_details": [],
"warnings": [],
"errors": [],
"current_phase": "promoting_volumes",
"steps_completed": ["validate", "snapshot"],
"steps_failed": []
}
}
progress is an integer from 0 to 100. status transitions through pending → running → completed or failed.
Replication health response
GET /v1/{tenant_id}/protection-groups/{pg_id}/replication-health returns a nested report. Key fields are nested under replication_health.health_report.* alongside sibling keys at the replication_health level:
{
"replication_health": {
"protection_group_id": "<uuid>",
"protection_group_name": "my-pg",
"failover_validation": {
"can_failover": true,
"warnings": [],
"blockers": [],
"message": "Protection group is ready for failover"
},
"storage_config": { ... },
"health_report": {
"sync_state": "in_sync",
"lag_seconds": 12,
"rpo_compliant": true,
"recovery_points_available": 8
}
}
}
Use replication_health.failover_validation.can_failover (boolean) as the programmatic readiness check before triggering failover.
Error envelope
All errors from protector-api — except microversion rejections from the version middleware — share this JSON shape:
{
"error": {
"code": 404,
"message": "Protection group <id> not found",
"type": "ProtectionGroupNotFound"
}
}
The type field is usually the exception class name. Two values are synthesised by the fault middleware itself and have no corresponding exception class:
ResourceConflict— a database unique-constraint violation, most often caused by reusing the name of a soft-deleted resource.ServiceUnavailable— the service has lost its connection to MariaDB/MySQL; retryable and unrelated to cross-site reachability.
Microversion rejections from the version middleware carry the same {"error": {"code": 400, "message": ...}} envelope but no type field.
HTTP status codes
| Code | Meaning | Common causes |
|---|---|---|
200 OK | Success (GET, PUT, some POST) | — |
201 Created | Resource created | Site, member, tenant mapping, resource mapping |
202 Accepted | Async operation initiated | Protection group create; action endpoint; member delete |
204 No Content | Deleted successfully | Site delete, protection group delete (clean), resource mapping delete, tenant mapping delete |
400 Bad Request | Invalid input or bad microversion | Missing required fields; invalid replication_type; unsupported OpenStack-API-Version value; InvalidAction; InvalidInput; InvalidParameters; ValidationError; ReplicationTypeMismatch; InvalidVolumeType (volume type mismatch, not missing replication support); InvalidExecutionSite |
401 Unauthorized | Missing or invalid token | No X-Auth-Token; expired Keystone token; Unauthorized; SiteAuthenticationFailed; TrustAuthFailed |
403 Forbidden | Action not permitted in current state | ProtectionGroupFailedOver; ProtectionGroupNotOnPrimarySite; Forbidden |
404 Not Found | Resource does not exist | ProtectionGroupNotFound; SiteNotFound; MemberNotFound; OperationNotFound; PolicyNotFound; ReplicationPolicyNotFound; ConsistencyGroupNotFound; VolumeNotFound; InstanceNotFound; TenantMappingNotFound; MappingNotFound; TrustNotFoundForTenant; MetadataNotFound; ReplicaNotFound; SiteOperationNotFound; RecoveryPointNotFound |
409 Conflict | State conflict or duplicate name | InvalidState (protection group in a state that does not allow the operation — returned as 409 on every endpoint including /action); InstanceAlreadyProtected; VolumeAlreadyInCG; VolumeAttachedElsewhere; VolumeInUse; SiteAlreadyExists; TenantMappingAlreadyExists; ResourceConflict (synthesised, DB unique-constraint violation) |
422 Unprocessable Entity | DR eligibility precondition failed | InstanceNotCinderBoot (VM does not boot from a Cinder volume); VolumeTypeNotReplicated (volume type lacks replication_enabled='<is> True'); FailoverNotReady; PreflightValidationFailed; PlannedFailoverValidationFailed; UnplannedFailoverValidationFailed; NoVolumesFound; NoBootableVolumeFound; VolumeBackendMismatch; ConsistencyGroupCreateFailed; NoVolumesInConsistencyGroup; ReplicationNotHealthy; RPOViolation; WriteOrderConsistencyFailed; DataConsistencyFailed; TenantValidationFailed; NoProtectionGroupsForSite; SiteFailoverNotReady |
503 Service Unavailable | Remote site or database unreachable | SiteUnreachable; SiteNotHealthy; RemoteSiteUnreachable; RemoteAPICallFailed; ServiceUnavailable (synthesised, local DB connectivity loss — retryable) |
Important:
InvalidStateis409on every endpoint, including the/actionendpoint. There is no per-endpoint status override anywhere in this service. The fault middleware returns each exception's declaredcodeunconditionally.
Notable error details
InstanceNotCinderBoot (422) — Raised at member-add time when the Nova instance uses an ephemeral boot disk instead of a Cinder volume. All VMs in a Protection Group must boot from Cinder.
VolumeTypeNotReplicated (422) — Raised at member-add time when an attached Cinder volume's type does not have replication_enabled='<is> True' as an extra-spec. When detected in-process this is HTTP 422; when the check crosses an RPC boundary it may surface as HTTP 500 before the exception is re-raised.
InvalidVolumeType (400) — Raised when a volume's type mismatches the Protection Group's configured type (not when replication support is absent).
ProtectionGroupFailedOver (403) — Raised when you attempt to modify a Protection Group that is currently failed over to the DR site. Run failback before making changes, or use the reset-to-primary / reset-to-secondary update flags.
ResourceConflict (409) — No exception class; synthesised by the fault middleware for any DB unique-constraint violation. The most common cause is reusing the name of a soft-deleted resource.
ServiceUnavailable (503) — No exception class; synthesised when the service loses its MariaDB/MySQL connection. This error is local and retryable; it is distinct from SiteUnreachable (which means the remote peer is unreachable).
Roles and trust errors — The service never creates Keystone roles; it looks them up. If dr_operator or dr_site_admin do not exist on both sites, trust creation fails with Role <name> does not exist on site <site>. Pre-create both roles before any cross-site operation.
Obtain a Keystone token
All versioned API calls require a token from Keystone on port 5000. The protector-api has no authentication endpoint of its own.
export OS_TOKEN=$(openstack token issue -f value -c id)
export TENANT_ID=$(openstack project show -f value -c id myproject)
export PROTECTOR_URL=http://protector-primary.example.com:8788
Discover supported API versions
No authentication required.
curl -s ${PROTECTOR_URL}/
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/"
}
]
}
]
}
Check service health
No authentication required. Use this as a liveness probe or before making cross-site calls.
curl -s ${PROTECTOR_URL}/healthz
Response (healthy):
{"status": "ok", "checks": {"database": "ok"}}
Register a site
curl -s -X POST ${PROTECTOR_URL}/v1/${TENANT_ID}/sites \
-H "X-Auth-Token: ${OS_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"site": {
"name": "site-secondary",
"site_type": "SECONDARY",
"auth_url": "http://keystone-dr.example.com:5000/v3",
"region_name": "RegionDR",
"service_username": "protector",
"service_password": "s3cr3t",
"service_project_name": "service",
"service_user_domain_name": "Default",
"service_project_domain_name": "Default"
}
}'
Response (201 Created):
{
"site": {
"id": "a1b2c3d4-...",
"name": "site-secondary",
"site_type": "SECONDARY",
"auth_url": "http://keystone-dr.example.com:5000/v3",
"region_name": "RegionDR",
"status": "ACTIVE"
}
}
The service validates the provided service_password against the remote Keystone before saving. If credentials are incomplete or authentication fails, you receive 400 Bad Request.
List sites
curl -s ${PROTECTOR_URL}/v1/${TENANT_ID}/sites \
-H "X-Auth-Token: ${OS_TOKEN}"
Create a Protection Group
A Protection Group requires both sites to be registered and a Cinder volume type with replication_enabled='<is> True'.
curl -s -X POST ${PROTECTOR_URL}/v1/${TENANT_ID}/protection-groups \
-H "X-Auth-Token: ${OS_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"protection_group": {
"name": "web-tier-pg",
"primary_site_id": "<primary-site-uuid>",
"secondary_site_id": "<secondary-site-uuid>",
"replication_type": "async",
"volume_type_id": "<replicated-volume-type-uuid>"
}
}'
Response (202 Accepted):
{
"protection_group": {
"id": "pg-uuid-...",
"name": "web-tier-pg",
"status": "creating",
"message": "Protection group creation initiated. Use GET to check status."
}
}
Creation is asynchronous. Poll GET /v1/${TENANT_ID}/protection-groups/<id> until status is active.
Valid replication_type values: async (periodic Pure Storage Protection Group snapshots) and active_dr (Pure Storage ActiveDR pod-based continuous replication, near-zero RPO, requires Purity//FA 6.0+). volume_type_id is required for async.
Add a VM to a Protection Group
The instance must boot from a Cinder volume; all attached volumes must use a volume type with replication_enabled='<is> True'.
curl -s -X POST \
${PROTECTOR_URL}/v1/${TENANT_ID}/protection-groups/<pg-id>/members \
-H "X-Auth-Token: ${OS_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"member": {
"instance_id": "<nova-instance-uuid>",
"boot_order": 1
}
}'
Response (201 Created):
{
"member": {
"id": "member-uuid",
"protection_group_id": "pg-uuid",
"instance_id": "<nova-instance-uuid>",
"instance_name": "web-01",
"boot_order": 1,
"status": "protected",
"volumes_added": 2
}
}
If the instance uses ephemeral storage, the service returns 422 with type: InstanceNotCinderBoot. If any attached volume type lacks replication support, it returns 422 with type: VolumeTypeNotReplicated.
Pre-validate a member without adding it
Use the validate endpoint to run all eligibility checks without side effects. This always returns 200 OK; check the valid field in the response body.
curl -s -X POST \
${PROTECTOR_URL}/v1/${TENANT_ID}/protection-groups/<pg-id>/members/validate \
-H "X-Auth-Token: ${OS_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"member": {"instance_id": "<nova-instance-uuid>"}}'
Response (200 OK, validation failed):
{
"validation": {
"valid": false,
"instance_id": "<nova-instance-uuid>",
"instance_name": "web-02",
"protection_group_id": "pg-uuid",
"errors": [
{
"code": "InstanceNotCinderBoot",
"message": "Instance web-02 is not a Cinder boot instance."
}
],
"warnings": [],
"details": {}
}
}
Trigger a planned failover
All DR actions use the single /action endpoint with a typed body.
curl -s -X POST \
${PROTECTOR_URL}/v1/${TENANT_ID}/protection-groups/<pg-id>/action \
-H "X-Auth-Token: ${OS_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"failover": {
"failover_type": "planned",
"vm_creation_params": {
"availability_zone": "nova-az-dr"
},
"network_mapping": {
"net-primary-uuid": "net-secondary-uuid"
}
}
}'
Response (202 Accepted):
{
"message": "failover operation initiated",
"status": "pending",
"protection_group_id": "<pg-uuid>",
"operation_id": "<operation-uuid>",
"operation": { "id": "<operation-uuid>", "status": "pending", "progress": 0 }
}
For failover_type: planned, the engine gracefully shuts down VMs and takes a final snapshot before promoting volumes — zero data loss. For failover_type: unplanned, the engine skips shutdown and accepts data loss up to the RPO.
Trigger an unplanned failover
curl -s -X POST \
${PROTECTOR_URL}/v1/${TENANT_ID}/protection-groups/<pg-id>/action \
-H "X-Auth-Token: ${OS_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"failover": {
"failover_type": "unplanned",
"force": true
}
}'
Trigger a test failover (non-disruptive DR validation)
A test failover clones replicated volumes from the secondary site and boots isolated test VMs (prefixed with test-) on a separate network without touching production.
curl -s -X POST \
${PROTECTOR_URL}/v1/${TENANT_ID}/protection-groups/<pg-id>/action \
-H "X-Auth-Token: ${OS_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"test_failover": {
"network_mapping": {
"net-primary-uuid": "net-test-isolated-uuid"
},
"vm_creation_params": {
"availability_zone": "nova-az-test"
},
"instance_name_prefix": "test-"
}
}'
Either network_mapping or auto_network is required. Omitting both returns 400 Bad Request.
Clean up test failover resources
curl -s -X POST \
${PROTECTOR_URL}/v1/${TENANT_ID}/protection-groups/<pg-id>/action \
-H "X-Auth-Token: ${OS_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"test_cleanup": {
"operation_id": "<test-failover-operation-uuid>"
}
}'
Trigger a failback
curl -s -X POST \
${PROTECTOR_URL}/v1/${TENANT_ID}/protection-groups/<pg-id>/action \
-H "X-Auth-Token: ${OS_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"failback": {
"failback_type": "planned"
}
}'
Poll operation status
curl -s \
${PROTECTOR_URL}/v1/${TENANT_ID}/operations/<operation-uuid> \
-H "X-Auth-Token: ${OS_TOKEN}"
Response (200 OK):
{
"operation": {
"id": "<operation-uuid>",
"operation_type": "failover",
"status": "completed",
"progress": 100,
"started_at": "2024-03-15T10:00:00",
"completed_at": "2024-03-15T10:04:22",
"error_message": null,
"steps_completed": ["validate", "snapshot", "promote", "create_vms"],
"steps_failed": []
}
}
List all operations (with filters)
curl -s \
"${PROTECTOR_URL}/v1/${TENANT_ID}/operations?status=running&operation_type=failover" \
-H "X-Auth-Token: ${OS_TOKEN}"
There is no DELETE route for operations — only GET /v1/{tenant_id}/operations and GET /v1/{tenant_id}/operations/{id} are registered.
Check replication health
curl -s \
${PROTECTOR_URL}/v1/${TENANT_ID}/protection-groups/<pg-id>/replication-health \
-H "X-Auth-Token: ${OS_TOKEN}"
Check replication_health.failover_validation.can_failover to determine programmatic failover readiness.
List recovery points
curl -s \
"${PROTECTOR_URL}/v1/${TENANT_ID}/protection-groups/<pg-id>/recovery-points?limit=10" \
-H "X-Auth-Token: ${OS_TOKEN}"
Create a tenant mapping
Tenant mappings are required because each site has an independent Keystone that assigns different UUIDs to the same project.
curl -s -X POST \
${PROTECTOR_URL}/v1/${TENANT_ID}/tenant-mappings \
-H "X-Auth-Token: ${OS_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"tenant_mapping": {
"local_tenant_id": "<primary-project-uuid>",
"remote_site_id": "<secondary-site-uuid>",
"remote_tenant_id": "<secondary-project-uuid>",
"description": "Production DR mapping"
}
}'
Response (201 Created): The response includes a warning field if the reverse mapping could not be synced to the remote site, indicating that only one half of the bidirectional mapping exists. Cross-site operations will not work until both halves are present.
Validate an existing tenant mapping
curl -s -X POST \
${PROTECTOR_URL}/v1/${TENANT_ID}/tenant-mappings/<mapping-id>/validate \
-H "X-Auth-Token: ${OS_TOKEN}" \
-H "Content-Type: application/json" \
-d '{}'
Create a resource mapping (network, flavor, floating-IP pool)
curl -s -X POST \
${PROTECTOR_URL}/v1/${TENANT_ID}/protection-groups/<pg-id>/mappings \
-H "X-Auth-Token: ${OS_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"mapping": {
"resource_type": "network",
"primary_resource_id": "<primary-network-uuid>",
"secondary_resource_id": "<secondary-network-uuid>"
}
}'
Supported resource_type values: network, flavor, floating_ip_pool.
Auto-map resources
Let the service attempt to match resources between sites by name.
curl -s -X POST \
${PROTECTOR_URL}/v1/${TENANT_ID}/protection-groups/<pg-id>/mappings/auto \
-H "X-Auth-Token: ${OS_TOKEN}" \
-H "Content-Type: application/json" \
-d '{}'
Response (200 OK):
{
"auto_mappings": { "networks": 2, "flavors": 1 },
"decisions": [
{ "resource_type": "network", "name": "internal", "outcome": "adopted" },
{ "resource_type": "network", "name": "external", "outcome": "not_found",
"reason": "No network named 'external' on secondary site" }
],
"message": "Auto-mapped 3 resources; 1 left unmapped -- see decisions for why"
}
Trigger a site-level action
Site-level failover requires the dr_site_admin role.
curl -s -X POST \
${PROTECTOR_URL}/v1/${TENANT_ID}/sites/<site-id>/action \
-H "X-Auth-Token: ${OS_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"failover": {"failover_type": "unplanned"}}'
The action body must use "failover" or "failback" as the top-level key. Unknown keys return 400 InvalidInput.
Retrieve a usage summary (admin only)
curl -s \
${PROTECTOR_URL}/v1/${TENANT_ID}/usage/summary \
-H "X-Auth-Token: ${OS_TOKEN}"
CLI equivalents
The openstack dr CLI plugin issues the same requests. Equivalent commands for the examples above:
# Register a site
openstack dr site create site-secondary \
--site-type SECONDARY \
--auth-url http://keystone-dr.example.com:5000/v3
# Create a Protection Group
openstack dr protection group create web-tier-pg \
--primary-site site-primary \
--secondary-site site-secondary \
--replication-type async \
--volume-type replicated-ceph
# Add a VM
openstack dr protection group member add web-tier-pg \
--instance <nova-instance-uuid> \
--boot-order 1
# Check replication health
openstack dr replication health <pg-id>
# Planned failover
openstack dr failover <pg-id> --failover-type planned --wait
# Non-disruptive test failover
openstack dr test failover <pg-id> \
--network-mapping net-primary-uuid=net-test-uuid
# Clean up test failover
openstack dr test failover cleanup <pg-id>
# Failback
openstack dr failback <pg-id>
# Poll an operation
openstack dr operation show <operation-id>
The CLI plugin is a separate package. Install it with:
pip install -e ./client
The openstackclient snap is incompatible with the triliositerecovery plugin; install python-openstackclient via pip instead.
Authentication and roles
All versioned API endpoints require a valid X-Auth-Token from Keystone on port 5000. The protector-api has no authentication endpoint of its own. Before any cross-site operation can succeed, the following Keystone roles must exist on both sites:
dr_operator— required for tenant DR operations. Must be granted alongsidememberon the tenant's project for the user, and on each DR-enabled project for theprotectorservice account.dr_site_admin— required for site-level operations (sites:create/update/delete/action, site-level failover, and site operations endpoints). Must be pre-created on both sites; if absent, trust creation fails with a message naming the missing role.
The service never creates Keystone roles — it only looks them up and grants them.
Microversioning
The OpenStack-API-Version header is optional. Omitting it is equivalent to sending protector 1.0; the request is processed normally. A version string outside the 1.0–1.1 range, or a malformed value, returns HTTP 400 with a structured error body (no type field).
Every response carries OpenStack-API-Version: protector 1.1 and OpenStack-API-Minimum-Version: protector 1.0 headers regardless of the requested version.
Bidirectional connectivity on port 8788
Port 8788 must be open bidirectionally between sites. One-way connectivity allows you to initiate DR operations but prevents cross-site metadata and tenant mapping synchronisation, leaving the two sites inconsistent. When the remote protector-api is unreachable, tenant mapping creation succeeds locally but sets status: pending_sync with a warning rather than failing hard.
Operations are long-running — always poll
Failover, failback, test failover, member removal, and Protection Group creation are all asynchronous. The 202 response body includes an operation_id; use GET /v1/{tenant_id}/operations/{operation_id} to track progress and detect completion or failure. The progress field runs from 0 to 100. There is no DELETE route for operations — only GET.
Action endpoint: single path, multiple actions
All DR actions (failover, failback, test failover, test cleanup, reconcile) are triggered through the single endpoint POST /v1/{tenant_id}/protection-groups/{pg_id}/action. The action is selected by the top-level key of the JSON body (failover, failback, test_failover, test_cleanup, reconcile). There are no separate /failover, /failback, or /test-failover sub-paths.
Protection Group member eligibility
Two constraints are enforced at member-add time and cannot be bypassed:
- Cinder boot volumes only. Instances that boot from ephemeral storage are rejected with HTTP 422 (
InstanceNotCinderBoot). Every protected VM must have a Cinder-backed root volume. - Replicated volume types only. Every Cinder volume attached to the instance must use a volume type with
replication_enabled='<is> True'as an extra-spec. Volumes on non-replicated types return HTTP 422 (VolumeTypeNotReplicated).
Use POST /v1/{tenant_id}/protection-groups/{pg_id}/members/validate to pre-check eligibility without side effects. The endpoint always returns HTTP 200 with a validation.valid boolean and a structured validation.errors array.
Storage driver
Activate the Pure Storage FlashArray driver by setting storage_driver = pure in the [storage] section of protector.conf. The default value is mock, which is suitable for development and testing only — do not use it in production. In Juju charm deployments, set this via:
juju config trilio-site-recovery storage-driver=pure
Do not edit /etc/protector/protector.conf directly in Juju deployments — the charm overwrites it on every config-changed event.
Protection Group delete behaviour
DELETE /v1/{tenant_id}/protection-groups/{id} returns 204 No Content on clean deletion. If you pass ?force=true and the engine cannot reach the peer site to clean up remote resources, the response is 200 with a leftovers array describing what was not removed, rather than 204, so you know the remote site still holds a copy.
Deleting a Protection Group that was already deleted returns 204 (idempotent). Deleting a site that was already deleted also returns 204.
Resource mappings and networking
Networks, subnets, security groups, and flavors are adopted from the target site by name during failover — they are never created by the DR service. A pre-flight check runs before planned failover shuts down any VMs; it reports all missing resources at once so you can fix them without retrying the failover one resource at a time. Use POST /v1/{tenant_id}/protection-groups/{pg_id}/mappings/auto to auto-discover mappings, or create them explicitly via POST /v1/{tenant_id}/protection-groups/{pg_id}/mappings.
InvalidState is HTTP 409 everywhere
InvalidState is declared with code = 409 in protector/common/exception.py. The fault middleware returns that code unconditionally on every route, including the /action endpoint. There is no per-endpoint override. If you see documentation or client code treating InvalidState as HTTP 400 on /action, that source is stale.
Consistency Group data is embedded
Consistency Group details are returned as part of the Protection Group API response. There is no separate /consistency-group sub-resource or endpoint.
CLI plugin installation
The openstack dr CLI plugin is a separate Python package (triliositerecovery) located in the client/ subdirectory. Installing the server package does not install the CLI. Install the CLI separately:
pip install -e ./client
The openstackclient snap is incompatible with the triliositerecovery plugin — install python-openstackclient via pip, not snap.
Juju charm deployments
In Juju charm deployments, protector.conf is rendered by the charm from relation data and juju config values. All configuration must be set via juju config trilio-site-recovery <hyphenated-key>=<value>. The charm creates the Keystone service account (protector) and the database automatically via relations — no manual pre-creation is needed.