Trilio Site Recovery for OpenStackOpenStack native DR service
API reference

API Reference

REST API and Python client bindings


Description

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.


Parameters

Common request conventions

ElementValue
Base URLhttp(s)://<host>:8788/v1/{tenant_id}
AuthenticationX-Auth-Token: <keystone-token> header
Content-Typeapplication/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:

ParameterTypeEndpointsDescription
all_projectsbooleanprotection groups, operationsAdmin only. List resources across all projects.
statusstringprotection groups, operations, tenant mappings, site operationsFilter by resource status.
namestringprotection groupsFilter by exact name.
limitintegerrecovery points, site operationsMaximum results to return.
all_tenantsbooleantenant mappingsAdmin only. List all tenant mappings.
remote_site_idUUIDtenant mappingsFilter by remote site.
operation_typestringoperationsFilter by operation type.
protection_group_id / pg_idUUIDoperationsFilter by protection group.
forcebooleansite delete, PG deleteSkip referential-integrity guard.
dry_runbooleanPG sync, floating IP prepareReport changes without applying them.
probebooleanreplication healthAlso 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.

Returns

All successful responses return JSON. The top-level key mirrors the resource name or collection:

Endpoint patternSuccess statusTop-level key
GET …/sites200sites (array)
POST …/sites201site
GET …/sites/{id}200site
PUT …/sites/{id}200site
DELETE …/sites/{id}204(no body)
GET …/protection-groups200protection_groups (array)
POST …/protection-groups202protection_group
GET …/protection-groups/{id}200protection_group
PUT …/protection-groups/{id}200protection_group
DELETE …/protection-groups/{id}204 or 200(204 = clean delete; 200 with leftovers array when remote cleanup failed)
POST …/protection-groups/{id}/action202operation + operation_id; for reconcile: 200 with reconcile key; for failback_plan: 200 with failback_plan key
POST …/sites/{id}/action202site_operation
GET …/operations200operations (array)
GET …/operations/{id}200operation
GET …/tenant-mappings200tenant_mappings (array)
POST …/tenant-mappings201tenant_mapping
DELETE …/tenant-mappings/{id}204(no body)
GET …/replication-audit200replication_audit
GET …/usage/summary200usage_summary
GET …/usage/report200usage_report (JSON) or PDF bytes

Operation object fields

The operation object returned by action endpoints and GET …/operations/{id} includes:

FieldTypeNotes
idUUIDOperation identifier
protection_group_idUUID
operation_typestringFAILOVER, FAILBACK, TEST_FAILOVER, TEST_CLEANUP, SYNC_VOLUMES, VALIDATE_REPLICATION, RECONCILE
failover_typestringplanned or unplanned
statusstringpending, running, completed, failed
progressinteger0–100
source_sitestringSource site name (falls back to its id)
target_sitestringTarget site name (falls back to its id)
source_site_idUUIDEmitted alongside source_site for API consumers
target_site_idUUIDEmitted alongside target_site for API consumers
started_atISO 8601
completed_atISO 8601
error_messagestringPopulated on failure
instances_createdarrayVM UUIDs created during the operation
volumes_managedarray
networks_configuredarray
floating_ips_allocatedarray
warningsarrayNon-fatal issues
steps_completedarray
steps_failedarray

No estimated completion field exists. The operation carries a progress integer (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.


Errors

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:

TypeHTTPTrigger
ResourceConflict409Any 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."
ServiceUnavailable503Any exception whose MRO contains OperationalError, InterfaceError, or DisconnectionError. Indicates a transient database connectivity failure; the request is retryable.

Selected error reference

TypeHTTPWhen
ProtectionGroupNotFound404PG UUID or name does not exist or is soft-deleted
MemberNotFound404Member UUID not in this PG
OperationNotFound404Operation UUID not found (or belongs to another project)
SiteNotFound404Site UUID or name does not exist
SiteAlreadyExists409Duplicate site name
TenantMappingNotFound404Tenant mapping UUID not found
TenantMappingAlreadyExists409Mapping for this local tenant + remote site pair already exists
InvalidState409Resource 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.
InvalidInput400Malformed field value or missing required field
InvalidAction400Unrecognised action key in POST …/action body
InvalidParameters400Parameter combination not permitted
Forbidden403Policy check failed
Unauthorized401Invalid or missing Keystone token
ProtectionGroupNotOnPrimarySite403Write operation attempted while PG is failed over
ProtectionGroupFailedOver403Operation not permitted in failed-over state
FailoverNotReady422PG preconditions not met for failover
PlannedFailoverValidationFailed422Pre-flight validation failed for planned failover
UnplannedFailoverValidationFailed422Pre-flight validation failed for unplanned failover
ReplicationNotHealthy422Replication lag or link status prevents operation
RPOViolation422Recovery point objective exceeded
NoVolumesFound422Instance has no Cinder volumes
InstanceNotCinderBoot422Instance does not boot from a Cinder volume
VolumeTypeNotReplicated422Volume type lacks replication_enabled=True
InstanceAlreadyProtected409Instance is already a member of a protection group
SiteUnreachable503Target site did not respond
PeerSiteUnavailable503Cross-site RPC or HTTP call failed
RemoteSiteUnreachable503Remote Protector API unreachable during tenant mapping creation
SiteNotHealthy503Remote /healthz returned non-200 during tenant mapping pre-flight
ConsistencyGroupCreateFailed422Storage driver rejected CG creation
MetadataSyncFailed500Cross-site metadata push failed
InstanceCreationFailed500Nova instance build failed during failover/failback
SnapshotReplicationTimeout500Storage snapshot did not replicate within timeout
StorageSiteMappingUnknown500Pure driver could not match the PG's primary site name to a configured array
LocalSiteNotConfigured500[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.


Examples

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'], never site.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'}. The src=dst string syntax is argparse-only and belongs to the CLI.
  • Client requires both session (a keystoneauth1.adapter.Adapter) and endpoint. Omitting endpoint causes every request URL to begin with None/ 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 --probe checks: 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.


Notes

Authentication and Keystone integration

  • The service registers in the Keystone catalogue under service type protector on every deployment; the Juju charm additionally registers disaster-recovery. Your keystoneauth1 adapter should use service_type='protector' for catalogue discovery, or set endpoint_override explicitly.
  • Every tenant-scoped trust the service creates delegates the roles listed in [service_credentials] default_trust_roles. The default is member,dr_operator. Both roles must already exist in Keystone: trust creation fails naming a missing role rather than skipping it. Never omit member from this list—it carries the Nova and Cinder access the trusts require.

RBAC summary

RoleScopeGates
memberProjectStandard OpenStack access; required in trust delegation
dr_operatorProjectDR operations on own protection groups (failover, failback, test failover, reconcile, metadata sync)
dr_site_adminCloud-wide (grant on admin project only)Site create/update/delete, tenant mapping create/delete/validate, site-level failover, replication audit, site operation list/show
adminCloud-wideSuperset; 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.