OpenKeel: the sovereign French Cloud solution of the inherent group, with a European reach Discover inherent

Reverse-spec · Services

designate — REST API

REST API — designate — branch stable/2026.2

Branch stable/2026.2 @ e3dd774db145 — repository https://opendev.org/openstack/designate — analysed on 2026-10-29

designate-api serves three applications through Paste urlmap (etc/designate/api-paste.ini:1): / (version document, Flask), /v2 (Pecan, the DNS API) and /admin (Pecan, disabled by default, [service:api]/enable_api_admin, designate/conf/api.py:46). There are no microversions: GET / lists v2, v2.0 … v2.4 with the same URL (designate/api/versions.py:37), and the API behaviour does not depend on the version a client asks for. The newest "version" (2.4) documents the JSON zone import with attributes.

The API process holds almost no logic: every handler parses the body into a Designate object, calls designate-central over RPC and renders the result. All policy checks and business rules are in central (designate/central/service.py), which is why the policy column below lists the rules enforced by the central methods each handler calls (extracted by _rewrite/tools/designate_routes.py and checked by sending every route to the real Pecan applications: 77/78 reach the expected handler; GET …/tasks/abandon is answered 405 on purpose, designate/api/v2/controllers/zones/tasks/abandon.py:49).

Cross-cutting behaviour

Pipeline

http_proxy_to_wsgi → cors → request_id → faultwrapper → validation_API_v2 → sizelimit → osprofiler → authtoken → keystonecontext → maintenance → normalizeuri → app (etc/designate/api-paste.ini:20). The admin pipeline is the same without validation_API_v2 (etc/designate/api-paste.ini:28). [service:api]/auth_strategy = noauth swaps authtoken keystonecontext for noauthcontext, which trusts X-Auth-Project-ID, X-Auth-User-ID and X-Roles (default admin!) headers (designate/api/middleware.py:172).

StepEffectDefined at
keystonecontextX-Identity-Status: Invalid → 401 with an empty body; builds the context from X-Tenant-ID, X-User-ID, X-Roles, X-Service-Catalog, OpenStack-System-Scopedesignate/api/middleware.py:138
context flagsheaders or query parameters turned into context flags, each guarded by a policy; a refused flag yields 403 with an empty bodydesignate/api/middleware.py:113, designate/api/middleware.py:168
maintenance[service:api]/maintenance_mode: 503 with Retry-After: 60 unless the caller has maintenance_mode_roledesignate/api/middleware.py:226
normalizeuristrips trailing / from the pathdesignate/api/middleware.py:251
faultwrapperDesignate exceptions → JSON error body; MessagingTimeout → 504; anything else → 500designate/api/middleware.py:260
validation_API_v2object validation errors → 400 with an errors listdesignate/api/middleware.py:340

Request headers and flags

Header (query parameter)Context flagPolicyDefined at
X-Auth-All-Projects (all_projects, all_tenants)all_tenantsall_tenants (admin)designate/api/middleware.py:63, designate/context.py:151
X-Auth-Sudo-Project-ID (X-Auth-Sudo-Tenant-ID)act as another projectuse_sudo (admin)designate/api/middleware.py:56, designate/context.py:130
X-Designate-Edit-Managed-Records (edit_managed_records)edit_managed_recordsedit_managed_records (admin)designate/api/middleware.py:82, designate/context.py:179
X-Designate-Hard-Deletehard_delete (delete the zone files on the backend)hard_delete (admin)designate/api/middleware.py:93, designate/context.py:189
X-Designate-Delete-Sharesdelete_shares (delete a shared zone and its shares)—designate/api/middleware.py:106
OpenStack-DNS-Hide-Countshide_counts (omit metadata.total_count)—designate/api/middleware.py:75

Bodies, content types, errors

  • Request bodies must be application/json (or application/json-patch+json where declared); anything else → 415 unsupported_content_type; empty body → 400 empty_request_body; unparsable → 400 invalid_json (designate/api/v2/patches.py:34). application/json-patch+json is accepted by Pecan on PATCH handlers but then raises NotImplementedError, i.e. 500 (designate/api/v2/controllers/zones/__init__.py:158).
  • Body keys are checked by the API_v2 adapters: unknown or read-only keys are rejected with 400 invalid_object (designate/objects/adapters/base.py:189); values are validated by the object fields (designate/objects/base.py:247). The writable fields per resource are in _rewrite/request-schemas_stable-2026.2.json.
  • Error body (designate/api/middleware.py:275): {"code": <status>, "type": "<error_type>", "message": "<text>", "request_id": "req-…"}, plus "errors": {"errors": [{"path": […], "message": "…", "validator": "…", "validator_value": …, "raw": "…"}]} for validation errors (designate/api/middleware.py:364). Pecan 404/405 are routed to the same handler (designate/api/v2/__init__.py:40, designate/api/v2/controllers/errors.py:27): {"code": 404, "type": "not_found", "message": "resource not found"}.
  • Path UUIDs are checked by @validate_uuid: invalid → 400 invalid_uuid; a wrong number of path segments → 404 (designate/utils.py:139).

Collections

  • Paging: limit (default [service:api]/default_limit_v2 = 20, max max_limit_v2 = 1000, limit=max allowed), marker (id of the last item seen), sort_key (per resource whitelist), sort_dir (asc/desc) (designate/utils.py:230). Errors: 400 invalid_limit, invalid_sort_dir, invalid_sort_key, invalid_marker, 400 marker_not_found. sort_key=tenant_id is silently ignored without all_projects (designate/utils.py:274).
  • Filters: one value per parameter, exact match, * is a wildcard (designate/api/v2/controllers/rest.py:55). A repeated parameter → 400 Duplicate filters …; an unknown one → 400 Invalid filters ….
  • Response: {"<collection>": […], "links": {"self": "…", "next": "…"}, "metadata": {"total_count": N}} (designate/objects/adapters/api_v2/base.py:33). next is present whenever the page is full, even on the last page (designate/objects/adapters/api_v2/base.py:126). Link hosts come from the request Host ([service:api]/enable_host_header, default true) or api_base_uri (designate/conf/api.py:28).
  • Resources carry links.self.

Statuses

Zone and recordset writes answer 202 when the object is PENDING (it will be pushed to the DNS servers) and 201/200 otherwise (designate/api/v2/controllers/zones/__init__.py:113). A zone delete always answers 202 with the zone in PENDING/DELETE (designate/api/v2/controllers/zones/__init__.py:199). Admin resources answer 201/200/204.

Summary

78 operations: 70 on /v2 (2 of them the /v2/service_status alias, 1 the deliberate 405 of GET …/tasks/abandon) and 8 on /admin when the admin extensions are enabled. The machine-readable table is _rewrite/routes_stable-2026.2.json.

MethodPathHandlerPolicy (enforced in central)SuccessErrors
GET/v2/zonesdesignate/api/v2/controllers/zones/__init__.py:61find_zones200400, 401, 403
POST/v2/zonesdesignate/api/v2/controllers/zones/__init__.py:84create_zone, use_low_ttl, use_blacklisted_zone, zone_create_forced_pool202400, 403, 409, 413, 500
GET/v2/zones/{zone_id}designate/api/v2/controllers/zones/__init__.py:47get_zone200400, 404
PATCH/v2/zones/{zone_id}designate/api/v2/controllers/zones/__init__.py:128get_zone, update_zone, use_low_ttl202 (200 for non-PENDING)400, 403, 404, 409
DELETE/v2/zones/{zone_id}designate/api/v2/controllers/zones/__init__.py:192delete_zone202400, 403, 404
GET/v2/zones/{zone_id}/recordsetsdesignate/api/v2/controllers/zones/recordsets.py:67get_zone, find_recordsets200400, 404
POST/v2/zones/{zone_id}/recordsetsdesignate/api/v2/controllers/zones/recordsets.py:78create_recordset, use_low_ttl202400, 403, 404, 409, 413
GET/v2/zones/{zone_id}/recordsets/{recordset_id}designate/api/v2/controllers/zones/recordsets.py:55get_recordset200400, 404
PUT/v2/zones/{zone_id}/recordsets/{recordset_id}designate/api/v2/controllers/zones/recordsets.py:118get_recordset, get_zone, update_recordset202400, 403, 404, 409, 413
DELETE/v2/zones/{zone_id}/recordsets/{recordset_id}designate/api/v2/controllers/zones/recordsets.py:166get_recordset, delete_recordset202400, 403, 404
GET/v2/zones/{zone_id}/nameserversdesignate/api/v2/controllers/zones/nameservers.py:30get_zone_ns_records200404
GET/v2/zones/{zone_id}/sharesdesignate/api/v2/controllers/zones/sharedzones.py:48find_zone_shares, find_project_zone_share200403
POST/v2/zones/{zone_id}/sharesdesignate/api/v2/controllers/zones/sharedzones.py:73share_zone201400, 403, 404, 409
GET/v2/zones/{zone_id}/shares/{zone_share_id}designate/api/v2/controllers/zones/sharedzones.py:31get_zone_share200404
DELETE/v2/zones/{zone_id}/shares/{zone_share_id}designate/api/v2/controllers/zones/sharedzones.py:101get_zone_share, unshare_zone204400, 403, 404
POST/v2/zones/{zone_id}/tasks/abandondesignate/api/v2/controllers/zones/tasks/abandon.py:30abandon_zone204 (500 if not deleted)403, 404
POST/v2/zones/{zone_id}/tasks/xfrdesignate/api/v2/controllers/zones/tasks/xfr.py:29get_zone, xfr_zone202400, 403, 404
POST/v2/zones/{zone_id}/tasks/pool_movedesignate/api/v2/controllers/zones/tasks/pool_move.py:33get_zone, pool_move_zone202 (500 if not PENDING)400, 403, 404
POST/v2/zones/{zone_id}/tasks/exportdesignate/api/v2/controllers/zones/tasks/exports.py:53create_zone_export202403, 404, 413
POST/v2/zones/{zone_id}/tasks/transfer_requestsdesignate/api/v2/controllers/zones/tasks/transfer_requests.py:73get_zone, create_zone_transfer_request201400, 403, 404, 409
GET/v2/zones/tasks/transfer_requestsdesignate/api/v2/controllers/zones/tasks/transfer_requests.py:50find_zone_transfer_requests200400
GET/v2/zones/tasks/transfer_requests/{zone_transfer_request_id}designate/api/v2/controllers/zones/tasks/transfer_requests.py:34get_zone_transfer_request200403, 404
PATCH/v2/zones/tasks/transfer_requests/{zone_transfer_request_id}designate/api/v2/controllers/zones/tasks/transfer_requests.py:112get_zone_transfer_request, update_zone_transfer_request200400, 403, 404
DELETE/v2/zones/tasks/transfer_requests/{zone_transfer_request_id}designate/api/v2/controllers/zones/tasks/transfer_requests.py:144delete_zone_transfer_request204403, 404
GET/v2/zones/tasks/transfer_acceptsdesignate/api/v2/controllers/zones/tasks/transfer_accepts.py:49find_zone_transfer_accepts200403
POST/v2/zones/tasks/transfer_acceptsdesignate/api/v2/controllers/zones/tasks/transfer_accepts.py:71get_zone_transfer_request, create_zone_transfer_accept201400, 403, 404, 413
GET/v2/zones/tasks/transfer_accepts/{transfer_accept_id}designate/api/v2/controllers/zones/tasks/transfer_accepts.py:33get_zone_transfer_accept200404
GET/v2/zones/tasks/importsdesignate/api/v2/controllers/zones/tasks/imports.py:50find_zone_imports200400
POST/v2/zones/tasks/importsdesignate/api/v2/controllers/zones/tasks/imports.py:71create_zone_import202400, 403, 415
GET/v2/zones/tasks/imports/{import_id}designate/api/v2/controllers/zones/tasks/imports.py:36get_zone_import200404
DELETE/v2/zones/tasks/imports/{zone_import_id}designate/api/v2/controllers/zones/tasks/imports.py:118delete_zone_import204404
GET/v2/zones/tasks/exportsdesignate/api/v2/controllers/zones/tasks/exports.py:94find_zone_exports200400
GET/v2/zones/tasks/exports/{export_id}designate/api/v2/controllers/zones/tasks/exports.py:81get_zone_export200404
DELETE/v2/zones/tasks/exports/{zone_export_id}designate/api/v2/controllers/zones/tasks/exports.py:119delete_zone_export204404
GET/v2/zones/tasks/exports/{export_id}/exportdesignate/api/v2/controllers/zones/tasks/exports.py:32zone_export, get_zone_export, get_zone200 text/dns400, 403, 404
GET/v2/recordsetsdesignate/api/v2/controllers/recordsets.py:48find_recordsets200400
GET/v2/recordsets/{recordset_id}designate/api/v2/controllers/recordsets.py:33get_recordset301 to the zone URL404
GET/v2/reverse/floatingipsdesignate/api/v2/controllers/floatingips.py:42none (Neutron filters with the user's token)200504
GET/v2/reverse/floatingips/{region}:{floatingip_id}designate/api/v2/controllers/floatingips.py:80none200400, 404, 504
PATCH/v2/reverse/floatingips/{region}:{floatingip_id}designate/api/v2/controllers/floatingips.py:54set_floatingip_reverse202400, 403, 404, 409, 504
GET, POST/v2/tldsdesignate/api/v2/controllers/tlds.py:44, designate/api/v2/controllers/tlds.py:66find_tlds, create_tld (admin)200, 201400, 403, 409
GET, PATCH, DELETE/v2/tlds/{tld_id}designate/api/v2/controllers/tlds.py:31, designate/api/v2/controllers/tlds.py:94, designate/api/v2/controllers/tlds.py:120get_tld, update_tld, delete_tld (admin)200, 200, 204400, 403, 404, 409
GET, POST/v2/tsigkeysdesignate/api/v2/controllers/tsigkeys.py:45, designate/api/v2/controllers/tsigkeys.py:67find_tsigkeys, create_tsigkey (admin)200, 201400, 403, 409
GET, PATCH, DELETE/v2/tsigkeys/{tsigkey_id}designate/api/v2/controllers/tsigkeys.py:32, designate/api/v2/controllers/tsigkeys.py:94, designate/api/v2/controllers/tsigkeys.py:123get_tsigkey, update_tsigkey, delete_tsigkey (admin)200, 200, 204400, 403, 404, 409
GET, POST/v2/blacklistsdesignate/api/v2/controllers/blacklists.py:46, designate/api/v2/controllers/blacklists.py:68find_blacklists, create_blacklist (admin)200, 201400, 403, 409
GET, PATCH, DELETE/v2/blacklists/{blacklist_id}designate/api/v2/controllers/blacklists.py:33, designate/api/v2/controllers/blacklists.py:98, designate/api/v2/controllers/blacklists.py:125get_blacklist, update_blacklist, delete_blacklist (admin)200, 200, 204400, 403, 404, 409
GET, POST/v2/poolsdesignate/api/v2/controllers/pools.py:44, designate/api/v2/controllers/pools.py:66find_pools, create_pool (admin; POST deprecated)200, 201400, 403, 409
GET, PATCH, DELETE/v2/pools/{pool_id}designate/api/v2/controllers/pools.py:32, designate/api/v2/controllers/pools.py:98, designate/api/v2/controllers/pools.py:130get_pool, update_pool, delete_pool (admin; PATCH/DELETE deprecated)200, 202, 204400, 403, 404, 409
GET/v2/quotasdesignate/api/v2/controllers/quotas.py:34get_quotas (own project)200403
GET, PATCH, DELETE/v2/quotas/{project_id}designate/api/v2/controllers/quotas.py:44, designate/api/v2/controllers/quotas.py:54, designate/api/v2/controllers/quotas.py:90get_quotas, set_quota, reset_quotas200, 200, 204400, 401, 403
GET/v2/limitsdesignate/api/v2/controllers/limits.py:28none200—
GET/v2/service_statuses (alias /v2/service_status)designate/api/v2/controllers/service_status.py:27find_service_statuses (admin)200403
GET/v2/service_statuses/{service_id} (alias)designate/api/v2/controllers/service_status.py:46find_service_status (admin)200403, 404
GET/admin/reports/counts[/{criterion}]designate/api/admin/controllers/extensions/counts.py:30count_zones, count_records, count_tenants (admin)200403, 404
GET/admin/reports/tenants[/{tenant_id}]designate/api/admin/controllers/extensions/tenants.py:31find_tenants, get_tenant (admin)200403
GET, PATCH, DELETE/admin/quotas/{tenant_id}designate/api/admin/controllers/extensions/quotas.py:35all_tenants + get_quotas, set_quota, reset_quotas200, 200, 204400, 403
GET/admin/zones/export/{zone_id}designate/api/admin/controllers/extensions/export.py:29zone_export, get_zone200 text/dns403, 404

Default rules (_rewrite/policies_stable-2026.2.yaml): project resources are admin or (member and project_id) for writes and admin or (reader and project_id) for reads; TLDs, TSIG keys, blacklists, pools, service statuses, tenants, abandon_zone, pool_move_zone, purge_zones, use_low_ttl, use_blacklisted_zone, all_tenants, edit_managed_records, use_sudo and hard_delete are role:admin (designate/common/policies/base.py:30). Every rule has scope_types = ['project']; a system-scoped token is refused with 403 invalid_token_scope (oslo.policy InvalidScope, designate/policy.py:73).

Zones

POST /v2/zones

Functional description

  • Purpose: create a DNS zone that the project owns and that Designate publishes on the DNS servers of a pool.
  • Actor: project member (create_zone); an admin may create it for another project by setting project_id through X-Auth-Sudo-Project-ID.
  • Preconditions: the pool chosen by the scheduler has at least one NS record (designate/central/service.py:914); zones in a non-default pool need a TSIG key with scope=POOL for that pool (designate/central/service.py:395); quota zones (default 10, designate/conf/base.py:80).
  • Business rules (all in designate/central/service.py:848):
    • type defaults to PRIMARY (designate/api/v2/controllers/zones/__init__.py:92); CATALOG zones cannot be created through the API (designate/central/service.py:3181). A SECONDARY zone needs masters and gets the email [service:central]/managed_resource_email (designate/api/v2/controllers/zones/__init__.py:98); a PRIMARY one needs email.
    • Name: fully qualified, ending with ., at most max_zone_name_len (255), at least two labels, under a configured TLD if TLDs exist and not equal to a TLD, not matching a blacklist pattern unless use_blacklisted_zone (designate/central/service.py:125). Blacklist regexes are evaluated with a 20 ms timeout, a timeout counts as a match (designate/central/service.py:279).
    • ttl ≥ [service:central]/min_ttl (default 0) unless use_low_ttl (designate/central/service.py:378).
    • Cross-tenant collisions are refused in every pool: same name → 409 duplicate_zone, sub-zone of another project's zone → 403 illegal_child, super-zone of another project's zone → 403 illegal_parent (designate/central/service.py:335). The same project may own the same name in several pools (split horizon).
    • Pool selection: scheduler filters [service:central]/scheduler_filters (default default_pool, designate/conf/central.py:46); attributes match pool attributes with the attribute filter; attributes.pool_id forces a pool with pool_id_attribute and zone_create_forced_pool (designate/scheduler/filters/pool_id_attribute_filter.py:69). No pool → 404 no_valid_pool_found; several → 500 multiple_pools_found (designate/scheduler/base.py:81).
    • SOA refresh is randomised between default_soa_refresh_min and default_soa_refresh_max (3500–3600 s) to spread AXFRs (designate/central/service.py:813).
  • Flow:
    1. (synchronous) Parse and validate the body, call central create_zone under the create-new-zone lock.
    2. (synchronous) Insert the zone CREATE/PENDING, its SOA recordset (MNAME = the pool NS with the lowest priority) and NS recordset, both managed (designate/central/service.py:952); set parent_zone_id of the zone and of existing sub-zones of the same project.
    3. (synchronous) Cast worker create_zone; for SECONDARY zones also perform_zone_xfr (designate/central/service.py:936).
    4. (asynchronous) The worker creates the zone on every pool target, sends NOTIFY, polls the nameservers and reports with update_status (async).
  • Outcome: 202 with status: PENDING, action: CREATE, Location header; then ACTIVE/NONE or ERROR. Clients poll GET /v2/zones/{id}.
  • Effects visible elsewhere: notifications dns.zone.create and dns.domain.create; zone added to the pool's catalog zone (serial bump) when the pool has one (designate/central/service.py:3165).

Technical sheet

  • Request (writable fields, designate/objects/adapters/api_v2/zone.py:18): name (required, immutable), email (PRIMARY), ttl, description (≤ 160), type (PRIMARY/SECONDARY, immutable), masters (list of host[:port] strings, SECONDARY), attributes (map, immutable). Any other key → 400.
  • Response: {id, pool_id, project_id, name, email, description, ttl, serial, status, action, version, attributes, type, masters, shared, created_at, updated_at, transferred_at, links}.
  • Example: json POST /v2/zones {"name": "example.org.", "email": "joe@example.org", "ttl": 7200, "description": "This is an example zone."}

GET /v2/zones, GET /v2/zones/{zone_id}

  • Purpose: list or read the project's zones, including zones shared with it (shared: true) and zones being deleted.
  • Rules: filters name, type, email, status, description, ttl; sort keys created_at, id, updated_at, name, tenant_id, serial, ttl, status (designate/api/v2/controllers/zones/__init__.py:37). Non-admins never see CATALOG zones (designate/central/service.py:1042). A zone of another project that is not shared is reported 404, not 403 (designate/central/service.py:984).

PATCH /v2/zones/{zone_id}

  • Purpose: change email, ttl, description, masters (SECONDARY) or pool_id **[inferred: pool_id is writable in the adapter but moving a zone is done by tasks/pool_move]**.
  • Rules: a zone being deleted → 400 Can not update a deleting zone (designate/api/v2/controllers/zones/__init__.py:142); name, type, project_id cannot change (designate/central/service.py:1073); changing attributes clears transferred_at (designate/api/v2/controllers/zones/__init__.py:166). The serial is incremented only for PRIMARY zones when a field other than description changed (designate/api/v2/controllers/zones/__init__.py:175, fixed in this branch, see history).
  • Flow: the zone becomes UPDATE/PENDING with increment_serial = true; the producer bumps the serial within 5 s and pushes it (async). A new masters list on a SECONDARY zone triggers an AXFR (designate/central/service.py:1097).

DELETE /v2/zones/{zone_id}

  • Purpose: remove the zone from the DNS servers, then from Designate.
  • Rules: a zone with sub-zones (in any project) → 400 zone_has_sub_zone (designate/central/service.py:1157); a shared zone → 400 zone_is_shared unless X-Designate-Delete-Shares: true (designate/central/service.py:1147); X-Designate-Hard-Delete: true also deletes the backend zone files (designate/central/service.py:1173).
  • Flow: the zone becomes DELETE/PENDING (202); the worker deletes it on the targets and polls until the nameservers stop answering (serial == 0), then update_status soft-deletes it (deleted = id) (designate/central/service.py:2573); the producer hard-deletes it 7 days later (zone_purge).

Zone tasks

OperationWhat it doesRulesDefined at
POST …/tasks/abandondelete the zone from Designate only, leaving it on the DNS serversadmin (abandon_zone); 204, or 500 when the zone was not marked deleteddesignate/central/service.py:1167
POST …/tasks/xfrfor a SECONDARY zone, compare one master's serial and AXFR if newernon-SECONDARY → 400 Can't XFR a non Secondary zone.designate/central/service.py:1209
POST …/tasks/pool_movemove the zone to pool_id (body) or to the pool the scheduler picksadmin; same pool → 400; unknown pool → 400 Target pool does not exist; NS records of the new pool replace the old onesdesignate/central/service.py:1415
GET …/nameserversthe pool's NS records (hostname, priority)readerdesignate/central/service.py:1007

Zone shares (API 2.1)

  • POST /v2/zones/{zone_id}/shares {"target_project_id": "…"}: the target project can then see the zone and create recordsets in it (create_recordset allows zone_shared). The target project id is checked in Keystone (designate/api/v2/controllers/zones/sharedzones.py:81, 400 invalid_project if Keystone says 404); sharing with the owner → 400 (designate/central/service.py:1297).
  • DELETE …/shares/{id}: refused while the target project owns sub-zones (400 shared_zone_has_sub_zone) or recordsets in the zone (400 shared_zone_has_recordsets) (designate/central/service.py:1327).
  • GET …/shares: filter target_project_id.

Recordsets

POST /v2/zones/{zone_id}/recordsets

  • Purpose: publish a set of records of one name and type in a zone.
  • Actor: member of the owning project, or of a project the zone is shared with (PRIMARY zones only); admin for SECONDARY zones (create_recordset rule).
  • Business rules:
    • name is fully qualified and inside the zone (designate/central/service.py:175), not inside a child zone (designate/central/service.py:218), at most max_recordset_name_len (255).
    • type in supported_record_type (designate/conf/base.py:55: A, AAAA, CNAME, MX, SRV, TXT, SPF, NS, PTR, SSHFP, SOA, NAPTR, CAA, CERT, HTTPS, SVCB, TLSA) (designate/objects/recordset.py:127); SOA cannot be created (designate/api/v2/controllers/zones/recordsets.py:91).
    • CNAME: not at the apex, alone at its name, one record only (designate/central/service.py:193, designate/central/service.py:250).
    • TLSA names follow _port._protocol.name with port ≤ 65535 (designate/api/v2/controllers/zones/recordsets.py:35).
    • Each record's text is parsed by its type's rdata object (designate/objects/rrdata_*.py, fields in _rewrite/request-schemas_stable-2026.2.json rrdata); records are a set: duplicates are dropped and order is not kept (designate/objects/adapters/api_v2/recordset.py:73).
    • Quotas: zone_recordsets (500), zone_records (500), recordset_records (20); managed records do not count (designate/central/service.py:558).
  • Flow: records are stored CREATE/PENDING with serial = now; the zone becomes UPDATE/PENDING with increment_serial (designate/central/service.py:1574); the producer bumps the serial and pushes the zone; update_status sets the records ACTIVE when the nameservers serve a serial ≥ theirs.
  • Outcome: 202, recordset status: PENDING, action: CREATE, Location.
  • Request: name, type (both immutable), records (list of strings), ttl, description.

PUT /v2/zones/{zone_id}/recordsets/{recordset_id}

  • Replaces records, ttl, description. Managed records (SOA, apex NS, PTR of floating IPs, sink records) need X-Designate-Edit-Managed-Records (designate/api/v2/controllers/zones/recordsets.py:129); SOA and apex NS cannot be updated (designate/api/v2/controllers/zones/recordsets.py:133). A project the zone is shared with may update only its own recordsets (update_recordset rule, recordset_project_id).

DELETE /v2/zones/{zone_id}/recordsets/{recordset_id}

  • 202; the recordset row is deleted at once while its records stay DELETE/PENDING until the push succeeds (designate/central/service.py:1865). SOA → 400; managed → 400 without the managed flag.

GET /v2/recordsets, GET /v2/recordsets/{id}

  • Cross-zone listing with the same filters (name, type, ttl, data, status, description, designate/api/v2/controllers/common.py:25); GET /v2/recordsets/{id} answers 301 to /v2/zones/{zone_id}/recordsets/{id} (designate/api/v2/controllers/recordsets.py:45).

Reverse DNS for floating IPs

  • GET /v2/reverse/floatingips: lists the project's floating IPs from Neutron (all regions of the catalog, the user's token) and joins the managed PTR records Designate holds for them (designate/central/service.py:2014). PTR records of a floating IP now owned by another project are deleted on the fly (designate/central/service.py:1972). Each item: {id: "<region>:<fip uuid>", address, ptrdname, ttl, description, action, status, links}; status is INACTIVE without a PTR.
  • PATCH /v2/reverse/floatingips/{region}:{id} {"ptrdname": "host.example.org.", "ttl": 300, "description": "…"} sets the PTR; {"ptrdname": null} removes it (designate/central/service.py:2332). The PTR lives in the zone <c>.<b>.<a>.in-addr.arpa. owned by [service:central]/managed_resource_tenant_id, created on first use (designate/central/service.py:2119); a zone with that name owned by another project → 409 duplicate_zone (fixed in this branch). 202 with the floating IP.
  • Limitations: the reverse zone name is computed for IPv4 only (designate/network_api/base.py:130) **[inferred: an IPv6 floating IP would produce .in-addr.arpa.]**; reads have no policy rule of their own.

Imports and exports

  • POST /v2/zones/tasks/imports with Content-Type: text/dns (a zone file) or, since 2.4, application/json {"zonefile": "…", "attributes": {…}} (designate/api/v2/controllers/zones/tasks/imports.py:79): 202 with a task {id, status: PENDING, message: null, zone_id: null, project_id, version, created_at, updated_at, links}. The zone is created asynchronously by central (async); the task becomes COMPLETE (message: "<zone> imported", zone_id) or ERROR (message, see errors). $ORIGIN must be the first statement and an SOA is required; the file's SOA and apex NS are replaced by Designate's own.
  • POST /v2/zones/{zone_id}/tasks/export: 202 with a task; the api_export_size quota (1000 recordsets) is checked first (designate/central/service.py:3043). The worker completes it with location: designate://v2/zones/tasks/exports/<id>/export (designate/worker/tasks/zone.py:992).
  • GET /v2/zones/tasks/exports/{id}/export (Accept: text/dns): renders the zone file from the database with designate/resources/templates/export-zone.jinja2 (designate/central/service.py:1706); a task whose location is not designate:// → 400 (designate/api/v2/controllers/zones/tasks/exports.py:44).

Zone ownership transfers

  • POST /v2/zones/{zone_id}/tasks/transfer_requests {"target_project_id": "…", "description": "…"} (body optional): creates a request with an 8-character key (uppercase letters and digits, designate/central/service.py:2664); one request per zone (409 duplicate_zone_transfer_request).
  • GET of a request: visible to the owner and to the target project; with no target, to any project that knows its id (get_zone_transfer_request rule None:%(target_project_id)s). key, project_id and target_project_id are only rendered for the owner (get_zone_transfer_request_detailed, designate/objects/adapters/api_v2/zone_transfer_request.py:66).
  • POST /v2/zones/tasks/transfer_accepts {"zone_transfer_request_id": "…", "key": "…"}: a used request → 400 Zone Transfer Request has been used, wrong key → 403 invalid_key (designate/central/service.py:2781); the accepting project's zone quota is checked; the zone's tenant_id changes in place (no DNS change); request and accept become COMPLETE (designate/central/service.py:2809). 201.

Administration resources

  • TLDs {name, description}: once at least one TLD exists, zones must be under one of them.
  • Blacklists {pattern, description}: Python regular expressions matched against zone names.
  • TSIG keys {name, algorithm, secret, scope: POOL|ZONE, resource_id, description}: the resource must exist (400 invalid_tsigkey, designate/central/service.py:703). The secret is returned in clear to admins. Keys are used by mdns, the worker's NOTIFY/SOA queries and the backends.
  • Pools: read-only in practice; creating, updating and deleting pools through the API is deprecated in favour of designate-manage pool update (designate/api/v2/controllers/pools.py:69). PATCH answers 202. Removing the last NS record → 400 last_server_delete_not_allowed; NS changes are applied to every zone of the pool with a delayed NOTIFY (designate/central/service.py:2447); a pool with zones (other than its catalog zone) cannot be deleted (400 invalid_operation).
  • Quotas: GET /v2/quotas (own project), GET/PATCH/DELETE /v2/quotas/{project_id} with {"zones", "zone_recordsets", "zone_records", "recordset_records", "api_export_size"}; another project needs all_projects (designate/central/service.py:615); a non-project token without all_projects → 401 missing_project_id (designate/api/v2/controllers/quotas.py:73).
  • Limits: {max_zones, max_zone_recordsets, max_zone_records, max_recordset_records, min_ttl, max_zone_name_length, max_recordset_name_length, max_page_limit} for the caller's project (designate/api/v2/controllers/limits.py:33).
  • Service statuses: {id, service_name, hostname, status: UP|DOWN|WARNING|STOPPED, stats, capabilities, heartbeated_at} from the heartbeats; filters hostname, service_name, status.
  • Admin API (enabled_extensions_admin = reports, quotas, zones): /admin/reports/counts → {"counts": [{zones, records, tenants}]}, counts/{zones|zones_delayed_notify|records|tenants}, unknown criterion → 404 report_not_found (designate/central/service.py:1253); /admin/reports/tenants → zone counts per project; /admin/quotas/{tenant_id} (sets all_tenants, so admin only); /admin/zones/export/{zone_id} → zone file.

Spec/code mismatches

  • The api-ref documents GET /v2/zones/tasks/transfer_requests/{zone_transfer_accept_id} where the code serves /v2/zones/tasks/transfer_accepts/{id}; the policy file repeats the mistake in get_zone_transfer_accept operations (_rewrite/route-probe_stable-2026.2.json lists five policy operations whose path matches no route).
  • /v2/service_status is served on purpose as an alias of /v2/service_statuses (bug 1919183, designate/api/v2/controllers/root.py:69).
  • The /admin API is not in the api-ref.
  • PATCH /v2/pools/{id} answers 202 although the change is synchronous.

Lifecycle: zone

stateDiagram-v2
  [*] --> CREATE_PENDING: POST /v2/zones (API)
  CREATE_PENDING --> NONE_ACTIVE: update_status SUCCESS (worker poll)
  CREATE_PENDING --> CREATE_ERROR: update_status ERROR / NO_ZONE
  CREATE_ERROR --> NONE_ACTIVE: recover_shard re-push (producer, 120 s) then SUCCESS
  NONE_ACTIVE --> UPDATE_PENDING: PATCH zone, recordset change, increment_serial / delayed_notify (producer)
  UPDATE_PENDING --> NONE_ACTIVE: update_status SUCCESS
  UPDATE_PENDING --> UPDATE_ERROR: update_status ERROR
  UPDATE_PENDING --> CREATE_ERROR: update_status NO_ZONE
  UPDATE_ERROR --> NONE_ACTIVE: recover_shard then SUCCESS
  NONE_ACTIVE --> DELETE_PENDING: DELETE (API)
  UPDATE_PENDING --> DELETE_PENDING: DELETE (API)
  DELETE_PENDING --> SOFT_DELETED: update_status SUCCESS / NO_ZONE (status DELETED, deleted = id)
  DELETE_PENDING --> DELETE_ERROR: update_status ERROR
  DELETE_ERROR --> SOFT_DELETED: recover_shard then SUCCESS
  SOFT_DELETED --> [*]: zone_purge after 7 days (producer)
  NONE_ACTIVE --> [*]: POST tasks/abandon (API, immediate delete)

States are <action>_<status> (designate/central/service.py:2637).

Lifecycle: record (inside a recordset)

stateDiagram-v2
  [*] --> CREATE_PENDING: POST/PUT recordset (serial = now)
  CREATE_PENDING --> NONE_ACTIVE: update_status SUCCESS with serial >= record serial
  CREATE_PENDING --> CREATE_ERROR: update_status ERROR
  NONE_ACTIVE --> UPDATE_PENDING: PUT recordset
  UPDATE_PENDING --> NONE_ACTIVE: update_status SUCCESS
  NONE_ACTIVE --> DELETE_PENDING: DELETE recordset or record removed by PUT
  DELETE_PENDING --> [*]: update_status SUCCESS (status DELETED)

The recordset's status shown by the API is the worst status of its records (ERROR, then PENDING, ACTIVE, DELETED), ACTIVE when it has none (designate/objects/recordset.py:69); managed is true when any record is managed (designate/objects/recordset.py:61).

Lifecycle: zone import, export and transfer request

stateDiagram-v2
  [*] --> PENDING: POST tasks/imports or tasks/export
  PENDING --> COMPLETE: import thread / worker export
  PENDING --> ERROR: parse error, duplicate, quota, no method
  COMPLETE --> [*]: DELETE task
  ERROR --> [*]: DELETE task
stateDiagram-v2
  [*] --> ACTIVE: POST transfer_requests
  ACTIVE --> COMPLETE: POST transfer_accepts with the right key
  ACTIVE --> [*]: DELETE request

Source: reverse-spec-designate/01-rest-api_stable-2026.2.md. The _rewrite/ and _inventory/ files are not published on the site.