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

Reverse-spec · Clients

python-designateclient — Rewrite pack

Client contract (rewrite pack) — python-designateclient — branch stable/2026.2

Branch stable/2026.2 @ 9231eef90192 — repository https://opendev.org/openstack/python-designateclient — analysed on 2026-11-01

This pack serves two purposes:

  • Keep the commands and the Python API working against a re-implemented DNS service, by stating what they require.
  • Re-implement the commands, from the machine-readable catalogue.

No replay against a real service was possible. Everything on the wire comes from a local stub, and the requirements below are what the client does with a response, not what Designate does. See § 5.

1. Contents of _rewrite/

FileContentUse
[cli-commands_stable-2026.2.json](_rewrite/cli-commands_stable-2026.2.json)the 60 commands: class, purpose, each of the 265 arguments (names, type, default, choices, required, exclusive groups, help), client calls, controller routes with file:line, errors raisedgenerate a parser; check argument names
[client-contract_stable-2026.2.json](_rewrite/client-contract_stable-2026.2.json)per command: client methods, observed requests with headers and body, static routes, what the response must hold, unit vector count, exit codes per stub moderequirements per command
[api-surface_stable-2026.2.md](_rewrite/api-surface_stable-2026.2.md)the 61 operations with their commands, whether they exist in Designate's route list, the 2 Python-only entries, and the Designate routes never calledgap analysis of a service
[unit-vectors_stable-2026.2.json](_rewrite/unit-vectors_stable-2026.2.json)97 vectors. kind: api: Python call → HTTP method, URL, headers, body. kind: command: argv → calls on the mock. Normalised ({uuid})test wire format without Python
[wire-probe-404_stable-2026.2.json](_rewrite/wire-probe-404_stable-2026.2.json), [wire-probe-200_stable-2026.2.json](_rewrite/wire-probe-200_stable-2026.2.json), [wire-probe-fixture_stable-2026.2.json](_rewrite/wire-probe-fixture_stable-2026.2.json)for each command: argv, exit code, stderr, requests with status, headers and body. Stub answers a fault, {}, or a plausible bodywire reference
[defects-repro_stable-2026.2.json](_rewrite/defects-repro_stable-2026.2.json)103 scenarios with chosen answers: argv, exit code, stdout, stderr, requestsevidence for 02 § 4 and for § 2 below
ScriptRole
[extract_plugin_stable-2026.2.py](_rewrite/extract_plugin_stable-2026.2.py)imports the entry points, dumps parsers, walks the code with ast, resolves controller methods to routes. Adapted: controller_rest reads the controllers
[probe_plugin_stable-2026.2.py](_rewrite/probe_plugin_stable-2026.2.py)local stub and real shell. Adapted: the version document follows designate:designate/api/versions.py:37, faults follow Designate's shape, a fixture mode, UUIDs for zone arguments, a temporary zone file, and a reset of the client cache between commands
[repro_defects_stable-2026.2.py](_rewrite/repro_defects_stable-2026.2.py)the scenarios, written for this package
[capture_plugin_stable-2026.2.py](_rewrite/capture_plugin_stable-2026.2.py)runs the unit tests with a recorder. Adapted: also records requests-mock history
[build_contract_stable-2026.2.py](_rewrite/build_contract_stable-2026.2.py)merges everything and compares with Designate's routes
[inventory.py](_rewrite/inventory.py), [check_refs.py](_rewrite/check_refs.py)copies of the skill's scripts, unchanged

The generic rewrite_pack.py and conformance_capture.py were not used: they expect a service or functional tests running in-process, while the functional tests here need a live cloud (designateclient/functionaltests/config.py:21).

Versions: python-designateclient 7.0.1.dev2, osc-lib 4.7.0, cliff 4.16.0, python-openstackclient 10.3.1, keystoneauth1 5.17.0, requests-mock 1.12.1, Python 3.13.

To regenerate:

python -I -B _rewrite/extract_plugin_stable-2026.2.py <repo> python-designateclient designateclient _rewrite
for m in 404 200 fixture; do python -I -B _rewrite/probe_plugin_stable-2026.2.py _rewrite/cli-commands_stable-2026.2.json <dir-$m> --mode $m; done
python -I -B _rewrite/repro_defects_stable-2026.2.py _rewrite/probe_plugin_stable-2026.2.py <repro.json>
python -I -B _rewrite/capture_plugin_stable-2026.2.py <repo> _rewrite/cli-commands_stable-2026.2.json <run-1>/c.json <repo>/designateclient/tests <repo>
python -I -B _rewrite/capture_plugin_stable-2026.2.py <repo> _rewrite/cli-commands_stable-2026.2.json <run-2>/c.json <repo>/designateclient/tests <repo>
python -I -B _rewrite/build_contract_stable-2026.2.py . <dir-404>/wire-probe_stable-2026.2.json <dir-200>/wire-probe_stable-2026.2.json <dir-fixture>/wire-probe_stable-2026.2.json <run-1>/c.json <run-2>/c.json <repro.json> <designate-routes.json> <gen-dir>

Set HOME to an empty directory and unset OS_*, so that no real clouds.yaml is read. The stub binds to 127.0.0.1.

2. What the client requires from a DNS service

Apply this to any service that must work with the unmodified client. "Client" evidence is on the stub; the service column of Designate is what was read.

#RequirementWhyEvidence
1a dns entry in the catalog, with the interface used (default public)otherwise public endpoint for dns service ... not found[observed] no-dns-in-catalog
2either the catalog URL has no version and its root answers a version list with a v2 or v2.x entry and a self link to .../v2, or the catalog URL ends in /v2a root 404 sends the client to /zones without /v2[observed] discovery-*, designate:designate/api/versions.py:37
3all routes of [api-surface](_rewrite/api-surface_stable-2026.2.md), verbs included: recordset set is PUT, zone set and others are PATCH[observed]
4a GET /zones/{id} and similar accept a UUID; ?name= filters exactly on zones, recordsets, TLDs and TSIG keys, and answers 200 with an empty list when nothing matchesthe name lookup; a 404 there shows a different messagedesignateclient/v2/utils.py:32
5collections always hold their key: zones, recordsets, transfer_requests, transfer_accepts, exports, imports, shared_zones, tlds, tsigkeys, blacklists, floatingips, service_statuses, nameserversotherwise a bare key name on stderr, exit 1[observed] wire-probe-200
6links, when present, is an object; links.next is present only when there is more to read, is never null, and carries the query to re-send, including a changed markerthe client loops forever otherwise[observed] pagination-*, list-links-null
7zone objects hold masters (list) and attributes (object) in every answer, including PATCH and DELETE_format_zone reads them without a default[observed] zone-no-attributes, designateclient/v2/cli/zones.py:32
8recordset objects hold records (list), including PUT and DELETE; zone delete, recordset delete, recordset set, ptr record set, zone share create and every create answer with a bodyotherwise a Python error, exit 1, after the action[observed] *-empty-*
9service statuses hold capabilities and stats, and quota, TLD and similar objects are non-emptyformatter and zip[observed] service-list-no-stats, quota-empty-object
10failures use only 400, 401, 403, 404, 409, 413 or ≥ 500, if the user is to see themany other 4xx is displayed as success[observed] defect 3. Designate itself uses 405, 406 and 415
11fault bodies are JSON objects holding message, or errors.errors[].message, or typethe text shown; a non-object gives a TypeError text[observed] 400-*, 404-other-shape
12POST /zones/tasks/imports accepts text/dns, and application/json with {zonefile, attributes}both forms exist[observed] import-*, designate:designate/api/v2/controllers/zones/tasks/imports.py:79
13GET .../export honours Accept: text/dns and answers the zone textshown as data[observed] hdr-export-accept
14Content-Type: application/json on a GET or DELETE without body is toleratedsent on every request[observed]
15the header values True and true are parsed as booleans; the four feature headers are honouredscripts rely on the flags[observed] hdr-*, designate:designate/api/middleware.py:67
16X-Auth-All-Projects: True works on a quota call for the caller's own projectsent implicitly by dns quota set and reset[observed] quota-*-own-implicit
17GET /quotas works without a project ID; /quotas/None should not be taken for a projectwhat a token without project produces[observed] quota-*-domain-scope
18floating IP keys region:id are accepted with a raw colon in the pathsent as is[observed]
19a success status is not checked: 200, 201, 202 and 204 are all fine, as long as the bodies of 8 are theredesignateclient/v2/client.py:119
20recordset list --action will send ?action=. Designate refuses it; decide whether to accept itcompatibility choicedefect 5

Fields the commands display but do not need are listed per command in the class columns. A missing one gives an empty cell, not an error.

What the Python API users add

RequirementSource
keep designateclient.client.Client('2', session=, endpoint_type=, service_type=, region_name=) and v2.client.Client(session=, auth=, endpoint_override=, edit_managed=, all_projects=)heat:heat/engine/clients/os/designate.py:32, neutron:neutron/services/externaldns/drivers/designate/driver.py:46
keep the exception classes and the status mapping: Neutron turns OverQuota into its own error and ignores a Conflict on zone creationneutron:neutron/services/externaldns/drivers/designate/driver.py:107
keep name resolution in recordsets.*: Neutron passes zone names and recordset namesneutron:neutron/services/externaldns/drivers/designate/driver.py:165

3. Coverage against Designate

Static comparison with reverse-spec-designate/_rewrite/routes_stable-2026.2.json: 61 of 61 client operations exist there. Of its 78 routes, 16 are never called by the client: the /admin API, pool administration, GET /recordsets/{id}, the /service_status alias, and GET /zones/{id}/tasks/abandon. GET /pools is reachable only through the Python API. This compares method and path only, with no bodies, and nothing was run.

4. For a rewrite of the commands

TopicRuleSource
Nameszone_transfer_request_create → zone transfer request create; scripts depend on these and on argument names, including --service_namepyproject.toml:42, designateclient/v2/cli/service_statuses.py:47
Exit codes0, 1, 2 as in 02 § 1; do not copy defect 3cliff:cliff/command.py:196
Columnslist columns are fixed per class; --long adds some; --all-projects inserts project_iddesignateclient/v2/cli/zones.py:50
Rows of ShowOnesorted by key, links removed except on transfers and quotasdesignateclient/v2/cli/zones.py:133
Name resolutionzones, recordsets, TLDs, TSIG keys only01 § 1
Defects not to copy1, 2, 3, 4, 6, 7, 8, 9, 10, 1402 § 4

Recommendation: keep the Python client and fix it upstream rather than rewriting it. It is small (60 commands, thin controllers), and Neutron and Heat call its Python API. A Go rewrite would not serve them.

5. Conformance: what was proved

EvidenceResultWhat it provesWhat it does not prove
Parser dump60 of 60 classes imported, 0 failuresthe catalogue matches pyproject.toml
Unit tests with recorder111 of 111 pass, 97 vectors, in two runsPython call → HTTP against requests-mock (87), argv → mock (10)anything about a service; only 5 of 60 command classes have command vectors
Comparison of the two runs97 of 97: same test, status and sequence, with HTTP content compared for API vectorsstability, after normalising UUIDsargument values of command vectors were not compared
Wire probe, 404 mode60 of 60 reach a request; all exit 1 with the fault textroutes up to the first error
Wire probe, {} mode60 of 60 reach a request; 15 exit 0, 45 exit 1 (20 unpacking errors, 25 missing keys)which commands read what
Wire probe, fixture mode60 of 60 exit 0 with hand-written bodiescommands complete on plausible bodiesthat the bodies are right
Defect scenarios103 run, reproducing the defects of 02 § 4 except 18, read onlyclient reaction to chosen answersthat a service sends them, unless read in Designate
Route comparison61 of 61names of routes existbodies, semantics

Replay against a real service, the usual bar, is not met. The mocks also hide real defects: none of the 18 defects is caught by a unit test.

To close the gap: run the probe's fixture commands, with real resources, against a devstack on stable/2026.2, and compare statuses and keys; or run designateclient/functionaltests against it. Neither was attempted, as the rules for this work allow only the local stub.

Command classes without a command vector

55 of 60. Vectors exist only for CreateRecordSetCommand, ListRecordSetsCommand, ListTLDsCommand, CreateZoneCommand, ListZonesCommand. The controllers behind the others mostly have API vectors, which do not test argument parsing. Zone shares, exports and transfers do have API vectors [see unit-vectors].

6. Gaps found while building the pack

  • The generic stub's version document pointed at /dns/, without /v2, so every command would have gone to /dns/zones [inferred; the unfixed stub was not run]. It was replaced by the shape of Designate before the first probe. Scenario discovery-root-404 then gave requirement 2.
  • The tool's logging.disable hid every error message, so it was removed.
  • OSC caches the client on the class (osc-lib:osc_lib/clientmanager.py:51). Without a reset, one in-process run leaked --sudo-project-id into later scenarios, which is how defect 8 was found.
  • capture_plugin records check_parser only, and would have missed the 87 API tests. It was extended.
  • Name lookups are not in the wire probes, since those pass UUIDs. They are in defects-repro.

7. Non-functional requirements

  • Latency: each openstack invocation asks for a token and reads the version document, so these two are on the hot path. A script of N commands sends N token requests.
  • Name arguments cost one to two extra requests per command.
  • Listing cost: lists read all pages; the page size is the service's.
  • Payload: zone import create reads the whole file into memory, then sends it (designateclient/v2/cli/zones.py:737).

8. Checklist for a DNS service

  • [ ] dns in the catalog, with a public endpoint.
  • [ ] Version document at the catalog root, or /v2 in the catalog URL.
  • [ ] Every route of api-surface_stable-2026.2.md implemented, or its absence documented.
  • [ ] Collection keys always present; links.next only when there is more.
  • [ ] masters, attributes, records, capabilities, stats, id always present where read.
  • [ ] Delete and set answers carry a body.
  • [ ] Failures use the translated statuses, or the gap is accepted knowingly.
  • [ ] Fault bodies are objects with message.
  • [ ] text/dns import and export, JSON import with attributes.
  • [ ] Feature headers honoured, values True and true accepted.
  • [ ] The probe in fixture mode's commands, pointed at the service, exit 0 with real resources.

Source: reverse-spec-python-designateclient/07-rewrite-pack_stable-2026.2.md. The _rewrite/ and _inventory/ files are not published on the site.