API Reference
REST API and Python client bindings
The Trilio Site Recovery REST API is a versioned OpenStack-native HTTP API served by the protector-api service on port 8788. It is registered in the Keystone service catalogue under the disaster-recovery service type (alias protector) and follows standard OpenStack conventions: Keystone token authentication, project-scoped requests, and JSON request/response bodies.
All resource URLs are prefixed with /v1/{tenant_id} where {tenant_id} is the Keystone project UUID of the calling user. Access control is enforced by oslo.policy; the roles dr_operator and dr_site_admin gate DR-specific operations, while member grants the Nova/Cinder access the service's per-tenant Keystone trusts require.
The Python client (from triliositerecovery.v1.client import Client) and the openstack dr CLI plugin (from the python3-triliositerecovery package) are both thin wrappers over this API. This page documents all three interfaces together so you can choose the right level of abstraction for each workflow.
Common request conventions
| Element | Value |
|---|---|
| Base URL | http(s)://<host>:8788/v1/{tenant_id} |
| Authentication | X-Auth-Token: <keystone-token> header |
| Content-Type | application/json for all POST/PUT bodies |
{tenant_id} | Keystone project UUID of the calling user |
{id} / {pg_id} | UUID or name of the target resource |
Query parameters common across collection endpoints:
| Parameter | Type | Endpoints | Description |
|---|---|---|---|
all_projects | boolean | protection groups, operations | Admin only. List resources across all projects. |
status | string | protection groups, operations, tenant mappings, site operations | Filter by resource status. |
name | string | protection groups | Filter by exact name. |
limit | integer | recovery points, site operations | Maximum results to return. |
all_tenants | boolean | tenant mappings | Admin only. List all tenant mappings. |
remote_site_id | UUID | tenant mappings | Filter by remote site. |
operation_type | string | operations | Filter by operation type. |
protection_group_id / pg_id | UUID | operations | Filter by protection group. |
force | boolean | site delete, PG delete | Skip referential-integrity guard. |
dry_run | boolean | PG sync, floating IP prepare | Report changes without applying them. |
probe | boolean | replication health | Also ask the target site whether the group's networks, security groups, and flavors resolve there, as the project it recovers into. Slower; talks to the peer. |
All successful responses return JSON. The top-level key mirrors the resource name or collection:
| Endpoint pattern | Success status | Top-level key |
|---|---|---|
GET …/sites | 200 | sites (array) |
POST …/sites | 201 | site |
GET …/sites/{id} | 200 | site |
PUT …/sites/{id} | 200 | site |
DELETE …/sites/{id} | 204 | (no body) |
GET …/protection-groups | 200 | protection_groups (array) |
POST …/protection-groups | 202 | protection_group |
GET …/protection-groups/{id} | 200 | protection_group |
PUT …/protection-groups/{id} | 200 | protection_group |
DELETE …/protection-groups/{id} | 204 or 200 | (204 = clean delete; 200 with leftovers array when remote cleanup failed) |
POST …/protection-groups/{id}/action | 202 | operation + operation_id; for reconcile: 200 with reconcile key; for failback_plan: 200 with failback_plan key |
POST …/sites/{id}/action | 202 | site_operation |
GET …/operations | 200 | operations (array) |
GET …/operations/{id} | 200 | operation |
GET …/tenant-mappings | 200 | tenant_mappings (array) |
POST …/tenant-mappings | 201 | tenant_mapping |
DELETE …/tenant-mappings/{id} | 204 | (no body) |
GET …/replication-audit | 200 | replication_audit |
GET …/usage/summary | 200 | usage_summary |
GET …/usage/report | 200 | usage_report (JSON) or PDF bytes |
Operation object fields
The operation object returned by action endpoints and GET …/operations/{id} includes:
| Field | Type | Notes |
|---|---|---|
id | UUID | Operation identifier |
protection_group_id | UUID | |
operation_type | string | FAILOVER, FAILBACK, TEST_FAILOVER, TEST_CLEANUP, SYNC_VOLUMES, VALIDATE_REPLICATION, RECONCILE |
failover_type | string | planned or unplanned |
status | string | pending, running, completed, failed |
progress | integer | 0–100 |
source_site | string | Source site name (falls back to its id) |
target_site | string | Target site name (falls back to its id) |
source_site_id | UUID | Emitted alongside source_site for API consumers |
target_site_id | UUID | Emitted alongside target_site for API consumers |
started_at | ISO 8601 | |
completed_at | ISO 8601 | |
error_message | string | Populated on failure |
instances_created | array | VM UUIDs created during the operation |
volumes_managed | array | |
networks_configured | array | |
floating_ips_allocated | array | |
warnings | array | Non-fatal issues |
steps_completed | array | |
steps_failed | array |
No estimated completion field exists. The operation carries a
progressinteger (0–100) but the service does not compute or expose an ETA or projected finish time for any operation type.
Recovered instances are named <original-name>-failover after a failover and <original-name>-restored after a failback. The member row retains the original instance_name, so suffixes do not compound over a round trip—but the workload never returns to its original name, which matters for DNS, monitoring, and CMDB entries keyed on instance name.
All error responses share the same JSON envelope:
{
"error": {
"code": 404,
"message": "Protection group abc not found",
"type": "ProtectionGroupNotFound"
}
}
Two additional error types are synthesised by the fault middleware and have no corresponding exception class:
| Type | HTTP | Trigger |
|---|---|---|
ResourceConflict | 409 | Any database unique-constraint violation. Common cause: reusing the name of a soft-deleted resource. Message: "The request conflicts with an existing resource. A name may still be held by a deleted resource." |
ServiceUnavailable | 503 | Any exception whose MRO contains OperationalError, InterfaceError, or DisconnectionError. Indicates a transient database connectivity failure; the request is retryable. |
Selected error reference
| Type | HTTP | When |
|---|---|---|
ProtectionGroupNotFound | 404 | PG UUID or name does not exist or is soft-deleted |
MemberNotFound | 404 | Member UUID not in this PG |
OperationNotFound | 404 | Operation UUID not found (or belongs to another project) |
SiteNotFound | 404 | Site UUID or name does not exist |
SiteAlreadyExists | 409 | Duplicate site name |
TenantMappingNotFound | 404 | Tenant mapping UUID not found |
TenantMappingAlreadyExists | 409 | Mapping for this local tenant + remote site pair already exists |
InvalidState | 409 | Resource is in a state that forbids the requested action (e.g. PG is failed over, member is being removed). Always 409 on every endpoint, including /action. |
InvalidInput | 400 | Malformed field value or missing required field |
InvalidAction | 400 | Unrecognised action key in POST …/action body |
InvalidParameters | 400 | Parameter combination not permitted |
Forbidden | 403 | Policy check failed |
Unauthorized | 401 | Invalid or missing Keystone token |
ProtectionGroupNotOnPrimarySite | 403 | Write operation attempted while PG is failed over |
ProtectionGroupFailedOver | 403 | Operation not permitted in failed-over state |
FailoverNotReady | 422 | PG preconditions not met for failover |
PlannedFailoverValidationFailed | 422 | Pre-flight validation failed for planned failover |
UnplannedFailoverValidationFailed | 422 | Pre-flight validation failed for unplanned failover |
ReplicationNotHealthy | 422 | Replication lag or link status prevents operation |
RPOViolation | 422 | Recovery point objective exceeded |
NoVolumesFound | 422 | Instance has no Cinder volumes |
InstanceNotCinderBoot | 422 | Instance does not boot from a Cinder volume |
VolumeTypeNotReplicated | 422 | Volume type lacks replication_enabled=True |
InstanceAlreadyProtected | 409 | Instance is already a member of a protection group |
SiteUnreachable | 503 | Target site did not respond |
PeerSiteUnavailable | 503 | Cross-site RPC or HTTP call failed |
RemoteSiteUnreachable | 503 | Remote Protector API unreachable during tenant mapping creation |
SiteNotHealthy | 503 | Remote /healthz returned non-200 during tenant mapping pre-flight |
ConsistencyGroupCreateFailed | 422 | Storage driver rejected CG creation |
MetadataSyncFailed | 500 | Cross-site metadata push failed |
InstanceCreationFailed | 500 | Nova instance build failed during failover/failback |
SnapshotReplicationTimeout | 500 | Storage snapshot did not replicate within timeout |
StorageSiteMappingUnknown | 500 | Pure driver could not match the PG's primary site name to a configured array |
LocalSiteNotConfigured | 500 | [DEFAULT] local_site is unset or names an unregistered site |
LUKS-encrypted volumes are rejected at member-add time with
InvalidInput. The Cinder manage API cannot associate Barbican keys on the DR site. Use array-side encryption (Pure data-at-rest) instead.
Authentication
All requests require a Keystone token. Source it from your environment or via openstack token issue.
export OS_TOKEN=$(openstack token issue -f value -c id)
export TENANT_ID=$(openstack project show -f value -c id myproject)
export API=http://protector-api.example.com:8788/v1/${TENANT_ID}
Sites
List sites (REST)
curl -s -H "X-Auth-Token: $OS_TOKEN" $API/sites | python3 -m json.tool
List sites (CLI)
openstack dr site list
Register a site (REST)
curl -s -X POST $API/sites \
-H "X-Auth-Token: $OS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"site": {
"name": "site-b",
"auth_url": "https://keystone-b.example.com:5000/v3",
"region_name": "RegionTwo",
"service_username": "protector",
"service_password": "s3cr3t",
"service_project_name": "service"
}
}'
Register a site (CLI)
openstack dr site create site-b \
--auth-url https://keystone-b.example.com:5000/v3 \
--region RegionTwo \
--service-username protector \
--service-password s3cr3t \
--service-project-name service
Register a site (Python client)
from keystoneauth1 import session as ks_session
from keystoneauth1.identity import v3 as ks_identity
from keystoneauth1 import adapter
from triliositerecovery.v1.client import Client
auth = ks_identity.Password(
auth_url="https://keystone-a.example.com:5000/v3",
username="protector",
password="s3cr3t",
project_name="myproject",
user_domain_name="Default",
project_domain_name="Default",
)
sess = ks_session.Session(auth=auth)
adapt = adapter.Adapter(
session=sess,
endpoint_override="http://protector-api.example.com:8788/v1/" + sess.get_project_id(),
)
client = Client(session=adapt, endpoint=adapt.endpoint_override)
site = client.sites.create(
name="site-b",
auth_url="https://keystone-b.example.com:5000/v3",
region_name="RegionTwo",
service_username="protector",
service_password="s3cr3t",
service_project_name="service",
)
print(site['id']) # plain dict — use site['id'], never site.id
Python client conventions:
- Managers return plain dicts (or lists of dicts), never object instances. Access fields with
site['id'], neversite.id.- The resource id is always the first positional argument to methods such as
get(),failover(),replication_health(). Passing it as a keyword argument silently misbehaves.- Mapping arguments (
network_mapping,flavor_mapping) are dicts at the library level:{'src-id': 'dst-id'}. Thesrc=dststring syntax is argparse-only and belongs to the CLI.Clientrequires bothsession(akeystoneauth1.adapter.Adapter) andendpoint. Omittingendpointcauses every request URL to begin withNone/and fail on the first call.
Protection groups
Create a protection group (REST, async replication)
curl -s -X POST $API/protection-groups \
-H "X-Auth-Token: $OS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"protection_group": {
"name": "prod-pg",
"primary_site_id": "<site-a-uuid>",
"secondary_site_id": "<site-b-uuid>",
"replication_type": "async",
"volume_type_id": "<replicated-volume-type-uuid>"
}
}'
Both primary_site_id and secondary_site_id are required; the API never infers the secondary site. volume_type_id is required for async replication and must reference a Cinder volume type with replication_enabled=True. It is not required for active_dr.
Create a protection group (CLI)
# async replication — --volume-type required
openstack dr protection group create prod-pg \
--primary-site site-a \
--secondary-site site-b \
--replication-type async \
--volume-type pure-replication
# ActiveDR continuous replication — --volume-type not required
openstack dr protection group create prod-pg-adr \
--primary-site site-a \
--secondary-site site-b \
--replication-type active_dr
Create a protection group (Python client)
pg = client.protection_groups.create(
name="prod-pg",
primary_site_id="<site-a-uuid>",
secondary_site_id="<site-b-uuid>",
replication_type="async",
volume_type_id="<replicated-volume-type-uuid>", # note: volume_type_id, not volume_type
)
print(pg['id'])
Members
Add a VM to a protection group (REST)
curl -s -X POST $API/protection-groups/<pg-uuid>/members \
-H "X-Auth-Token: $OS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"member": {"instance_id": "<instance-uuid>", "boot_order": 1}}'
Add a VM (CLI)
openstack dr protection group member add prod-pg --instance <instance-uuid> --boot-order 1
Add a VM (Python client)
# pg_id is the FIRST positional argument; boot_order is a keyword
member = client.protection_groups.add_member("<pg-uuid>", "<instance-uuid>", boot_order=1)
print(member['id'])
Validate a VM before adding (REST)
curl -s -X POST $API/protection-groups/<pg-uuid>/members/validate \
-H "X-Auth-Token: $OS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"member": {"instance_id": "<instance-uuid>"}}'
Resource mappings
Mappings tell the engine which network, flavor, floating IP pool, or floating IP on the primary site corresponds to which resource on the secondary site. They are stored under both the source resource name and id; the engine resolves by id first, then by name.
The resource_type field accepts network, flavor, floating_ip_pool, and floating_ip. Submitting subnet or security_group returns HTTP 400 "Unsupported resource type".
Create a network mapping (REST)
curl -s -X POST $API/protection-groups/<pg-uuid>/mappings \
-H "X-Auth-Token: $OS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"mapping": {
"resource_type": "network",
"primary_resource_id": "<primary-net-uuid>",
"secondary_resource_id": "<secondary-net-uuid>"
}
}'
Create a network mapping (CLI)
The separator between source and destination is =, never :.
# During failover — inline mapping
openstack dr failover prod-pg \
--failover-type planned \
--network-mapping <primary-net-uuid>=<secondary-net-uuid> \
--flavor-mapping <primary-flavor-uuid>=<secondary-flavor-uuid>
# Persistent mapping stored on the PG
openstack dr protection group mapping create prod-pg \
--resource-type network \
--primary-resource-id <primary-net-uuid> \
--secondary-resource-id <secondary-net-uuid>
Auto-map resources (REST)
curl -s -X POST $API/protection-groups/<pg-uuid>/mappings/auto \
-H "X-Auth-Token: $OS_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
Prepare floating IP mappings
openstack dr protection group floating ip prepare prod-pg --dr-network <secondary-external-net>
Or via REST:
curl -s -X POST $API/protection-groups/<pg-uuid>/mappings/floating-ips/prepare \
-H "X-Auth-Token: $OS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"dr_network": "<secondary-external-net-uuid>"}'
DR operations (failover, failback, test failover, reconcile)
All DR operations are submitted to POST /v1/{tenant_id}/protection-groups/{id}/action. The top-level key in the request body selects the operation. Valid keys are: failover, failback, failback_plan, test_failover, test_cleanup, reconcile.
Note: failback_plan is read-only — it returns {"failback_plan": <plan>} with HTTP 200 and does not start an operation.
Failover (REST)
curl -s -X POST $API/protection-groups/<pg-uuid>/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": {
"<primary-net-uuid>": "<secondary-net-uuid>"
}
}
}'
The endpoint accepts failover or failback as the top-level key. It does not accept failover_to_secondary or any other spelling.
Failover (CLI)
openstack dr failover prod-pg \
--failover-type planned \
--network-mapping <primary-net-uuid>=<secondary-net-uuid> \
--wait
Failover (Python client)
# DR operation methods are on client.operations (OperationManager),
# NOT on client.protection_groups.
op = client.operations.failover(
"<pg-uuid>", # positional
failover_type="planned",
network_mapping={"<primary-net-uuid>": "<secondary-net-uuid>"},
)
print(op['id'], op['status'])
Failback (CLI)
openstack dr failback prod-pg --wait
Failback (Python client)
op = client.operations.failback("<pg-uuid>")
print(op['id'])
Failback and volume detach: Failback waits up to 120 seconds per volume for Nova to release the Cinder attachment after the DR instance is deleted before unmanaging the volume. A slow detach produces a visibly longer failback; this is expected behaviour, not a hang.
Test failover (REST)
curl -s -X POST $API/protection-groups/<pg-uuid>/action \
-H "X-Auth-Token: $OS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"test_failover": {
"network_mapping": {
"<primary-net-uuid>": "<isolated-test-net-uuid>"
},
"vm_creation_params": {
"availability_zone": "nova-az-dr"
}
}
}'
Test failover cleanup (CLI)
openstack dr test failover cleanup prod-pg
The Python client method for cleanup is client.operations.cleanup_test_failover(pg_id), which sends the wire key test_cleanup.
Reconcile an inconsistent protection group (REST)
Reconcile repairs a protection group left inconsistent by an interrupted failover or failback. It works even when the primary site is unreachable.
curl -s -X POST $API/protection-groups/<pg-uuid>/action \
-H "X-Auth-Token: $OS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"reconcile": {}}'
Returns HTTP 200 (not 202) with:
{
"message": "reconcile completed",
"reconcile": { ... }
}
The operation is recorded in dr_operations with operation_type=RECONCILE and appears in dr operation list and dr operation show.
Reconcile (CLI)
openstack dr protection group reconcile prod-pg
Reconcile (Python client)
# reconcile() is on client.operations (OperationManager)
result = client.operations.reconcile("<pg-uuid>")
print(result)
Site-level failover
Site-level failover fails over all protection groups at a site simultaneously. It requires the dr_site_admin role.
Site failover (REST)
curl -s -X POST $API/sites/<site-uuid>/action \
-H "X-Auth-Token: $OS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"failover": {"failover_type": "planned"}}'
The top-level key must be failover or failback. The key failover_to_secondary is not accepted.
Site failover (CLI)
openstack dr site failover site-a --failover-type planned
Failover to this site (CLI)
openstack dr failover to this site --failover-type planned
Replication health and recovery points
Check replication health (REST)
curl -s -H "X-Auth-Token: $OS_TOKEN" \
"$API/protection-groups/<pg-uuid>/replication-health"
# With probe: also asks the recovery site whether networks, security groups,
# and flavors resolve there, as the project this group recovers into.
curl -s -H "X-Auth-Token: $OS_TOKEN" \
"$API/protection-groups/<pg-uuid>/replication-health?probe=true"
What
--probechecks: It queries the recovery site's Neutron and Nova to verify that the networks, security groups, and flavors the group needs resolve there—scoped to the project it recovers into. A resource that exists on the recovery site under a different project is correctly reported missing. It does not check the storage array or replication lag.
Check replication health (CLI)
openstack dr replication health prod-pg
openstack dr replication health prod-pg --probe
List recovery points (CLI)
openstack dr recovery point list prod-pg --limit 10
Create an on-demand recovery point (CLI)
openstack dr recovery point create prod-pg --suffix manual-$(date +%Y%m%d)
Replication audit (CLI)
openstack dr replication audit
The replication audit reports volumes being replicated at the array level that no protection group accounts for. These volumes are NOT protected—they consume capacity and have no managed recovery points or failover path.
Tenant mappings
# Create a tenant mapping (required for independent Keystone deployments)
openstack dr tenant mapping create \
--local-tenant <local-project-uuid> \
--remote-site site-b \
--remote-tenant <remote-project-uuid>
# Validate an existing mapping
openstack dr tenant mapping validate <mapping-uuid>
Create a tenant mapping (REST)
curl -s -X POST $API/tenant-mappings \
-H "X-Auth-Token: $OS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"tenant_mapping": {
"local_tenant_id": "<local-project-uuid>",
"remote_site_id": "<site-b-uuid>",
"remote_tenant_id": "<remote-project-uuid>"
}
}'
Tenant mapping creation pre-flights the remote Protector API for health before saving the local record. If the remote site is unreachable the mapping is saved with status: pending_sync and a warning field explains what to do next.
Operations (query)
List operations (CLI)
openstack dr operation list
openstack dr operation list --protection-group prod-pg --status running
Show operation detail (CLI)
openstack dr operation show <operation-uuid>
Show operation detail (Python client)
op = client.operations.get("<operation-uuid>")
print(op['status'], op['progress']) # progress is 0-100
print(op['source_site'], op['target_site']) # site names, not ids
Usage reports
# Current snapshot
openstack dr usage summary
# Monthly report as JSON
openstack dr usage report --period monthly --format json
# Custom date range
openstack dr usage report --start-date 2024-01-01 --end-date 2024-03-31 --format json
PDF is the default format for dr usage report. Pass --format json for a machine-readable response.
Authentication and Keystone integration
- The service registers in the Keystone catalogue under service type
protectoron every deployment; the Juju charm additionally registersdisaster-recovery. Yourkeystoneauth1adapter should useservice_type='protector'for catalogue discovery, or setendpoint_overrideexplicitly. - Every tenant-scoped trust the service creates delegates the roles listed in
[service_credentials] default_trust_roles. The default ismember,dr_operator. Both roles must already exist in Keystone: trust creation fails naming a missing role rather than skipping it. Never omitmemberfrom this list—it carries the Nova and Cinder access the trusts require.
RBAC summary
| Role | Scope | Gates |
|---|---|---|
member | Project | Standard OpenStack access; required in trust delegation |
dr_operator | Project | DR operations on own protection groups (failover, failback, test failover, reconcile, metadata sync) |
dr_site_admin | Cloud-wide (grant on admin project only) | Site create/update/delete, tenant mapping create/delete/validate, site-level failover, replication audit, site operation list/show |
admin | Cloud-wide | Superset; also gates protector:sites:storage_arrays |
dr_site_admin carries no project-scoping clause. A holder on any project can register sites and map tenants for every project. Grant it on the admin project only.
Soft deletes and ResourceConflict
Sites and protection groups are soft-deleted (the row is marked, not removed). A name held by a soft-deleted record blocks a new resource with the same name and returns HTTP 409 ResourceConflict. This is the most common cause of unexpected 409 responses outside of duplicate-key errors.
Mapping separator
The --network-mapping and --flavor-mapping CLI flags use = as the separator: --network-mapping <src-id>=<dst-id>. A : separator is accepted by argparse as a single opaque string; the mapping silently does not apply and failover falls back to name-based resolution—which may resolve to the wrong network without any error at parse time.
Writing operations require primary-site context
Member add/remove, mapping create/delete, and protection group update all require the PG to be on its primary site (not failed over). Attempting them on a failed-over PG returns ProtectionGroupNotOnPrimarySite (403) or InvalidState (409).
No oslo.messaging notifications
The service emits no oslo.messaging notifications. The observability surface is entirely polled: use dr replication health (with --probe), dr replication audit, dr recovery point list, and operation records (dr operation list / dr operation show) to monitor DR state. Do not build alerting on a notification consumer—none will ever arrive.
Tenant mapping fallback
If no tenant mapping is configured, the engine assumes the same project UUID on the peer site. This is correct for shared or federated Keystone deployments and essentially never correct for two independent clusters (the deployment this product targets). The error surfaces during a later cross-site operation rather than at setup time. Configure a tenant mapping for every project using DR across independent clusters.
Pure Storage site name pairing
In [pure_storage], primary_site_name must name the site whose array is at primary_management_ip. The driver resolves which array to use by comparing this name against the protection group's primary_site_id. An identical [pure_storage] section works unchanged on both nodes because the names travel with the IPs. Inverting the names causes failover to read from the wrong array; the driver raises StorageSiteMappingUnknown rather than guessing.