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).
| Step | Effect | Defined at |
|---|---|---|
| keystonecontext | X-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-Scope | designate/api/middleware.py:138 |
| context flags | headers or query parameters turned into context flags, each guarded by a policy; a refused flag yields 403 with an empty body | designate/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_role | designate/api/middleware.py:226 |
| normalizeuri | strips trailing / from the path | designate/api/middleware.py:251 |
| faultwrapper | Designate exceptions → JSON error body; MessagingTimeout → 504; anything else → 500 | designate/api/middleware.py:260 |
| validation_API_v2 | object validation errors → 400 with an errors list | designate/api/middleware.py:340 |
Request headers and flags
| Header (query parameter) | Context flag | Policy | Defined at |
|---|---|---|---|
X-Auth-All-Projects (all_projects, all_tenants) | all_tenants | all_tenants (admin) | designate/api/middleware.py:63, designate/context.py:151 |
X-Auth-Sudo-Project-ID (X-Auth-Sudo-Tenant-ID) | act as another project | use_sudo (admin) | designate/api/middleware.py:56, designate/context.py:130 |
X-Designate-Edit-Managed-Records (edit_managed_records) | edit_managed_records | edit_managed_records (admin) | designate/api/middleware.py:82, designate/context.py:179 |
X-Designate-Hard-Delete | hard_delete (delete the zone files on the backend) | hard_delete (admin) | designate/api/middleware.py:93, designate/context.py:189 |
X-Designate-Delete-Shares | delete_shares (delete a shared zone and its shares) | — | designate/api/middleware.py:106 |
OpenStack-DNS-Hide-Counts | hide_counts (omit metadata.total_count) | — | designate/api/middleware.py:75 |
Bodies, content types, errors
- Request bodies must be
application/json(orapplication/json-patch+jsonwhere declared); anything else → 415unsupported_content_type; empty body → 400empty_request_body; unparsable → 400invalid_json(designate/api/v2/patches.py:34).application/json-patch+jsonis accepted by Pecan on PATCH handlers but then raisesNotImplementedError, 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 → 400invalid_uuid; a wrong number of path segments → 404 (designate/utils.py:139).
Collections
- Paging:
limit(default[service:api]/default_limit_v2= 20, maxmax_limit_v2= 1000,limit=maxallowed),marker(id of the last item seen),sort_key(per resource whitelist),sort_dir(asc/desc) (designate/utils.py:230). Errors: 400invalid_limit,invalid_sort_dir,invalid_sort_key,invalid_marker, 400marker_not_found.sort_key=tenant_idis silently ignored withoutall_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 → 400Duplicate filters …; an unknown one → 400Invalid filters …. - Response:
{"<collection>": […], "links": {"self": "…", "next": "…"}, "metadata": {"total_count": N}}(designate/objects/adapters/api_v2/base.py:33).nextis present whenever the page is full, even on the last page (designate/objects/adapters/api_v2/base.py:126). Link hosts come from the requestHost([service:api]/enable_host_header, default true) orapi_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.
| Method | Path | Handler | Policy (enforced in central) | Success | Errors |
|---|---|---|---|---|---|
| GET | /v2/zones | designate/api/v2/controllers/zones/__init__.py:61 | find_zones | 200 | 400, 401, 403 |
| POST | /v2/zones | designate/api/v2/controllers/zones/__init__.py:84 | create_zone, use_low_ttl, use_blacklisted_zone, zone_create_forced_pool | 202 | 400, 403, 409, 413, 500 |
| GET | /v2/zones/{zone_id} | designate/api/v2/controllers/zones/__init__.py:47 | get_zone | 200 | 400, 404 |
| PATCH | /v2/zones/{zone_id} | designate/api/v2/controllers/zones/__init__.py:128 | get_zone, update_zone, use_low_ttl | 202 (200 for non-PENDING) | 400, 403, 404, 409 |
| DELETE | /v2/zones/{zone_id} | designate/api/v2/controllers/zones/__init__.py:192 | delete_zone | 202 | 400, 403, 404 |
| GET | /v2/zones/{zone_id}/recordsets | designate/api/v2/controllers/zones/recordsets.py:67 | get_zone, find_recordsets | 200 | 400, 404 |
| POST | /v2/zones/{zone_id}/recordsets | designate/api/v2/controllers/zones/recordsets.py:78 | create_recordset, use_low_ttl | 202 | 400, 403, 404, 409, 413 |
| GET | /v2/zones/{zone_id}/recordsets/{recordset_id} | designate/api/v2/controllers/zones/recordsets.py:55 | get_recordset | 200 | 400, 404 |
| PUT | /v2/zones/{zone_id}/recordsets/{recordset_id} | designate/api/v2/controllers/zones/recordsets.py:118 | get_recordset, get_zone, update_recordset | 202 | 400, 403, 404, 409, 413 |
| DELETE | /v2/zones/{zone_id}/recordsets/{recordset_id} | designate/api/v2/controllers/zones/recordsets.py:166 | get_recordset, delete_recordset | 202 | 400, 403, 404 |
| GET | /v2/zones/{zone_id}/nameservers | designate/api/v2/controllers/zones/nameservers.py:30 | get_zone_ns_records | 200 | 404 |
| GET | /v2/zones/{zone_id}/shares | designate/api/v2/controllers/zones/sharedzones.py:48 | find_zone_shares, find_project_zone_share | 200 | 403 |
| POST | /v2/zones/{zone_id}/shares | designate/api/v2/controllers/zones/sharedzones.py:73 | share_zone | 201 | 400, 403, 404, 409 |
| GET | /v2/zones/{zone_id}/shares/{zone_share_id} | designate/api/v2/controllers/zones/sharedzones.py:31 | get_zone_share | 200 | 404 |
| DELETE | /v2/zones/{zone_id}/shares/{zone_share_id} | designate/api/v2/controllers/zones/sharedzones.py:101 | get_zone_share, unshare_zone | 204 | 400, 403, 404 |
| POST | /v2/zones/{zone_id}/tasks/abandon | designate/api/v2/controllers/zones/tasks/abandon.py:30 | abandon_zone | 204 (500 if not deleted) | 403, 404 |
| POST | /v2/zones/{zone_id}/tasks/xfr | designate/api/v2/controllers/zones/tasks/xfr.py:29 | get_zone, xfr_zone | 202 | 400, 403, 404 |
| POST | /v2/zones/{zone_id}/tasks/pool_move | designate/api/v2/controllers/zones/tasks/pool_move.py:33 | get_zone, pool_move_zone | 202 (500 if not PENDING) | 400, 403, 404 |
| POST | /v2/zones/{zone_id}/tasks/export | designate/api/v2/controllers/zones/tasks/exports.py:53 | create_zone_export | 202 | 403, 404, 413 |
| POST | /v2/zones/{zone_id}/tasks/transfer_requests | designate/api/v2/controllers/zones/tasks/transfer_requests.py:73 | get_zone, create_zone_transfer_request | 201 | 400, 403, 404, 409 |
| GET | /v2/zones/tasks/transfer_requests | designate/api/v2/controllers/zones/tasks/transfer_requests.py:50 | find_zone_transfer_requests | 200 | 400 |
| GET | /v2/zones/tasks/transfer_requests/{zone_transfer_request_id} | designate/api/v2/controllers/zones/tasks/transfer_requests.py:34 | get_zone_transfer_request | 200 | 403, 404 |
| PATCH | /v2/zones/tasks/transfer_requests/{zone_transfer_request_id} | designate/api/v2/controllers/zones/tasks/transfer_requests.py:112 | get_zone_transfer_request, update_zone_transfer_request | 200 | 400, 403, 404 |
| DELETE | /v2/zones/tasks/transfer_requests/{zone_transfer_request_id} | designate/api/v2/controllers/zones/tasks/transfer_requests.py:144 | delete_zone_transfer_request | 204 | 403, 404 |
| GET | /v2/zones/tasks/transfer_accepts | designate/api/v2/controllers/zones/tasks/transfer_accepts.py:49 | find_zone_transfer_accepts | 200 | 403 |
| POST | /v2/zones/tasks/transfer_accepts | designate/api/v2/controllers/zones/tasks/transfer_accepts.py:71 | get_zone_transfer_request, create_zone_transfer_accept | 201 | 400, 403, 404, 413 |
| GET | /v2/zones/tasks/transfer_accepts/{transfer_accept_id} | designate/api/v2/controllers/zones/tasks/transfer_accepts.py:33 | get_zone_transfer_accept | 200 | 404 |
| GET | /v2/zones/tasks/imports | designate/api/v2/controllers/zones/tasks/imports.py:50 | find_zone_imports | 200 | 400 |
| POST | /v2/zones/tasks/imports | designate/api/v2/controllers/zones/tasks/imports.py:71 | create_zone_import | 202 | 400, 403, 415 |
| GET | /v2/zones/tasks/imports/{import_id} | designate/api/v2/controllers/zones/tasks/imports.py:36 | get_zone_import | 200 | 404 |
| DELETE | /v2/zones/tasks/imports/{zone_import_id} | designate/api/v2/controllers/zones/tasks/imports.py:118 | delete_zone_import | 204 | 404 |
| GET | /v2/zones/tasks/exports | designate/api/v2/controllers/zones/tasks/exports.py:94 | find_zone_exports | 200 | 400 |
| GET | /v2/zones/tasks/exports/{export_id} | designate/api/v2/controllers/zones/tasks/exports.py:81 | get_zone_export | 200 | 404 |
| DELETE | /v2/zones/tasks/exports/{zone_export_id} | designate/api/v2/controllers/zones/tasks/exports.py:119 | delete_zone_export | 204 | 404 |
| GET | /v2/zones/tasks/exports/{export_id}/export | designate/api/v2/controllers/zones/tasks/exports.py:32 | zone_export, get_zone_export, get_zone | 200 text/dns | 400, 403, 404 |
| GET | /v2/recordsets | designate/api/v2/controllers/recordsets.py:48 | find_recordsets | 200 | 400 |
| GET | /v2/recordsets/{recordset_id} | designate/api/v2/controllers/recordsets.py:33 | get_recordset | 301 to the zone URL | 404 |
| GET | /v2/reverse/floatingips | designate/api/v2/controllers/floatingips.py:42 | none (Neutron filters with the user's token) | 200 | 504 |
| GET | /v2/reverse/floatingips/{region}:{floatingip_id} | designate/api/v2/controllers/floatingips.py:80 | none | 200 | 400, 404, 504 |
| PATCH | /v2/reverse/floatingips/{region}:{floatingip_id} | designate/api/v2/controllers/floatingips.py:54 | set_floatingip_reverse | 202 | 400, 403, 404, 409, 504 |
| GET, POST | /v2/tlds | designate/api/v2/controllers/tlds.py:44, designate/api/v2/controllers/tlds.py:66 | find_tlds, create_tld (admin) | 200, 201 | 400, 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:120 | get_tld, update_tld, delete_tld (admin) | 200, 200, 204 | 400, 403, 404, 409 |
| GET, POST | /v2/tsigkeys | designate/api/v2/controllers/tsigkeys.py:45, designate/api/v2/controllers/tsigkeys.py:67 | find_tsigkeys, create_tsigkey (admin) | 200, 201 | 400, 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:123 | get_tsigkey, update_tsigkey, delete_tsigkey (admin) | 200, 200, 204 | 400, 403, 404, 409 |
| GET, POST | /v2/blacklists | designate/api/v2/controllers/blacklists.py:46, designate/api/v2/controllers/blacklists.py:68 | find_blacklists, create_blacklist (admin) | 200, 201 | 400, 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:125 | get_blacklist, update_blacklist, delete_blacklist (admin) | 200, 200, 204 | 400, 403, 404, 409 |
| GET, POST | /v2/pools | designate/api/v2/controllers/pools.py:44, designate/api/v2/controllers/pools.py:66 | find_pools, create_pool (admin; POST deprecated) | 200, 201 | 400, 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:130 | get_pool, update_pool, delete_pool (admin; PATCH/DELETE deprecated) | 200, 202, 204 | 400, 403, 404, 409 |
| GET | /v2/quotas | designate/api/v2/controllers/quotas.py:34 | get_quotas (own project) | 200 | 403 |
| 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:90 | get_quotas, set_quota, reset_quotas | 200, 200, 204 | 400, 401, 403 |
| GET | /v2/limits | designate/api/v2/controllers/limits.py:28 | none | 200 | — |
| GET | /v2/service_statuses (alias /v2/service_status) | designate/api/v2/controllers/service_status.py:27 | find_service_statuses (admin) | 200 | 403 |
| GET | /v2/service_statuses/{service_id} (alias) | designate/api/v2/controllers/service_status.py:46 | find_service_status (admin) | 200 | 403, 404 |
| GET | /admin/reports/counts[/{criterion}] | designate/api/admin/controllers/extensions/counts.py:30 | count_zones, count_records, count_tenants (admin) | 200 | 403, 404 |
| GET | /admin/reports/tenants[/{tenant_id}] | designate/api/admin/controllers/extensions/tenants.py:31 | find_tenants, get_tenant (admin) | 200 | 403 |
| GET, PATCH, DELETE | /admin/quotas/{tenant_id} | designate/api/admin/controllers/extensions/quotas.py:35 | all_tenants + get_quotas, set_quota, reset_quotas | 200, 200, 204 | 400, 403 |
| GET | /admin/zones/export/{zone_id} | designate/api/admin/controllers/extensions/export.py:29 | zone_export, get_zone | 200 text/dns | 403, 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 settingproject_idthroughX-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 withscope=POOLfor that pool (designate/central/service.py:395); quotazones(default 10,designate/conf/base.py:80). - Business rules (all in
designate/central/service.py:848):typedefaults toPRIMARY(designate/api/v2/controllers/zones/__init__.py:92);CATALOGzones cannot be created through the API (designate/central/service.py:3181). ASECONDARYzone needsmastersand gets the email[service:central]/managed_resource_email(designate/api/v2/controllers/zones/__init__.py:98); a PRIMARY one needsemail.- Name: fully qualified, ending with
., at mostmax_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 unlessuse_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) unlessuse_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 → 403illegal_child, super-zone of another project's zone → 403illegal_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(defaultdefault_pool,designate/conf/central.py:46);attributesmatch pool attributes with theattributefilter;attributes.pool_idforces a pool withpool_id_attributeandzone_create_forced_pool(designate/scheduler/filters/pool_id_attribute_filter.py:69). No pool → 404no_valid_pool_found; several → 500multiple_pools_found(designate/scheduler/base.py:81). - SOA
refreshis randomised betweendefault_soa_refresh_minanddefault_soa_refresh_max(3500–3600 s) to spread AXFRs (designate/central/service.py:813).
- Flow:
- (synchronous) Parse and validate the body, call central
create_zoneunder thecreate-new-zonelock. - (synchronous) Insert the zone
CREATE/PENDING, its SOA recordset (MNAME = the pool NS with the lowest priority) and NS recordset, bothmanaged(designate/central/service.py:952); setparent_zone_idof the zone and of existing sub-zones of the same project. - (synchronous) Cast worker
create_zone; for SECONDARY zones alsoperform_zone_xfr(designate/central/service.py:936). - (asynchronous) The worker creates the zone on every pool target, sends NOTIFY, polls the nameservers and reports with
update_status(async).
- (synchronous) Parse and validate the body, call central
- Outcome: 202 with
status: PENDING,action: CREATE,Locationheader; thenACTIVE/NONEorERROR. Clients pollGET /v2/zones/{id}. - Effects visible elsewhere: notifications
dns.zone.createanddns.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 ofhost[: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 keyscreated_at, id, updated_at, name, tenant_id, serial, ttl, status(designate/api/v2/controllers/zones/__init__.py:37). Non-admins never seeCATALOGzones (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) orpool_id**[inferred:pool_idis writable in the adapter but moving a zone is done bytasks/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_idcannot change (designate/central/service.py:1073); changingattributesclearstransferred_at(designate/api/v2/controllers/zones/__init__.py:166). The serial is incremented only for PRIMARY zones when a field other thandescriptionchanged (designate/api/v2/controllers/zones/__init__.py:175, fixed in this branch, see history). - Flow: the zone becomes
UPDATE/PENDINGwithincrement_serial = true; the producer bumps the serial within 5 s and pushes it (async). A newmasterslist 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 → 400zone_is_sharedunlessX-Designate-Delete-Shares: true(designate/central/service.py:1147);X-Designate-Hard-Delete: truealso 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), thenupdate_statussoft-deletes it (deleted = id) (designate/central/service.py:2573); the producer hard-deletes it 7 days later (zone_purge).
Zone tasks
| Operation | What it does | Rules | Defined at |
|---|---|---|---|
POST …/tasks/abandon | delete the zone from Designate only, leaving it on the DNS servers | admin (abandon_zone); 204, or 500 when the zone was not marked deleted | designate/central/service.py:1167 |
POST …/tasks/xfr | for a SECONDARY zone, compare one master's serial and AXFR if newer | non-SECONDARY → 400 Can't XFR a non Secondary zone. | designate/central/service.py:1209 |
POST …/tasks/pool_move | move the zone to pool_id (body) or to the pool the scheduler picks | admin; same pool → 400; unknown pool → 400 Target pool does not exist; NS records of the new pool replace the old ones | designate/central/service.py:1415 |
GET …/nameservers | the pool's NS records (hostname, priority) | reader | designate/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_recordsetallowszone_shared). The target project id is checked in Keystone (designate/api/v2/controllers/zones/sharedzones.py:81, 400invalid_projectif 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 (400shared_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_recordsetrule). - Business rules:
nameis fully qualified and inside the zone (designate/central/service.py:175), not inside a child zone (designate/central/service.py:218), at mostmax_recordset_name_len(255).typeinsupported_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);SOAcannot 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.namewith 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.jsonrrdata); 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/PENDINGwithserial = now; the zone becomesUPDATE/PENDINGwithincrement_serial(designate/central/service.py:1574); the producer bumps the serial and pushes the zone;update_statussets the recordsACTIVEwhen 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) needX-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_recordsetrule,recordset_project_id).
DELETE /v2/zones/{zone_id}/recordsets/{recordset_id}
- 202; the recordset row is deleted at once while its records stay
DELETE/PENDINGuntil 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};statusisINACTIVEwithout 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 → 409duplicate_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 becomesCOMPLETE(message: "<zone> imported",zone_id) orERROR(message, see errors).$ORIGINmust 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_sizequota (1000 recordsets) is checked first (designate/central/service.py:3043). The worker completes it withlocation: 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 withdesignate/resources/templates/export-zone.jinja2(designate/central/service.py:1706); a task whose location is notdesignate://→ 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-characterkey(uppercase letters and digits,designate/central/service.py:2664); one request per zone (409duplicate_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_requestruleNone:%(target_project_id)s).key,project_idandtarget_project_idare 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 → 400Zone Transfer Request has been used, wrong key → 403invalid_key(designate/central/service.py:2781); the accepting project's zone quota is checked; the zone'stenant_idchanges in place (no DNS change); request and accept becomeCOMPLETE(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 (400invalid_tsigkey,designate/central/service.py:703). Thesecretis 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).PATCHanswers 202. Removing the last NS record → 400last_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 (400invalid_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 needsall_projects(designate/central/service.py:615); a non-project token withoutall_projects→ 401missing_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; filtershostname, 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 → 404report_not_found(designate/central/service.py:1253);/admin/reports/tenants→ zone counts per project;/admin/quotas/{tenant_id}(setsall_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 inget_zone_transfer_acceptoperations (_rewrite/route-probe_stable-2026.2.jsonlists five policy operations whose path matches no route). /v2/service_statusis served on purpose as an alias of/v2/service_statuses(bug 1919183,designate/api/v2/controllers/root.py:69).- The
/adminAPI 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.