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

Reverse-spec · Clients

python-ironicclient — Summary

Reverse-specification of python-ironicclient — branch stable/2026.2

Branch stable/2026.2 @ af2ceeb9c4f2 (6.3.0-2-gaf2ceeb, installed as version 6.3.1.dev2) — repository https://opendev.org/openstack/python-ironicclient — analysed on 2026-11-01

  • Scope: all non-test code of the repository: the openstack baremetal plugin, the Python API it uses (ironicclient.v1) and the separate baremetal shell. The change history (step 7) was not requested, so there is no file 06.
  • Dependencies read at fixed versions: osc-lib 4.7.0, cliff 4.16.0, python-openstackclient 10.3.1, keystoneauth1 5.17.0, openstacksdk 4.20.0, requests 2.34.2. They are cited with the prefixes osc-lib:, cliff:, openstackclient:, keystoneauth1: and requests:. The server side is cited as ironic: (Ironic branch stable/2026.2 @ 39ce09ec3), read only, never run.
  • References are file:line from the repository root. [observed] marks a behaviour seen by running the real CLI against a local stub on 127.0.0.1. [inferred] marks a statement not followed to the end.
  • Observed behaviour says nothing about a real Ironic. The stub answers {}, a 404, or what a scenario scripts. An [observed] result shows how the client reacts to that response. No Ironic, lab or cloud was contacted.

Files of this specification

FileContent
01-cli-commands_stable-2026.2.mdshared mechanics, functional description per resource, and the 118 commands with class, arguments and observed requests
02-errors_stable-2026.2.mdexit codes, how failures reach the user, exception classes, error messages, and the defects found
03-interactions_stable-2026.2.mdKeystone, Ironic (headers, negotiation, retries, pagination), local files, stdin, local binaries
04-scenarios_stable-2026.2.md5 scenarios with sequence diagrams and diagnostics
05-async-processing_stable-2026.2.mdno background processing; the client-side polling that exists
07-rewrite-pack_stable-2026.2.mdclient contract: what the client requires from a Bare Metal service, test data, proof and limits
_verification_stable-2026.2.mdoutput of the reference checker

Overview

python-ironicclient is the client of Ironic, the OpenStack Bare Metal service. It gives three ways in:

  1. **The openstack baremetal … commands**, a plugin of python-openstackclient. This is the main interface.
  2. **The baremetal shell**, a separate program with the same commands, without the baremetal prefix. It works without Keystone.
  3. A Python API (ironicclient.client.get_client, then client.node.list() and so on). Both shells call it, and it holds the HTTP, retry and negotiation logic.

It uses neither openstacksdk proxies nor a generated client: ironicclient/common/http.py:368 is its own keystoneauth adapter, and the ironicclient/v1/*.py managers build each URL by hand. It has no database, bus, server or background process.

Architecture

flowchart LR
  user[User or script] -->|argv, env, stdin| osc[openstack process]
  user -->|argv, env, stdin| legacy[baremetal process]
  cfg[clouds.yaml, OS_* variables] --> osc
  cfg --> legacy
  osc -->|entry points| cmds[ironicclient.osc.v1 commands]
  legacy -->|same entry points| cmds
  cmds --> mgr[ironicclient.v1 managers]
  mgr --> http[SessionClient]
  http --> ksa[keystoneauth1 session]
  ksa -->|HTTPS, X-Auth-Token| ks[Keystone]
  ksa -->|HTTPS, X-OpenStack-Ironic-API-Version| ironic[Ironic]
  http <--> cache[(ironic-api-version.dbm)]
  mgr -.->|config drive from a directory| iso[genisoimage, mkisofs or xorrisofs]

Processes

ProcessEntry pointRoleListens to / consumes
openstack (python-openstackclient)plugin baremetal = ironicclient.osc.plugin at pyproject.toml:45, commands from pyproject.toml:47runs one commandargv, environment, stdin for - arguments; listens on nothing
baremetalpyproject.toml:42 → ironicclient/shell.py:192same commands, prefix stripped (ironicclient/shell.py:95), default auth type none (ironicclient/shell.py:33)same

External dependencies

SystemProtocolLibraryConfigurationUsage
KeystoneHTTPS RESTkeystoneauth1--os-auth-url, clouds.yamltoken and catalog; the baremetal shell can skip it with --os-auth-type none
IronicHTTPS RESTironicclient/common/http.py:368catalog type baremetal, or baremetal_endpoint_override (osc-lib:osc_lib/clientmanager.py:285), or --os-endpoint on the baremetal shellevery command
Local cache filedbm through dogpile.cacheironicclient/common/filecache.py:63IRONICCLIENT_CACHE_EXPIRYnegotiated version; written by both shells, read only by the Python API (see point 11)
genisoimage, mkisofs, xorrisofschild processironicclient/common/utils.py:282PATHonly for --config-drive <directory>

Key figures

ItemCount
Commands in pyproject.toml118 (116 classes: 2 old trait names are aliases), 546 arguments
Commands reaching a request on the stub118 of 118, in both 404 and {} modes; 397 further runs with one option each
Routes in the surface114: 89 from plain commands, 6 only with an option, 19 only from the Python API
Latest known API version1.115 (ironicclient/common/http.py:45), which matches Ironic (ironic:ironic/api/controllers/v1/versions.py:288)
Client-side version gates7 sites, all for runbooks at 1.112; everything else is left to the server
Exception classes43 in ironicclient/common/apiclient/exceptions.py, plus 4 in ironicclient/exc.py
Unit tests1,155 of 1,155 pass in two runs; 632 vectors, on 110 of 116 classes
Scripted scenarios85, in _rewrite/scenarios_stable-2026.2.json
Defects reproduced13, see 02 § 5

Points of attention

  1. **baremetal driver passthru call fails on any JSON answer [observed]**: the command gives a Driver object to dict2columns, which needs a dict (ironicclient/osc/v1/baremetal_driver.py:208, cliff:cliff/show.py:70). The stderr says only id and the exit code is 1, after the vendor method has run. A script will retry an action that already happened. The unit test mocks the return with a plain dict (ironicclient/tests/unit/osc/v1/test_baremetal_driver.py:298), so it passes.
  2. **baremetal node children list shows only the first child [observed]**: it is a ShowOne with one column, but returns one row per child (ironicclient/osc/v1/baremetal_node.py:2640), and cliff pairs columns with values. With children c1 and c2, the output holds ['c1'] and the exit code is 0.
  3. **A Retry-After header turns a 429 or 503 into a Python error [observed]**: from_response passes retry_after to a constructor that only RequestEntityTooLarge accepts (ironicclient/common/apiclient/exceptions.py:449, ironicclient/common/apiclient/exceptions.py:304). The user reads HttpError.__init__() got an unexpected keyword argument 'retry_after', and the retry on 503 does not happen. A proxy in front of the service can trigger this [inferred]; Ironic's API code has no Retry-After of its own (grep of ironic/api).
  4. **The error_message field must be a string [observed]**: an object gives the JSON object must be str, bytes or bytearray, not dict (ironicclient/common/http.py:106). Ironic sends a string (ironic:ironic/api/middleware/parsable_error.py:71). A compatible service must do the same: this is the first rule of the client contract.
  5. Writes are retried silently [observed]: 409 and 503 are retried 5 times, 2 seconds apart, on every method including POST (ironicclient/common/http.py:324). baremetal node create against a constant 409 sent 6 POST in 10 seconds. The plugin offers no option for this (ironicclient/osc/plugin.py:77); only the baremetal shell has --max-retries (ironicclient/shell.py:139).
  6. Query values are not URL-encoded [observed]: --description-contains 'a&retired=true b' sent ?description_contains=a&retired=true%20b, which adds a retired filter (ironicclient/v1/node.py:1493).
  7. **--name 123 becomes a JSON number on set, but stays a string on create [observed]**: set commands parse each value as JSON (ironicclient/common/utils.py:66). --description true sends a boolean.
  8. **baremetal inspection rule delete all does not delete all rules [observed]**: the test compares a list with a string, so it is never true (ironicclient/osc/v1/baremetal_inspection_rule.py:329). The client sends DELETE /v1/inspection_rules/all. These six commands have no unit test at all.
  9. **--config-drive <base64 string> is refused [observed]**: the help offers it (ironicclient/osc/v1/baremetal_node.py:37), but any string that is not a file, a directory or a JSON object raises ValueError before any request (ironicclient/v1/node.py:797).
  10. **Without --limit, a list returns only the first page [observed]**: next is followed only when a limit is given, including --limit 0 (ironicclient/v1/node.py:1502). The help hints at this; a script counting nodes on a large cloud gets at most the server's page size.
  11. The negotiation cache does nothing for the two shells [observed]: both always pass a version, and the cache is read only when none is given (ironicclient/v1/client.py:64). Against an older server, every process sends the 406 round trip again.
  12. Show commands crash on an empty object [observed]: 200 {} gives 'NoneType' object has no attribute '_info' on 15 commands. Ironic does not answer that for a resource [inferred]; a partial implementation might.
  13. **--wait without a timeout never ends if nothing changes**: timeout 0 means no limit (ironicclient/common/utils.py:438). With --wait, a node that still shows the expected state just after the request counts as done [observed for service, ironicclient/v1/node.py:1134].

Blind spots / [inferred]

  • No real service was contacted, as instructed. Nothing here proves that a command works against Ironic, or that a body is accepted by it. Statements about Ironic come from reading its code.
  • The probes stop early: the stub has no resources, so anything after the first response is known only from scripted scenarios, or from reading. Output columns were not checked against real data.
  • Not run: --config-drive <directory>, which executes a local binary; argument values were synthetic everywhere else. Authentication with anything but a password against the stub was not tried. clouds.yaml was not exercised, since HOME was empty.
  • **baremetal shell**: only 8 scenarios ran through it, plus the shared code. It was not probed command by command.
  • **ironicclient/common/apiclient/base.py** lazy loading: seen to do nothing when the response has no id [observed, shape-create-partial-response]; a response with an id field was not tried.
  • Server-side microversion requirements per option are not listed here, because the client does not hold them. See the Ironic spec in reverse-spec-ironic/.
  • Inspection rule commands have no unit tests, so their vectors are absent. Their requests come from the probes only.
  • Output formats (-f json, -c, and so on) belong to cliff and were not specified per command.

Source: reverse-spec-python-ironicclient/00-summary_stable-2026.2.md. The _rewrite/ and _inventory/ files are not published on the site.