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

Reverse-spec · Clients

python-designateclient — Errors

Errors — python-designateclient — branch stable/2026.2

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

A CLI has no HTTP errors of its own. A failure reaches the user as a line on stderr and an exit code. Behaviours marked [observed] come from _rewrite/defects-repro_stable-2026.2.json, whose scenarios run the real shell against a stub with answers chosen by the scenario. They show how the client reads such a response, not that a service sends it.

1. Exit codes

CodeWhenSource
0the command returned normally, including on an untranslated 4xx, see defect 3cliff:cliff/command.py:196
1any exception raised in take_action; its str() is logged on stderrcliff:cliff/app.py:462
2argparse error, or unknown command, as with --os-dns-api-version 1 [observed: zone-create-bad-type, api-version-1]cliff:cliff/app.py:428

**No command class returns a value from take_action.** The 12 classes deriving from command.Command all return nothing, which was checked with ast on designateclient/v2/cli/, and those commands exit 0 in {} mode [observed]. The OSC defect "a Command returning a value becomes an exit code" does not exist here.

2. Exception classes

All are in designateclient/exceptions.py.

ClassParentHTTP statusRaised atSituationSeen by the user as
BaseExceptiondesignateclient/exceptions.py:18never directly; an empty message becomes the class name
UnsupportedVersionBasedesignateclient/client.py:134client.Client(version) with a version that is not the string '2'; the integer 2 fails too [observed: python-api]Python API only
ResourceNotFoundBasedesignateclient/utils.py:84find_resourceid_by_name_or_id, which only its own test callsnot reachable from a command
NoUniqueMatchBasedesignateclient/v2/utils.py:40a name matches several resourcesMultiple matches found for {name}, please use ID instead., exit 1, defect 4
NoUniqueMatchBasedesignateclient/utils.py:88same dead helpernot reachable from a command
RemoteErrorBasedesignateclient/exceptions.py:37parent of the next ones
BadRequestRemoteError400designateclient/v2/client.py:108message, exit 1
ForbiddenRemoteError401 and 403designateclient/v2/client.py:110a 401 first triggers one re-authentication and one retry in keystoneauth (keystoneauth1:keystoneauth1/session.py:1130) [observed: 401: 2 tokens, 2 requests]message, exit 1
NotFoundRemoteError404designateclient/v2/client.py:112message, exit 1
NotFoundRemoteErrordesignateclient/v2/utils.py:38a name lookup returns nothing; no HTTP error involvedName <x> didn't resolve, exit 1
ConflictRemoteError409designateclient/v2/client.py:114message, exit 1
OverQuotaRemoteError413designateclient/v2/client.py:116message, exit 1
UnknownRemoteErrorany ≥ 500designateclient/v2/client.py:118message, exit 1

Translation

DesignateAdapter.request (designateclient/v2/client.py:57) forces raise_exc=False, then:

  1. it parses the body as JSON; if that fails, the payload becomes {code, message: <body text>} (designateclient/v2/client.py:98), so an HTML page is shown as it is [observed: 503-html, 404-html];
  2. it raises the class of the status with **payload (designateclient/v2/client.py:107).

Which text is shown (designateclient/exceptions.py:49), in order:

  1. message;
  2. else the message of each item of errors.errors, joined by . [observed: 400-validation gives bad ttl. bad email];
  3. else type [observed: 400-type-only];
  4. else the class name [observed: 404-other-shape gives NotFound].

Read in Designate, the fault body holds code, type, message or errors, and request_id (designate:designate/api/middleware.py:276). request_id is stored on the exception and not displayed.

3. Client-side errors

MessageExceptionSituationRaised at
Zone type PRIMARY requires --email.CommandErrorzone create of a PRIMARY zone without --email; no requestdesignateclient/v2/cli/zones.py:181
Type <t> is not supported. Please choose between PRIMARY or SECONDARYCommandErrorunreachable through the CLI, since argparse restricts --typedesignateclient/v2/cli/zones.py:193
Attribute '<a>' is in an incorrect format. Attributes are <key>:<value> formattedCommandErrorzone create --attributes with zero or several :designateclient/v2/cli/zones.py:173
sameCommandErrorzone import create --attributesdesignateclient/v2/cli/zones.py:747
[Errno 2] No such file or directory: ...OSErrorzone import create with a missing file [observed: import-missing-file]designateclient/v2/cli/zones.py:736
A session instance is requiredValueErrorClient() without a session; Python API only [observed: python-api]designateclient/v2/client.py:129
public endpoint for dns service in RegionOne region not foundkeystoneauthno dns service in the catalog [observed: no-dns-in-catalog]keystoneauth1:keystoneauth1/access/service_catalog.py:535
Could not find requested endpoint in Service Catalog.keystoneauthdiscovery offers no v2 [observed: discovery-only-v3]keystoneauth1:keystoneauth1/exceptions/catalog.py:24

Bad option values are refused by argparse with exit 2. --ttl on zone list and recordset list is a plain string, not an integer (designateclient/v2/cli/zones.py:61, designateclient/v2/cli/recordsets.py:56).

4. Defects

Each one was reproduced on the stub or read in the code, as stated. Scenario names refer to _rewrite/defects-repro_stable-2026.2.json. None is caught by the unit tests. Whether a real Designate triggers the case is stated only where its code was read; it was never run.

#DefectEvidenceSource
1**ptr record set --no-description and --no-ttl do nothing.** The code builds a data dict with the null values, then ignores it and passes parsed_args.description and parsed_args.ttl. The body holds only ptrdname, exit 0, so the user believes the field was cleared.[observed] ptr-no-description, ptr-no-ttldesignateclient/v2/cli/reverse.py:99, designateclient/v2/cli/reverse.py:114
2**--ttl 0 and --description "" are dropped on set commands.** if parsed_args.ttl: treats 0 as absent. zone set --ttl 0 sends {} and recordset set --ttl 0 sends {}, both exit 0. zone create --ttl 0 and ptr record set --ttl 0 do send 0, so the commands disagree. TLD, blacklist, transfer and TSIG set commands drop empty strings the same way.[observed] zone-set-ttl-0, recordset-set-ttl-0, zone-create-ttl-0, ptr-ttl-0, zone-set-empty-descriptiondesignateclient/v2/cli/zones.py:234, designateclient/v2/cli/zones.py:239, designateclient/v2/cli/recordsets.py:223, designateclient/v2/cli/zones.py:188, designateclient/v2/reverse.py:28
3Any 4xx other than 400, 401, 403, 404, 409 and 413 counts as success. The adapter has no branch for it and returns the response. A Command exits 0 and prints nothing: zone blacklist delete on a 405, zone abandon on a 429. A ShowOne displays the fault as if it were the resource: tld create on a 415 prints code, message, request_id, type and exits 0; zone export showfile on a 406 prints the fault as data. A list on a 405 prints 'zones', exit 1. Read in Designate, 405, 406 and 415 exist (designate:designate/exceptions.py:199, designate:designate/exceptions.py:204, designate:designate/exceptions.py:363).[observed] 405-delete-exit0, 429-abandon-exit0, 415-tld-create-exit0, 406-showfile, 405-zone-listdesignateclient/v2/client.py:107
4**The ambiguity message prints a literal {name}.** The string lacks its f prefix: Multiple matches found for {name}, please use ID instead.[observed] name-zone-many, name-zone-many-recordset-listdesignateclient/v2/utils.py:41
5**recordset list --action X sends a filter that Designate refuses.** The client sends ?action=X. Read in Designate, action is not in the accepted filters, and an unaccepted filter raises BadRequest Invalid filters action. The option looks usable and would fail at every use [inferred: Designate was read, not run]. The other filters of all commands are accepted, per the same reading.[observed] for the request, recordset-list-action-filter; read for the refusaldesignateclient/v2/cli/recordsets.py:102, designate:designate/api/v2/controllers/common.py:25, designate:designate/api/v2/controllers/rest.py:73
6**dns quota set and dns quota reset without --project-id send X-Auth-All-Projects: True for the user's own project.** They compare parsed_args.project_id, which is None, with the current project. dns quota list compares after the fallback and does not do this; with --project-id <own> the header is absent too. Read in Designate, a true value triggers a policy check on all_tenants, so the implicit form can need more rights than the explicit one [inferred].[observed] quota-set-own-implicit, quota-set-own-explicit, quota-reset-own-implicit, quota-list-owndesignateclient/v2/cli/quotas.py:102, designateclient/v2/cli/quotas.py:128, designateclient/v2/cli/quotas.py:58, designate:designate/context.py:153
7**With a token that has no project, the quota commands target a project called None.** PATCH /quotas/None and DELETE /quotas/None, since get_project_id() returns None and goes through an f-string. Exit 0 on the stub. dns quota list falls back to GET /quotas.[observed] quota-set-domain-scope, quota-reset-domain-scope, quota-list-domain-scopedesignateclient/v2/cli/quotas.py:100, designateclient/v2/cli/quotas.py:126, designateclient/v2/quotas.py:25
8Headers stick from one command to the next in the same process. The client is cached (osc-lib:osc_lib/clientmanager.py:51), and set_all_common_headers only overwrites what the current command parsed. zone delete --sudo-project-id other, then zone list, sends the sudo header on the list. After zone delete --hard-delete, recordset delete still carries X-Designate-Hard-Delete: True; after recordset set --edit-managed, a later zone delete carries the edit-managed header. A normal one-shot openstack run is not affected. The interactive shell is, since it calls run_subcommand on the same app (cliff:cliff/interactive.py:83). The scenario calls run_subcommand directly; the interactive loop itself was not run, because it reads a terminal. A Python user who reuses a Client after calling the same helpers would see this too [inferred].[observed] same-process-sticky-headers, same-process-sticky-hard-deletedesignateclient/v2/cli/common.py:86, designateclient/v2/cli/common.py:90, designateclient/v2/cli/common.py:81
9No limit on pagination. get_all loops as long as the response has a next key. A next that repeats, or a next: null, loops forever: urlparse(None) gives an empty query, so the same request goes out again. The stub stopped each loop after 200 requests. Read in Designate, a real next can appear on a last full page, then an empty page ends the loop (designate:designate/objects/adapters/api_v2/base.py:124). links: null gives argument of type 'NoneType' is not iterable.[observed] pagination-endless, pagination-next-null, list-links-nulldesignateclient/v2/utils.py:70, designateclient/v2/base.py:36
10**dns service list|show lose the values of stats and capabilities.** '\n'.join(status[k]) on an object keeps the keys only: {"zones": 3} shows zones. Read in Designate, these fields are objects (designate:designate/objects/service_status.py:32). With an empty value, - is shown.[observed] service-stats-dictdesignateclient/v2/cli/service_statuses.py:33
11Crash after the action succeeded, when the success body is empty. A delete or set that answers 202 with no body gives a raw Python message and exit 1, after the change was accepted: zone delete prints 'str' object has no attribute 'pop', recordset delete prints string indices must be integers, not 'str'. ptr record set and zone share create do the same. Read in Designate, these routes do return a body (designate:designate/api/v2/controllers/zones/recordsets.py:183), except a floating IP update may not (designate:designate/api/v2/controllers/floatingips.py:76). It binds a re-implementation of the service.[observed] zone-delete-empty-202, recordset-delete-empty-202, ptr-set-empty-202, zone-share-create-emptydesignateclient/v2/client.py:105, designateclient/v2/cli/zones.py:275, designateclient/v2/cli/recordsets.py:263, designateclient/v2/cli/reverse.py:120
12**KeyError on a missing response key shows only the key name**, exit 1: 'zones', 'masters', 'attributes', 'records', 'capabilities', 'id'.[observed] all list commands in {} mode; zone-no-attributes, service-list-no-statsdesignateclient/v2/base.py:34, designateclient/v2/cli/zones.py:32, designateclient/v2/cli/recordsets.py:31, designateclient/v2/cli/service_statuses.py:33, designateclient/v2/cli/zones.py:605
13**An empty object on a ShowOne gives not enough values to unpack (expected 2, got 0)**, exit 1. 20 commands behave this way in {} mode.[observed] quota-empty-object, _rewrite/wire-probe-200_stable-2026.2.jsondesignateclient/v2/cli/zones.py:446, cliff:cliff/display.py:160
14**--attributes values cannot hold a colon.** k:v:w is refused, although a value such as a URL or a timestamp is legitimate.[observed] zone-create-attribute-colondesignateclient/v2/cli/zones.py:170, designateclient/v2/cli/zones.py:744
15**A JSON fault that is not an object gives a TypeError text**: designateclient.exceptions.BadRequest() argument after ** must be a mapping, not list.[observed] 400-json-listdesignateclient/v2/client.py:108
16**Subclassing DesignateAdapter recurses forever.** super(self.__class__, self) resolves to the subclass. Same pattern in AdapterWithTimeout. Python API only.[observed] for DesignateAdapter, python-api; read for the otherdesignateclient/v2/client.py:55, designateclient/v2/client.py:95, designateclient/utils.py:105
17**utils.get_columns always returns [].** The map calls are lazy and never consumed. Nothing in the package calls it.[observed] python-apidesignateclient/utils.py:68
18**dns quota reset logs Quota for project None was reset** at INFO without --project-id.readdesignateclient/v2/cli/quotas.py:133

Behaviours that are by design, though surprising: set commands with no option still send a request [observed: zone-set-nothing, recordset-set-nothing, quota-set-nothing]; zone create drops options that do not apply to the type (01 § 2).

5. Asynchronous errors

The client has none of its own, as it exits after the first answer. A zone or recordset that later goes to ERROR is only visible through status in zone show, zone list or recordset show. A failed import or export shows in the status and message of zone import show or zone export show. See 05 and reverse-spec-designate.

6. Timeouts and transport errors

  • The plugin sets no timeout (designateclient/osc/plugin.py:39); only the session's applies, from --timeout (openstacksdk:openstack/config/loader.py:936, osc-lib:osc_lib/clientmanager.py:125) [inferred: not followed to the session]. Client(timeout=...) exists for the Python API (designateclient/v2/client.py:60), and designateclient/tests/v2/test_timeout.py:31 tests it.
  • Connection failures are keystoneauth exceptions, not translated by the adapter: their text is shown with exit 1 [inferred, not reproduced].

7. Logs

The plugin logs at INFO only, with messages such as Zone %s was deleted, visible with -v (designateclient/v2/cli/zones.py:273). The warnings seen come from OSC, for a wrong API version, and from keystoneauth, for a failed discovery.

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