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
| Code | When | Source |
|---|---|---|
| 0 | the command returned normally, including on an untranslated 4xx, see defect 3 | cliff:cliff/command.py:196 |
| 1 | any exception raised in take_action; its str() is logged on stderr | cliff:cliff/app.py:462 |
| 2 | argparse 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.
| Class | Parent | HTTP status | Raised at | Situation | Seen by the user as |
|---|---|---|---|---|---|
Base | Exception | designateclient/exceptions.py:18 | never directly; an empty message becomes the class name | ||
UnsupportedVersion | Base | designateclient/client.py:134 | client.Client(version) with a version that is not the string '2'; the integer 2 fails too [observed: python-api] | Python API only | |
ResourceNotFound | Base | designateclient/utils.py:84 | find_resourceid_by_name_or_id, which only its own test calls | not reachable from a command | |
NoUniqueMatch | Base | designateclient/v2/utils.py:40 | a name matches several resources | Multiple matches found for {name}, please use ID instead., exit 1, defect 4 | |
NoUniqueMatch | Base | designateclient/utils.py:88 | same dead helper | not reachable from a command | |
RemoteError | Base | designateclient/exceptions.py:37 | parent of the next ones | ||
BadRequest | RemoteError | 400 | designateclient/v2/client.py:108 | message, exit 1 | |
Forbidden | RemoteError | 401 and 403 | designateclient/v2/client.py:110 | a 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 |
NotFound | RemoteError | 404 | designateclient/v2/client.py:112 | message, exit 1 | |
NotFound | RemoteError | designateclient/v2/utils.py:38 | a name lookup returns nothing; no HTTP error involved | Name <x> didn't resolve, exit 1 | |
Conflict | RemoteError | 409 | designateclient/v2/client.py:114 | message, exit 1 | |
OverQuota | RemoteError | 413 | designateclient/v2/client.py:116 | message, exit 1 | |
Unknown | RemoteError | any ≥ 500 | designateclient/v2/client.py:118 | message, exit 1 |
Translation
DesignateAdapter.request (designateclient/v2/client.py:57) forces raise_exc=False, then:
- 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]; - it raises the class of the status with
**payload(designateclient/v2/client.py:107).
Which text is shown (designateclient/exceptions.py:49), in order:
message;- else the
messageof each item oferrors.errors, joined by.[observed:400-validationgivesbad ttl. bad email]; - else
type[observed:400-type-only]; - else the class name [observed:
404-other-shapegivesNotFound].
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
| Message | Exception | Situation | Raised at |
|---|---|---|---|
Zone type PRIMARY requires --email. | CommandError | zone create of a PRIMARY zone without --email; no request | designateclient/v2/cli/zones.py:181 |
Type <t> is not supported. Please choose between PRIMARY or SECONDARY | CommandError | unreachable through the CLI, since argparse restricts --type | designateclient/v2/cli/zones.py:193 |
Attribute '<a>' is in an incorrect format. Attributes are <key>:<value> formatted | CommandError | zone create --attributes with zero or several : | designateclient/v2/cli/zones.py:173 |
| same | CommandError | zone import create --attributes | designateclient/v2/cli/zones.py:747 |
[Errno 2] No such file or directory: ... | OSError | zone import create with a missing file [observed: import-missing-file] | designateclient/v2/cli/zones.py:736 |
A session instance is required | ValueError | Client() without a session; Python API only [observed: python-api] | designateclient/v2/client.py:129 |
public endpoint for dns service in RegionOne region not found | keystoneauth | no 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. | keystoneauth | discovery 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.
| # | Defect | Evidence | Source |
|---|---|---|---|
| 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-ttl | designateclient/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-description | designateclient/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 |
| 3 | Any 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-list | designateclient/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-list | designateclient/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 refusal | designateclient/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-own | designateclient/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-scope | designateclient/v2/cli/quotas.py:100, designateclient/v2/cli/quotas.py:126, designateclient/v2/quotas.py:25 |
| 8 | Headers 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-delete | designateclient/v2/cli/common.py:86, designateclient/v2/cli/common.py:90, designateclient/v2/cli/common.py:81 |
| 9 | No 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-null | designateclient/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-dict | designateclient/v2/cli/service_statuses.py:33 |
| 11 | Crash 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-empty | designateclient/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-stats | designateclient/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.json | designateclient/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-colon | designateclient/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-list | designateclient/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 other | designateclient/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-api | designateclient/utils.py:68 |
| 18 | **dns quota reset logs Quota for project None was reset** at INFO without --project-id. | read | designateclient/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), anddesignateclient/tests/v2/test_timeout.py:31tests 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.